patterncsharpMinor
Converting a list to a CSV string
Viewed 0 times
convertingliststringcsv
Problem
I want to be able to convert a list of objects into a string csv format. I've written this extension method below but have a feeling I'm missing something as this seems like potentially a common thing to want to do.
Is there anything I can do to improve this or refactor to a more elegant or working solution. Or even an existing method out there already that I may have missed.
UPDATE: Updated to take into account quotations as per Jesse comments below.
private static readonly char[] csvChars = new[] { ',', '"', ' ', '\n', '\r' };
public static string ToCsv(this IEnumerable source, Func getItem)
{
if ((source == null) || (getItem == null))
{
return string.Empty;
}
var builder = new StringBuilder();
var items = from item in source.Select(getItem)
where item != null
select item.ToString();
foreach (var str in items)
{
if (str.IndexOfAny(csvChars) > 0)
{
builder.Append("\"").Append(str).Append("\"").Append(", ");
}
else
{
builder.Append(str).Append(", ");
}
}
var csv = builder.ToString();
return csv.Length > 0 ? csv.TrimEnd(", ".ToCharArray()) : csv;
}Is there anything I can do to improve this or refactor to a more elegant or working solution. Or even an existing method out there already that I may have missed.
UPDATE: Updated to take into account quotations as per Jesse comments below.
Solution
If your items contain a comma, carriage return or other special CSV character, you must delimit it with quotation marks.
namespace CsvStuff
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
internal static class CsvConstants
{
public static char[] TrimEnd { get; } = { ' ', ',' };
public static char[] CsvChars { get; } = { ',', '"', ' ', '\n', '\r' };
}
public abstract class CsvBase
{
private readonly IEnumerable values;
private readonly Func getItem;
protected CsvBase(IEnumerable values, Func getItem)
{
this.values = values;
this.getItem = getItem;
}
public override string ToString()
{
var builder = new StringBuilder();
foreach (var item in
from element in this.values.Select(this.getItem)
where element != null
select element.ToString())
{
this.Build(builder, item).Append(", ");
}
return builder.ToString().TrimEnd(CsvConstants.TrimEnd);
}
protected abstract StringBuilder Build(StringBuilder builder, string item);
}
public class CsvBare : CsvBase
{
public CsvBare(IEnumerable values, Func getItem) : base(values, getItem)
{
}
protected override StringBuilder Build(StringBuilder builder, string item)
{
return builder.Append(item);
}
}
public sealed class CsvTrimBare : CsvBare
{
public CsvTrimBare(IEnumerable values, Func getItem) : base(values, getItem)
{
}
protected override StringBuilder Build(StringBuilder builder, string item)
{
return base.Build(builder, item.Trim());
}
}
public class CsvRfc4180 : CsvBase
{
public CsvRfc4180(IEnumerable values, Func getItem) : base(values, getItem)
{
}
protected override StringBuilder Build(StringBuilder builder, string item)
{
item = item.Replace("\"", "\"\"");
return item.IndexOfAny(CsvConstants.CsvChars) >= 0
? builder.Append("\"").Append(item).Append("\"")
: builder.Append(item);
}
}
public sealed class CsvTrimRfc4180 : CsvRfc4180
{
public CsvTrimRfc4180(IEnumerable values, Func getItem) : base(values, getItem)
{
}
protected override StringBuilder Build(StringBuilder builder, string item)
{
return base.Build(builder, item.Trim());
}
}
public class CsvAlwaysQuote : CsvBare
{
public CsvAlwaysQuote(IEnumerable values, Func getItem) : base(values, getItem)
{
}
protected override StringBuilder Build(StringBuilder builder, string item)
{
return builder.Append("\"").Append(item.Replace("\"", "\"\"")).Append("\"");
}
}
public sealed class CsvTrimAlwaysQuote : CsvAlwaysQuote
{
public CsvTrimAlwaysQuote(IEnumerable values, Func getItem) : base(values, getItem)
{
}
protected override StringBuilder Build(StringBuilder builder, string item)
{
return base.Build(builder, item.Trim());
}
}
public static class CsvExtensions
{
public static string ToCsv(this IEnumerable source, Func getItem, Type csvProcessorType)
{
if ((source == null)
|| (getItem == null)
|| (csvProcessorType == null)
|| !csvProcessorType.IsSubclassOf(typeof(CsvBase)))
{
return string.Empty;
}
return csvProcessorType
.GetConstructor(new[] { source.GetType(), getItem.GetType() })
?.Invoke(new object[] { source, getItem })
.ToString();
}
private static void Main()
{
var words = new[] { ",this", " is ", "a", "test", "Super, \"luxurious\" truck" };
Console.WriteLine(words.ToCsv(word => word, typeof(CsvAlwaysQuote)));
Console.WriteLine(words.ToCsv(word => word, typeof(CsvRfc4180)));
Console.WriteLine(words.ToCsv(word => word, typeof(CsvBare)));
Console.WriteLine(words.ToCsv(word => word, typeof(CsvTrimAlwaysQuote)));
Console.WriteLine(words.ToCsv(word => word, typeof(CsvTrimRfc4180)));
Console.WriteLine(words.ToCsv(word => word, typeof(CsvTrimBare)));
Console.ReadLine();
}
}
}Code Snippets
namespace CsvStuff
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
internal static class CsvConstants
{
public static char[] TrimEnd { get; } = { ' ', ',' };
public static char[] CsvChars { get; } = { ',', '"', ' ', '\n', '\r' };
}
public abstract class CsvBase<T>
{
private readonly IEnumerable<T> values;
private readonly Func<T, object> getItem;
protected CsvBase(IEnumerable<T> values, Func<T, object> getItem)
{
this.values = values;
this.getItem = getItem;
}
public override string ToString()
{
var builder = new StringBuilder();
foreach (var item in
from element in this.values.Select(this.getItem)
where element != null
select element.ToString())
{
this.Build(builder, item).Append(", ");
}
return builder.ToString().TrimEnd(CsvConstants.TrimEnd);
}
protected abstract StringBuilder Build(StringBuilder builder, string item);
}
public class CsvBare<T> : CsvBase<T>
{
public CsvBare(IEnumerable<T> values, Func<T, object> getItem) : base(values, getItem)
{
}
protected override StringBuilder Build(StringBuilder builder, string item)
{
return builder.Append(item);
}
}
public sealed class CsvTrimBare<T> : CsvBare<T>
{
public CsvTrimBare(IEnumerable<T> values, Func<T, object> getItem) : base(values, getItem)
{
}
protected override StringBuilder Build(StringBuilder builder, string item)
{
return base.Build(builder, item.Trim());
}
}
public class CsvRfc4180<T> : CsvBase<T>
{
public CsvRfc4180(IEnumerable<T> values, Func<T, object> getItem) : base(values, getItem)
{
}
protected override StringBuilder Build(StringBuilder builder, string item)
{
item = item.Replace("\"", "\"\"");
return item.IndexOfAny(CsvConstants.CsvChars) >= 0
? builder.Append("\"").Append(item).Append("\"")
: builder.Append(item);
}
}
public sealed class CsvTrimRfc4180<T> : CsvRfc4180<T>
{
public CsvTrimRfc4180(IEnumerable<T> values, Func<T, object> getItem) : base(values, getItem)
{
}
protected override StringBuilder Build(StringBuilder builder, string item)
{
return base.Build(builder, item.Trim());
}
}
public class CsvAlwaysQuote<T> : CsvBare<T>
{
public CsvAlwaysQuote(IEnumerable<T> values, Func<T, object> getItem) : base(values, getItem)
{
}
protected override StringBuilder Build(StringBuilder builder, string item)
{
return builder.Append("\"").Append(item.Replace("\"", "\"Context
StackExchange Code Review Q#8228, answer score: 7
Revisions (0)
No revisions yet.