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<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("\"", "\"\"")).Append("\"");
}
}
public sealed class CsvTrimAlwaysQuote<T> : CsvAlwaysQuote<T>
{
public CsvTrimAlwaysQuote(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 static class CsvExtensions
{
public static string ToCsv<T>(this IEnumerable<T> source, Func<T, object> getItem, Type csvProcessorType)
{
if ((source == null)
|| (getItem == null)
|| (csvProcessorType == null)
|| !csvProcessorType.IsSubclassOf(typeof(CsvBase<T>)))
{
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<string>)));
Console.WriteLine(words.ToCsv(word => word, typeof(CsvRfc4180<string>)));
Console.WriteLine(words.ToCsv(word => word, typeof(CsvBare<string>)));
Console.WriteLine(words.ToCsv(word => word, typeof(CsvTrimAlwaysQuote<string>)));
Console.WriteLine(words.ToCsv(word => word, typeof(CsvTrimRfc4180<string>)));
Console.WriteLine(words.ToCsv(word => word, typeof(CsvTrimBare<string>)));
Console.ReadLine();
}
}
}