Converting a generic list to a CSV string

Viewed 196238

I have a list of integer values (List) and would like to generate a string of comma delimited values. That is all items in the list output to a single comma delimted list.

My thoughts... 1. pass the list to a method. 2. Use stringbuilder to iterate the list and append commas 3. Test the last character and if it's a comma, delete it.

What are your thoughts? Is this the best way?

How would my code change if I wanted to handle not only integers (my current plan) but strings, longs, doubles, bools, etc, etc. in the future? I guess make it accept a list of any type.

16 Answers

It's amazing what the Framework already does for us.

List<int> myValues;
string csv = String.Join(",", myValues.Select(x => x.ToString()).ToArray());

For the general case:

IEnumerable<T> myList;
string csv = String.Join(",", myList.Select(x => x.ToString()).ToArray());

As you can see, it's effectively no different. Beware that you might need to actually wrap x.ToString() in quotes (i.e., "\"" + x.ToString() + "\"") in case x.ToString() contains commas.

For an interesting read on a slight variant of this: see Comma Quibbling on Eric Lippert's blog.

Note: This was written before .NET 4.0 was officially released. Now we can just say

IEnumerable<T> sequence;
string csv = String.Join(",", sequence);

using the overload String.Join<T>(string, IEnumerable<T>). This method will automatically project each element x to x.ToString().

You can create an extension method that you can call on any IEnumerable:

public static string JoinStrings<T>(
    this IEnumerable<T> values, string separator)
{
    var stringValues = values.Select(item =>
        (item == null ? string.Empty : item.ToString()));
    return string.Join(separator, stringValues.ToArray());
}

Then you can just call the method on the original list:

string commaSeparated = myList.JoinStrings(", ");

You can use String.Join.

String.Join(
  ",",
  Array.ConvertAll(
     list.ToArray(),
     element => element.ToString()
  )
);

For whatever reason, @AliUmair reverted the edit to his answer that fixes his code that doesn't run as is, so here is the working version that doesn't have the file access error and properly handles null object property values:

/// <summary>
/// Creates the CSV from a generic list.
/// </summary>;
/// <typeparam name="T"></typeparam>;
/// <param name="list">The list.</param>;
/// <param name="csvNameWithExt">Name of CSV (w/ path) w/ file ext.</param>;
public static void CreateCSVFromGenericList<T>(List<T> list, string csvCompletePath)
{
    if (list == null || list.Count == 0) return;

    //get type from 0th member
    Type t = list[0].GetType();
    string newLine = Environment.NewLine;

    if (!Directory.Exists(Path.GetDirectoryName(csvCompletePath))) Directory.CreateDirectory(Path.GetDirectoryName(csvCompletePath));

    using (var sw = new StreamWriter(csvCompletePath))
    {
        //make a new instance of the class name we figured out to get its props
        object o = Activator.CreateInstance(t);
        //gets all properties
        PropertyInfo[] props = o.GetType().GetProperties();

        //foreach of the properties in class above, write out properties
        //this is the header row
        sw.Write(string.Join(",", props.Select(d => d.Name).ToArray()) + newLine);

        //this acts as datarow
        foreach (T item in list)
        {
            //this acts as datacolumn
            var row = string.Join(",", props.Select(d => $"\"{item.GetType().GetProperty(d.Name).GetValue(item, null)?.ToString()}\"")
                                                    .ToArray());
            sw.Write(row + newLine);

        }
    }
}

Here is my extension method, it returns a string for simplicity but my implementation writes the file to a data lake.

It provides for any delimiter, adds quotes to string (in case they contain the delimiter) and deals will nulls and blanks.

    /// <summary>
    /// A class to hold extension methods for C# Lists 
    /// </summary>
    public static class ListExtensions
    {
        /// <summary>
        /// Convert a list of Type T to a CSV
        /// </summary>
        /// <typeparam name="T">The type of the object held in the list</typeparam>
        /// <param name="items">The list of items to process</param>
        /// <param name="delimiter">Specify the delimiter, default is ,</param>
        /// <returns></returns>
        public static string ToCsv<T>(this List<T> items, string delimiter = ",")
        {
            Type itemType = typeof(T);
            var props = itemType.GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(p => p.Name);

            var csv = new StringBuilder();

            // Write Headers
            csv.AppendLine(string.Join(delimiter, props.Select(p => p.Name)));

            // Write Rows
            foreach (var item in items)
            {
                // Write Fields
                csv.AppendLine(string.Join(delimiter, props.Select(p => GetCsvFieldasedOnValue(p, item))));
            }

            return csv.ToString();
        }

        /// <summary>
        /// Provide generic and specific handling of fields
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="p"></param>
        /// <param name="item"></param>
        /// <returns></returns>
        private static object GetCsvFieldasedOnValue<T>(PropertyInfo p, T item)
        {
            string value = "";

            try
            {
                value = p.GetValue(item, null)?.ToString();
                if (value == null) return "NULL";  // Deal with nulls
                if (value.Trim().Length == 0) return ""; // Deal with spaces and blanks

                // Guard strings with "s, they may contain the delimiter!
                if (p.PropertyType == typeof(string))
                {
                    value = string.Format("\"{0}\"", value);
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
            return value;
        }
    }

Usage:

 // Tab Delimited (TSV)
 var csv = MyList.ToCsv<MyClass>("\t");

The other answers work, but my issue is loading unknown data from the database, so I needed something a bit more robust than what's already here.

I wanted something that fit the following requirements:

  • able to be opened in excel
  • had to be able to handle date time formats in an excel compatible way
  • had to automatically exclude linked entities (EF navigation properties)
  • had to support column contents containing " and the delimiter ,
  • had to support nullable columns
  • had to support a wide array of data types
    • numbers of every kind
    • guids
    • datetimes
    • custom type definitions (ie name from a linked entity)

I used month/day/year formats for the date exports for compatibility reasons

public static IReadOnlyDictionary<System.Type, Func<object, string>> CsvTypeFormats = new Dictionary<System.Type, Func<object, string>> {
    // handles escaping column delimiter (',') and quote marks
    { typeof(string), x => string.IsNullOrWhiteSpace(x as string) ? null as string : $"\"{(x as string).Replace("\"", "\"\"")}\""},
    { typeof(DateTime), x => $"{x:M/d/yyyy H:m:s.fff}" },
    { typeof(DateTime?), x => x == null ? "" : $"{x:M/d/yyyy H:m:s.fff}" },
    { typeof(DateTimeOffset), x => $"{x:M/d/yyyy H:m:s.fff}" },
    { typeof(DateTimeOffset?), x => x == null ? "" : $"{x:M/d/yyyy H:m:s.fff}" },
};
public void WriteCsvContent<T>(ICollection<T> data, StringBuilder writer, IDictionary<System.Type, Func<object, string>> explicitMapping = null)
{
    var typeMappings = CsvTypeFormats.ToDictionary(x=>x.Key, x=>x.Value);
    if (explicitMapping != null) {
        foreach(var mapping in explicitMapping) {
            typeMappings[mapping.Key] = mapping.Value;
        }
    }
    var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
        .Where(x => IsSimpleType(x.PropertyType))
        .ToList();
    // header row
    writer.AppendJoin(',', props.Select(x => x.Name));
    writer.AppendLine();
    foreach (var item in data)
    {
        writer.AppendJoin(',',
            props.Select(prop => typeMappings.ContainsKey(prop.PropertyType)
                ? typeMappings[prop.PropertyType](prop.GetValue(item))
                : prop.GetValue(item)?.ToString() ?? ""
            )
            // escaping and special characters
            .Select(x => x != null && x != "" ? $"\"{x.Replace("\"", "\"\"")}\"" : null)
        );
        writer.AppendLine();
    }
}
private bool IsSimpleType(System.Type t)
{
    return
      t.IsPrimitive ||
      t.IsValueType ||
      t.IsEnum ||
      (t == typeof(string)) ||
      CsvTypeFormats.ContainsKey(t);
}

If your class uses fields instead of properties, change the GetProperties to GetFields and the PropertyType accessors to FieldType

Related