Domanda

Tipico modo di creare un file CSV stringa (pseudocodice):

  1. Crea un oggetto contenitore CSV (come uno StringBuilder in C#).
  2. Passa in rassegna le stringhe che desideri aggiungere aggiungendo una virgola dopo ciascuna di esse.
  3. Dopo il ciclo, rimuovi l'ultima virgola superflua.

Esempio di codice:

public string ReturnAsCSV(ContactList contactList)
{
    StringBuilder sb = new StringBuilder();
    foreach (Contact c in contactList)
    {
        sb.Append(c.Name + ",");
    }

    sb.Remove(sb.Length - 1, 1);
    //sb.Replace(",", "", sb.Length - 1, 1)

    return sb.ToString();
}

Mi piace l'idea di aggiungere la virgola controllando se il contenitore è vuoto, ma ciò non significa una maggiore elaborazione in quanto è necessario controllare la lunghezza della stringa ad ogni occorrenza?

Sento che dovrebbe esserci un modo più semplice/pulito/efficiente per rimuovere l'ultima virgola.Qualche idea?

È stato utile?

Soluzione

Potresti usare LINQ agli oggetti:

string [] strings = contactList.Select(c => c.Name).ToArray();
string csv = string.Join(",", strings);

Ovviamente tutto ciò potrebbe essere fatto in una riga, ma è un po' più chiaro su due.

Altri suggerimenti

Il tuo codice non è realmente conforme formato CSV completo.Se stai semplicemente generando CSV da dati senza virgole, spazi iniziali/finali, tabulazioni, ritorni a capo o virgolette, dovrebbe andare bene.Tuttavia, nella maggior parte degli scenari di scambio dati reali, è necessaria l'implementazione completa.

Per generare un CSV corretto, puoi utilizzare questo:

public static String EncodeCsvLine(params String[] fields)
{
    StringBuilder line = new StringBuilder();

    for (int i = 0; i < fields.Length; i++)
    {
        if (i > 0)
        {
            line.Append(DelimiterChar);
        }

        String csvField = EncodeCsvField(fields[i]);
        line.Append(csvField);
    }

    return line.ToString();
}

static String EncodeCsvField(String field)
{
    StringBuilder sb = new StringBuilder();
    sb.Append(field);

    // Some fields with special characters must be embedded in double quotes
    bool embedInQuotes = false;

    // Embed in quotes to preserve leading/tralining whitespace
    if (sb.Length > 0 && 
        (sb[0] == ' ' || 
         sb[0] == '\t' ||
         sb[sb.Length-1] == ' ' || 
         sb[sb.Length-1] == '\t' ))
    {
        embedInQuotes = true;
    }

    for (int i = 0; i < sb.Length; i++)
    {
        // Embed in quotes to preserve: commas, line-breaks etc.
        if (sb[i] == DelimiterChar || 
            sb[i]=='\r' || 
            sb[i]=='\n' || 
            sb[i] == '"') 
        { 
            embedInQuotes = true;
            break;
        }
    }

    // If the field itself has quotes, they must each be represented 
    // by a pair of consecutive quotes.
    sb.Replace("\"", "\"\"");

    String rv = sb.ToString();

    if (embedInQuotes)
    {
        rv = "\"" + rv + "\"";
    }

    return rv;
}

Potrebbe non essere il codice più efficiente del mondo, ma è stato testato.Il mondo reale fa schifo rispetto al codice di esempio rapido :)

Perché non utilizzare una delle librerie CSV open source disponibili?

So che sembra eccessivo per qualcosa che sembra così semplice, ma come puoi vedere dai commenti e dagli snippet di codice, c'è di più di quanto sembri.Oltre a gestire la piena conformità CSV, alla fine vorrai gestire sia la lettura che la scrittura dei CSV...e potresti voler manipolare i file.

ho usato Apri CSV su uno dei miei progetti prima (ma ce ne sono molti altri tra cui scegliere).Sicuramente mi ha reso la vita più facile.;)

Non dimenticare il nostro vecchio amico "per".Non è bello come foreach ma ha il vantaggio di poter iniziare dal secondo elemento.

public string ReturnAsCSV(ContactList contactList)
{
    if (contactList == null || contactList.Count == 0)
        return string.Empty;

    StringBuilder sb = new StringBuilder(contactList[0].Name);

    for (int i = 1; i < contactList.Count; i++)
    {
        sb.Append(",");
        sb.Append(contactList[i].Name);
    }

    return sb.ToString();
}

Potresti anche racchiudere il secondo Append in un "if" che verifica se la proprietà Name contiene virgolette doppie o virgole e, in tal caso, eseguirne l'escape appropriato.

Potresti invece aggiungere la virgola come prima cosa all'interno del tuo foreach.

if (sb.Length > 0) sb.Append(",");

Potresti anche creare una serie di c.Nome dati e utilizzo String.Join metodo per creare la tua linea.

public string ReturnAsCSV(ContactList contactList)
{
    List<String> tmpList = new List<string>();

    foreach (Contact c in contactList)
    {
        tmpList.Add(c.Name);
    }

    return String.Join(",", tmpList.ToArray());
}

Questo potrebbe non essere performante come il StringBuilder approccio, ma sembra decisamente più pulito.

Inoltre, potresti prendere in considerazione l'utilizzo .CurrentCulture.TextInfo.ListSeparator invece di una virgola codificata: se il tuo output verrà importato in altre applicazioni, potresti avere problemi con esso.ListSeparator può essere diverso a seconda delle culture e, almeno, MS Excel rispetta questa impostazione.COSÌ:

return String.Join(
    System.Globalization.CultureInfo.CurrentCulture.TextInfo.ListSeparator,
    tmpList.ToArray());

Mi piace l'idea di aggiungere la virgola controllando se il contenitore è vuoto, ma ciò non significa una maggiore elaborazione in quanto è necessario controllare la lunghezza della stringa ad ogni occorrenza?

Stai ottimizzando prematuramente, il calo delle prestazioni sarebbe trascurabile.

Solo un pensiero, ma ricorda di gestire le virgole e le virgolette ("") nei valori dei campi, altrimenti il ​​tuo file CSV potrebbe interrompere il lettore dei consumatori.

Ho scritto una piccola lezione per questo nel caso in cui qualcun altro lo trovi utile...

public class clsCSVBuilder
{
    protected int _CurrentIndex = -1;
    protected List<string> _Headers = new List<string>();
    protected List<List<string>> _Records = new List<List<string>>();
    protected const string SEPERATOR = ",";

    public clsCSVBuilder() { }

    public void CreateRow()
    {
        _Records.Add(new List<string>());
        _CurrentIndex++;
    }

    protected string _EscapeString(string str)
    {
        return string.Format("\"{0}\"", str.Replace("\"", "\"\"")
                                            .Replace("\r\n", " ")
                                            .Replace("\n", " ")
                                            .Replace("\r", " "));
    }

    protected void _AddRawString(string item)
    {
        _Records[_CurrentIndex].Add(item);
    }

    public void AddHeader(string name)
    {
        _Headers.Add(_EscapeString(name));
    }

    public void AddRowItem(string item)
    {
        _AddRawString(_EscapeString(item));
    }

    public void AddRowItem(int item)
    {
        _AddRawString(item.ToString());
    }

    public void AddRowItem(double item)
    {
        _AddRawString(item.ToString());
    }

    public void AddRowItem(DateTime date)
    {
        AddRowItem(date.ToShortDateString());
    }

    public static string GenerateTempCSVPath()
    {
        return Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString().ToLower().Replace("-", "") + ".csv");
    }

    protected string _GenerateCSV()
    {
        StringBuilder sb = new StringBuilder();

        if (_Headers.Count > 0)
        {
            sb.AppendLine(string.Join(SEPERATOR, _Headers.ToArray()));
        }

        foreach (List<string> row in _Records)
        {
            sb.AppendLine(string.Join(SEPERATOR, row.ToArray()));
        }

        return sb.ToString();
    }

    public void SaveAs(string path)
    {
        using (StreamWriter sw = new StreamWriter(path))
        {
            sw.Write(_GenerateCSV());
        }
    }
}

Ho già usato questo metodo.La proprietà Length di StringBuilder NON è di sola lettura, quindi sottrarla di uno significa troncare l'ultimo carattere.Ma devi assicurarti che la tua lunghezza non sia zero per cominciare (cosa che accadrebbe se la tua lista fosse vuota) perché impostare la lunghezza su un valore inferiore a zero è un errore.

public string ReturnAsCSV(ContactList contactList)
{
    StringBuilder sb = new StringBuilder();

    foreach (Contact c in contactList)       
    { 
        sb.Append(c.Name + ",");       
    }

    if (sb.Length > 0)  
        sb.Length -= 1;

    return sb.ToString();  
}

Io uso CSVHelper - è un'ottima libreria open source che ti consente di generare flussi CSV conformi un elemento alla volta o di mappare in modo personalizzato le tue classi:

public string ReturnAsCSV(ContactList contactList)
{
    StringBuilder sb = new StringBuilder();
    using (StringWriter stringWriter = new StringWriter(sb))
    {
        using (var csvWriter = new CsvHelper.CsvWriter(stringWriter))
        {
            csvWriter.Configuration.HasHeaderRecord = false;
            foreach (Contact c in contactList)
            {
                csvWriter.WriteField(c.Name);
            }
        }
    }
    return sb.ToString();
}

o se mappi qualcosa del genere: csvWriter.WriteRecords<ContactList>(contactList);

Che ne dici di un po' di rifinitura?

public string ReturnAsCSV(ContactList contactList)
{
    StringBuilder sb = new StringBuilder();

    foreach (Contact c in contactList)
    {
        sb.Append(c.Name + ",");
    }

    return sb.ToString().Trim(',');
}

Che ne dici di monitorare se sei sul primo elemento e aggiungere solo una virgola Prima l'elemento se non è il primo.

public string ReturnAsCSV(ContactList contactList)
{
    StringBuilder sb = new StringBuilder();
    bool isFirst = true;

    foreach (Contact c in contactList) {
        if (!isFirst) { 
          // Only add comma before item if it is not the first item
          sb.Append(","); 
        } else {
          isFirst = false;
        }

        sb.Append(c.Name);
    }

    return sb.ToString();
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top