HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Converting a list to a CSV string

Submitted by: @import:stackexchange-codereview··
0
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.

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.