Converting a list to a CSV string
$begingroup$
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<T>(this IEnumerable<T> source, Func<T, object> 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.
c# csv
$endgroup$
add a comment |
$begingroup$
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<T>(this IEnumerable<T> source, Func<T, object> 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.
c# csv
$endgroup$
$begingroup$
You are excluding null values. Shouldn't a null value still be represented in the CSV output? Depends on what you're using this for, I suppose.
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 17:48
add a comment |
$begingroup$
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<T>(this IEnumerable<T> source, Func<T, object> 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.
c# csv
$endgroup$
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<T>(this IEnumerable<T> source, Func<T, object> 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.
c# csv
c# csv
edited Aug 24 '15 at 22:04
200_success
129k15152415
129k15152415
asked Jan 23 '12 at 20:47
drezadreza
5,9422241
5,9422241
$begingroup$
You are excluding null values. Shouldn't a null value still be represented in the CSV output? Depends on what you're using this for, I suppose.
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 17:48
add a comment |
$begingroup$
You are excluding null values. Shouldn't a null value still be represented in the CSV output? Depends on what you're using this for, I suppose.
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 17:48
$begingroup$
You are excluding null values. Shouldn't a null value still be represented in the CSV output? Depends on what you're using this for, I suppose.
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 17:48
$begingroup$
You are excluding null values. Shouldn't a null value still be represented in the CSV output? Depends on what you're using this for, I suppose.
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 17:48
add a comment |
3 Answers
3
active
oldest
votes
$begingroup$
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();
}
}
}
$endgroup$
$begingroup$
Excellent. Have edited my post to reflect your suggestions.
$endgroup$
– dreza
Jan 23 '12 at 21:43
$begingroup$
I made an edit to my code. IndexOfAny() should be >= 0 instead of > 0. Whoops!
$endgroup$
– Jesse C. Slicer
Jan 23 '12 at 22:22
$begingroup$
There's a chance that I may not want to include quotations. Would you suggest just adding a boolean flag to the method parameter to handle this case.
$endgroup$
– dreza
Jan 23 '12 at 22:34
$begingroup$
Hm. Boolean flag would likely be the quickest, but it's not a very object-oriented solution. Let me noodle on that for a few and put something up.
$endgroup$
– Jesse C. Slicer
Jan 23 '12 at 22:37
1
$begingroup$
@JesseC.Slicer +1 for the comprehensive example! But I think there's one more thing to add for it to be complete: if the value enclosed in quotes contains a quote itself, then that quote has to be doubled.
$endgroup$
– Cristian Lupascu
Jan 24 '12 at 15:14
|
show 3 more comments
$begingroup$
Here is my suggested version, which you should take as coming from a fellow learner.
ToCsv is doing too much. I would be inclined to keep it very simple
and perform only its very narrow responsibility: properly
transforming a sequence of strings to a properly formatted csv
string. Let the caller transform its sequence of arbitrary objects
into strings.String.Join exists in the BCL, which is exactly suited to this
purpose.- Properly encodes the quote character
private class Program
{
private static void Main(string args)
{
int someInts = {1, 2, 3, 4, 10, 9, 8};
string someStrings = {"one", "two", "seven", "eight"};
string specialStrings = {"o'ne", ""tw"o", ",,three", "fo,ur", "five"};
Console.WriteLine(someInts.Select(s => s.ToString()).ToCsv());
Console.WriteLine(someStrings.ToCsv());
Console.WriteLine(specialStrings.ToCsv());
}
}
public static class CsvHelpers
{
private static readonly char csvSpecialChars = new { ',', '"', ' ', 'n', 'r' };
public static string ToCsv(this IEnumerable<string> source)
{
if (source == null)
{
return string.Empty;
}
var encodedStrings = from item in source
select EncodeCsvField(item);
return string.Join(",", encodedStrings);
}
private static string EncodeCsvField(string input)
{
string encodedString = input.Replace(""", """");
if (input.IndexOfAny(csvSpecialChars) >= 0)
{
encodedString = """ + encodedString + """;
}
return encodedString;
}
}
$endgroup$
$begingroup$
I sort of wanted to stay away from putting the dependancy on the string object itself in the IEnumerable hence the use of generics. However didn't really know about the Join operater. Thanks.
$endgroup$
– dreza
Jan 24 '12 at 20:13
$begingroup$
My reasoning was that you either provide the function to project from your object to string or just do the projection (possibly through LINQ so you could defer the execution) at the call site. Seemed simpler to just do the projection at the call site if needed so that the extension method has only one responsibility. Cheers.
$endgroup$
– WorkerThread
Jan 24 '12 at 21:13
$begingroup$
Simple, short, clean, correct. Thanks.
$endgroup$
– mmdemirbas
Mar 6 '12 at 11:29
add a comment |
$begingroup$
A couple of things:
Although you are using a StringBuilder (which is good in this case), you're generating a string for each row of data. If you're intending to use this to create multiple rows of data, then it will not quite as efficient as it could be. Maybe this is no big deal, although that might depend on how much data you're processing.
Although I don't feel it's a major concern, I don't like stripping off the trailing comma. I always like to avoid messing up my output in the first place, rather than having additional code at the end that fixes the problem. One thing to realize, though, is that your StringBuilder generates an entire string in memory to contain the row of data, but when you call TrimEnd, I believe that this will create yet another string containing the entire row of data. Not very efficient.
I imagine that in most cases, this might be used to output to a file, or perhaps some other kind of stream. Therefore, it would make more sense to me to rewrite this functionality to write to a stream.
I've provided an alternate implementation below. The core functionality is in the WriteAsCsv method for the TextWriter. There are a few overloads to make this functionality easy to use for someone who has access to a FileStream object, a StringBuilder object, or just the original IEnumerable object.
This approach uses an enumerator in order to avoid the issue with the trailing comma.
static void WriteCsvValue(System.IO.TextWriter writer, string str)
{
if (str.IndexOfAny(csvChars) > 0)
{
writer.Write(""");
writer.Write(str); // TODO: perform any necessary escaping
writer.Write(""");
}
else
{
writer.Write(str);
}
}
public static void WriteAsCsv<T>(this System.IO.TextWriter writer, IEnumerable<T> source, Func<T, object> getItem)
{
if ((source == null) || (getItem == null))
{
return;
}
var items = from item in source.Select(getItem)
where item != null
select item.ToString();
using (var enumerator = items.GetEnumerator())
{
if (enumerator.MoveNext())
{
WriteCsvValue(writer, enumerator.Current);
while (enumerator.MoveNext())
{
writer.Write(", ");
WriteCsvValue(writer, enumerator.Current);
}
}
}
}
public static void WriteAsCsv<T>(this System.IO.Stream stream, IEnumerable<T> source, Func<T, object> getItem)
{
if ((source == null) || (getItem == null))
{
return;
}
using (var writer = new System.IO.StreamWriter(stream))
{
writer.WriteAsCsv(source, getItem);
}
}
public static void AppendAsCsv<T>(this StringBuilder builder, IEnumerable<T> source, Func<T, object> getItem)
{
if ((source == null) || (getItem == null))
{
return;
}
using (var writer = new System.IO.StringWriter(builder))
{
writer.WriteAsCsv(source, getItem);
}
}
public static string ToCsv<T>(this IEnumerable<T> source, Func<T, object> getItem)
{
StringBuilder builder = new StringBuilder();
builder.AppendAsCsv(source, getItem);
return builder.ToString();
}
- A StreamWriter is a TextWriter that wraps a Stream object.
- A StringWriter is a TextWriter that wraps a StringBuilder object.
The gist of what I've done here is to decompose your original approach into separate pieces, making it easier to consume in multiple different ways.
$endgroup$
$begingroup$
Although efficiency in the particular case I'm using it for isn't an issue as I'm not dealing with 100's of lines etc it's still something I didn't take into account. thanks.
$endgroup$
– dreza
Jan 24 '12 at 20:15
$begingroup$
@dreza if you don't care to use an enumerator (I know it feels clunky, at least until you get used to it), you could avoid having to strip off the trailing comma by prepending the comma:if(builder.Length>0)builder.Append(", ");
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 20:20
$begingroup$
@dreza - See my answer on StackOverflow to a similar question.
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 20:54
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f8228%2fconverting-a-list-to-a-csv-string%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
3 Answers
3
active
oldest
votes
3 Answers
3
active
oldest
votes
active
oldest
votes
active
oldest
votes
$begingroup$
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();
}
}
}
$endgroup$
$begingroup$
Excellent. Have edited my post to reflect your suggestions.
$endgroup$
– dreza
Jan 23 '12 at 21:43
$begingroup$
I made an edit to my code. IndexOfAny() should be >= 0 instead of > 0. Whoops!
$endgroup$
– Jesse C. Slicer
Jan 23 '12 at 22:22
$begingroup$
There's a chance that I may not want to include quotations. Would you suggest just adding a boolean flag to the method parameter to handle this case.
$endgroup$
– dreza
Jan 23 '12 at 22:34
$begingroup$
Hm. Boolean flag would likely be the quickest, but it's not a very object-oriented solution. Let me noodle on that for a few and put something up.
$endgroup$
– Jesse C. Slicer
Jan 23 '12 at 22:37
1
$begingroup$
@JesseC.Slicer +1 for the comprehensive example! But I think there's one more thing to add for it to be complete: if the value enclosed in quotes contains a quote itself, then that quote has to be doubled.
$endgroup$
– Cristian Lupascu
Jan 24 '12 at 15:14
|
show 3 more comments
$begingroup$
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();
}
}
}
$endgroup$
$begingroup$
Excellent. Have edited my post to reflect your suggestions.
$endgroup$
– dreza
Jan 23 '12 at 21:43
$begingroup$
I made an edit to my code. IndexOfAny() should be >= 0 instead of > 0. Whoops!
$endgroup$
– Jesse C. Slicer
Jan 23 '12 at 22:22
$begingroup$
There's a chance that I may not want to include quotations. Would you suggest just adding a boolean flag to the method parameter to handle this case.
$endgroup$
– dreza
Jan 23 '12 at 22:34
$begingroup$
Hm. Boolean flag would likely be the quickest, but it's not a very object-oriented solution. Let me noodle on that for a few and put something up.
$endgroup$
– Jesse C. Slicer
Jan 23 '12 at 22:37
1
$begingroup$
@JesseC.Slicer +1 for the comprehensive example! But I think there's one more thing to add for it to be complete: if the value enclosed in quotes contains a quote itself, then that quote has to be doubled.
$endgroup$
– Cristian Lupascu
Jan 24 '12 at 15:14
|
show 3 more comments
$begingroup$
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();
}
}
}
$endgroup$
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();
}
}
}
edited 2 hours ago
answered Jan 23 '12 at 21:26
Jesse C. SlicerJesse C. Slicer
11.4k2740
11.4k2740
$begingroup$
Excellent. Have edited my post to reflect your suggestions.
$endgroup$
– dreza
Jan 23 '12 at 21:43
$begingroup$
I made an edit to my code. IndexOfAny() should be >= 0 instead of > 0. Whoops!
$endgroup$
– Jesse C. Slicer
Jan 23 '12 at 22:22
$begingroup$
There's a chance that I may not want to include quotations. Would you suggest just adding a boolean flag to the method parameter to handle this case.
$endgroup$
– dreza
Jan 23 '12 at 22:34
$begingroup$
Hm. Boolean flag would likely be the quickest, but it's not a very object-oriented solution. Let me noodle on that for a few and put something up.
$endgroup$
– Jesse C. Slicer
Jan 23 '12 at 22:37
1
$begingroup$
@JesseC.Slicer +1 for the comprehensive example! But I think there's one more thing to add for it to be complete: if the value enclosed in quotes contains a quote itself, then that quote has to be doubled.
$endgroup$
– Cristian Lupascu
Jan 24 '12 at 15:14
|
show 3 more comments
$begingroup$
Excellent. Have edited my post to reflect your suggestions.
$endgroup$
– dreza
Jan 23 '12 at 21:43
$begingroup$
I made an edit to my code. IndexOfAny() should be >= 0 instead of > 0. Whoops!
$endgroup$
– Jesse C. Slicer
Jan 23 '12 at 22:22
$begingroup$
There's a chance that I may not want to include quotations. Would you suggest just adding a boolean flag to the method parameter to handle this case.
$endgroup$
– dreza
Jan 23 '12 at 22:34
$begingroup$
Hm. Boolean flag would likely be the quickest, but it's not a very object-oriented solution. Let me noodle on that for a few and put something up.
$endgroup$
– Jesse C. Slicer
Jan 23 '12 at 22:37
1
$begingroup$
@JesseC.Slicer +1 for the comprehensive example! But I think there's one more thing to add for it to be complete: if the value enclosed in quotes contains a quote itself, then that quote has to be doubled.
$endgroup$
– Cristian Lupascu
Jan 24 '12 at 15:14
$begingroup$
Excellent. Have edited my post to reflect your suggestions.
$endgroup$
– dreza
Jan 23 '12 at 21:43
$begingroup$
Excellent. Have edited my post to reflect your suggestions.
$endgroup$
– dreza
Jan 23 '12 at 21:43
$begingroup$
I made an edit to my code. IndexOfAny() should be >= 0 instead of > 0. Whoops!
$endgroup$
– Jesse C. Slicer
Jan 23 '12 at 22:22
$begingroup$
I made an edit to my code. IndexOfAny() should be >= 0 instead of > 0. Whoops!
$endgroup$
– Jesse C. Slicer
Jan 23 '12 at 22:22
$begingroup$
There's a chance that I may not want to include quotations. Would you suggest just adding a boolean flag to the method parameter to handle this case.
$endgroup$
– dreza
Jan 23 '12 at 22:34
$begingroup$
There's a chance that I may not want to include quotations. Would you suggest just adding a boolean flag to the method parameter to handle this case.
$endgroup$
– dreza
Jan 23 '12 at 22:34
$begingroup$
Hm. Boolean flag would likely be the quickest, but it's not a very object-oriented solution. Let me noodle on that for a few and put something up.
$endgroup$
– Jesse C. Slicer
Jan 23 '12 at 22:37
$begingroup$
Hm. Boolean flag would likely be the quickest, but it's not a very object-oriented solution. Let me noodle on that for a few and put something up.
$endgroup$
– Jesse C. Slicer
Jan 23 '12 at 22:37
1
1
$begingroup$
@JesseC.Slicer +1 for the comprehensive example! But I think there's one more thing to add for it to be complete: if the value enclosed in quotes contains a quote itself, then that quote has to be doubled.
$endgroup$
– Cristian Lupascu
Jan 24 '12 at 15:14
$begingroup$
@JesseC.Slicer +1 for the comprehensive example! But I think there's one more thing to add for it to be complete: if the value enclosed in quotes contains a quote itself, then that quote has to be doubled.
$endgroup$
– Cristian Lupascu
Jan 24 '12 at 15:14
|
show 3 more comments
$begingroup$
Here is my suggested version, which you should take as coming from a fellow learner.
ToCsv is doing too much. I would be inclined to keep it very simple
and perform only its very narrow responsibility: properly
transforming a sequence of strings to a properly formatted csv
string. Let the caller transform its sequence of arbitrary objects
into strings.String.Join exists in the BCL, which is exactly suited to this
purpose.- Properly encodes the quote character
private class Program
{
private static void Main(string args)
{
int someInts = {1, 2, 3, 4, 10, 9, 8};
string someStrings = {"one", "two", "seven", "eight"};
string specialStrings = {"o'ne", ""tw"o", ",,three", "fo,ur", "five"};
Console.WriteLine(someInts.Select(s => s.ToString()).ToCsv());
Console.WriteLine(someStrings.ToCsv());
Console.WriteLine(specialStrings.ToCsv());
}
}
public static class CsvHelpers
{
private static readonly char csvSpecialChars = new { ',', '"', ' ', 'n', 'r' };
public static string ToCsv(this IEnumerable<string> source)
{
if (source == null)
{
return string.Empty;
}
var encodedStrings = from item in source
select EncodeCsvField(item);
return string.Join(",", encodedStrings);
}
private static string EncodeCsvField(string input)
{
string encodedString = input.Replace(""", """");
if (input.IndexOfAny(csvSpecialChars) >= 0)
{
encodedString = """ + encodedString + """;
}
return encodedString;
}
}
$endgroup$
$begingroup$
I sort of wanted to stay away from putting the dependancy on the string object itself in the IEnumerable hence the use of generics. However didn't really know about the Join operater. Thanks.
$endgroup$
– dreza
Jan 24 '12 at 20:13
$begingroup$
My reasoning was that you either provide the function to project from your object to string or just do the projection (possibly through LINQ so you could defer the execution) at the call site. Seemed simpler to just do the projection at the call site if needed so that the extension method has only one responsibility. Cheers.
$endgroup$
– WorkerThread
Jan 24 '12 at 21:13
$begingroup$
Simple, short, clean, correct. Thanks.
$endgroup$
– mmdemirbas
Mar 6 '12 at 11:29
add a comment |
$begingroup$
Here is my suggested version, which you should take as coming from a fellow learner.
ToCsv is doing too much. I would be inclined to keep it very simple
and perform only its very narrow responsibility: properly
transforming a sequence of strings to a properly formatted csv
string. Let the caller transform its sequence of arbitrary objects
into strings.String.Join exists in the BCL, which is exactly suited to this
purpose.- Properly encodes the quote character
private class Program
{
private static void Main(string args)
{
int someInts = {1, 2, 3, 4, 10, 9, 8};
string someStrings = {"one", "two", "seven", "eight"};
string specialStrings = {"o'ne", ""tw"o", ",,three", "fo,ur", "five"};
Console.WriteLine(someInts.Select(s => s.ToString()).ToCsv());
Console.WriteLine(someStrings.ToCsv());
Console.WriteLine(specialStrings.ToCsv());
}
}
public static class CsvHelpers
{
private static readonly char csvSpecialChars = new { ',', '"', ' ', 'n', 'r' };
public static string ToCsv(this IEnumerable<string> source)
{
if (source == null)
{
return string.Empty;
}
var encodedStrings = from item in source
select EncodeCsvField(item);
return string.Join(",", encodedStrings);
}
private static string EncodeCsvField(string input)
{
string encodedString = input.Replace(""", """");
if (input.IndexOfAny(csvSpecialChars) >= 0)
{
encodedString = """ + encodedString + """;
}
return encodedString;
}
}
$endgroup$
$begingroup$
I sort of wanted to stay away from putting the dependancy on the string object itself in the IEnumerable hence the use of generics. However didn't really know about the Join operater. Thanks.
$endgroup$
– dreza
Jan 24 '12 at 20:13
$begingroup$
My reasoning was that you either provide the function to project from your object to string or just do the projection (possibly through LINQ so you could defer the execution) at the call site. Seemed simpler to just do the projection at the call site if needed so that the extension method has only one responsibility. Cheers.
$endgroup$
– WorkerThread
Jan 24 '12 at 21:13
$begingroup$
Simple, short, clean, correct. Thanks.
$endgroup$
– mmdemirbas
Mar 6 '12 at 11:29
add a comment |
$begingroup$
Here is my suggested version, which you should take as coming from a fellow learner.
ToCsv is doing too much. I would be inclined to keep it very simple
and perform only its very narrow responsibility: properly
transforming a sequence of strings to a properly formatted csv
string. Let the caller transform its sequence of arbitrary objects
into strings.String.Join exists in the BCL, which is exactly suited to this
purpose.- Properly encodes the quote character
private class Program
{
private static void Main(string args)
{
int someInts = {1, 2, 3, 4, 10, 9, 8};
string someStrings = {"one", "two", "seven", "eight"};
string specialStrings = {"o'ne", ""tw"o", ",,three", "fo,ur", "five"};
Console.WriteLine(someInts.Select(s => s.ToString()).ToCsv());
Console.WriteLine(someStrings.ToCsv());
Console.WriteLine(specialStrings.ToCsv());
}
}
public static class CsvHelpers
{
private static readonly char csvSpecialChars = new { ',', '"', ' ', 'n', 'r' };
public static string ToCsv(this IEnumerable<string> source)
{
if (source == null)
{
return string.Empty;
}
var encodedStrings = from item in source
select EncodeCsvField(item);
return string.Join(",", encodedStrings);
}
private static string EncodeCsvField(string input)
{
string encodedString = input.Replace(""", """");
if (input.IndexOfAny(csvSpecialChars) >= 0)
{
encodedString = """ + encodedString + """;
}
return encodedString;
}
}
$endgroup$
Here is my suggested version, which you should take as coming from a fellow learner.
ToCsv is doing too much. I would be inclined to keep it very simple
and perform only its very narrow responsibility: properly
transforming a sequence of strings to a properly formatted csv
string. Let the caller transform its sequence of arbitrary objects
into strings.String.Join exists in the BCL, which is exactly suited to this
purpose.- Properly encodes the quote character
private class Program
{
private static void Main(string args)
{
int someInts = {1, 2, 3, 4, 10, 9, 8};
string someStrings = {"one", "two", "seven", "eight"};
string specialStrings = {"o'ne", ""tw"o", ",,three", "fo,ur", "five"};
Console.WriteLine(someInts.Select(s => s.ToString()).ToCsv());
Console.WriteLine(someStrings.ToCsv());
Console.WriteLine(specialStrings.ToCsv());
}
}
public static class CsvHelpers
{
private static readonly char csvSpecialChars = new { ',', '"', ' ', 'n', 'r' };
public static string ToCsv(this IEnumerable<string> source)
{
if (source == null)
{
return string.Empty;
}
var encodedStrings = from item in source
select EncodeCsvField(item);
return string.Join(",", encodedStrings);
}
private static string EncodeCsvField(string input)
{
string encodedString = input.Replace(""", """");
if (input.IndexOfAny(csvSpecialChars) >= 0)
{
encodedString = """ + encodedString + """;
}
return encodedString;
}
}
answered Jan 24 '12 at 17:22
WorkerThreadWorkerThread
1484
1484
$begingroup$
I sort of wanted to stay away from putting the dependancy on the string object itself in the IEnumerable hence the use of generics. However didn't really know about the Join operater. Thanks.
$endgroup$
– dreza
Jan 24 '12 at 20:13
$begingroup$
My reasoning was that you either provide the function to project from your object to string or just do the projection (possibly through LINQ so you could defer the execution) at the call site. Seemed simpler to just do the projection at the call site if needed so that the extension method has only one responsibility. Cheers.
$endgroup$
– WorkerThread
Jan 24 '12 at 21:13
$begingroup$
Simple, short, clean, correct. Thanks.
$endgroup$
– mmdemirbas
Mar 6 '12 at 11:29
add a comment |
$begingroup$
I sort of wanted to stay away from putting the dependancy on the string object itself in the IEnumerable hence the use of generics. However didn't really know about the Join operater. Thanks.
$endgroup$
– dreza
Jan 24 '12 at 20:13
$begingroup$
My reasoning was that you either provide the function to project from your object to string or just do the projection (possibly through LINQ so you could defer the execution) at the call site. Seemed simpler to just do the projection at the call site if needed so that the extension method has only one responsibility. Cheers.
$endgroup$
– WorkerThread
Jan 24 '12 at 21:13
$begingroup$
Simple, short, clean, correct. Thanks.
$endgroup$
– mmdemirbas
Mar 6 '12 at 11:29
$begingroup$
I sort of wanted to stay away from putting the dependancy on the string object itself in the IEnumerable hence the use of generics. However didn't really know about the Join operater. Thanks.
$endgroup$
– dreza
Jan 24 '12 at 20:13
$begingroup$
I sort of wanted to stay away from putting the dependancy on the string object itself in the IEnumerable hence the use of generics. However didn't really know about the Join operater. Thanks.
$endgroup$
– dreza
Jan 24 '12 at 20:13
$begingroup$
My reasoning was that you either provide the function to project from your object to string or just do the projection (possibly through LINQ so you could defer the execution) at the call site. Seemed simpler to just do the projection at the call site if needed so that the extension method has only one responsibility. Cheers.
$endgroup$
– WorkerThread
Jan 24 '12 at 21:13
$begingroup$
My reasoning was that you either provide the function to project from your object to string or just do the projection (possibly through LINQ so you could defer the execution) at the call site. Seemed simpler to just do the projection at the call site if needed so that the extension method has only one responsibility. Cheers.
$endgroup$
– WorkerThread
Jan 24 '12 at 21:13
$begingroup$
Simple, short, clean, correct. Thanks.
$endgroup$
– mmdemirbas
Mar 6 '12 at 11:29
$begingroup$
Simple, short, clean, correct. Thanks.
$endgroup$
– mmdemirbas
Mar 6 '12 at 11:29
add a comment |
$begingroup$
A couple of things:
Although you are using a StringBuilder (which is good in this case), you're generating a string for each row of data. If you're intending to use this to create multiple rows of data, then it will not quite as efficient as it could be. Maybe this is no big deal, although that might depend on how much data you're processing.
Although I don't feel it's a major concern, I don't like stripping off the trailing comma. I always like to avoid messing up my output in the first place, rather than having additional code at the end that fixes the problem. One thing to realize, though, is that your StringBuilder generates an entire string in memory to contain the row of data, but when you call TrimEnd, I believe that this will create yet another string containing the entire row of data. Not very efficient.
I imagine that in most cases, this might be used to output to a file, or perhaps some other kind of stream. Therefore, it would make more sense to me to rewrite this functionality to write to a stream.
I've provided an alternate implementation below. The core functionality is in the WriteAsCsv method for the TextWriter. There are a few overloads to make this functionality easy to use for someone who has access to a FileStream object, a StringBuilder object, or just the original IEnumerable object.
This approach uses an enumerator in order to avoid the issue with the trailing comma.
static void WriteCsvValue(System.IO.TextWriter writer, string str)
{
if (str.IndexOfAny(csvChars) > 0)
{
writer.Write(""");
writer.Write(str); // TODO: perform any necessary escaping
writer.Write(""");
}
else
{
writer.Write(str);
}
}
public static void WriteAsCsv<T>(this System.IO.TextWriter writer, IEnumerable<T> source, Func<T, object> getItem)
{
if ((source == null) || (getItem == null))
{
return;
}
var items = from item in source.Select(getItem)
where item != null
select item.ToString();
using (var enumerator = items.GetEnumerator())
{
if (enumerator.MoveNext())
{
WriteCsvValue(writer, enumerator.Current);
while (enumerator.MoveNext())
{
writer.Write(", ");
WriteCsvValue(writer, enumerator.Current);
}
}
}
}
public static void WriteAsCsv<T>(this System.IO.Stream stream, IEnumerable<T> source, Func<T, object> getItem)
{
if ((source == null) || (getItem == null))
{
return;
}
using (var writer = new System.IO.StreamWriter(stream))
{
writer.WriteAsCsv(source, getItem);
}
}
public static void AppendAsCsv<T>(this StringBuilder builder, IEnumerable<T> source, Func<T, object> getItem)
{
if ((source == null) || (getItem == null))
{
return;
}
using (var writer = new System.IO.StringWriter(builder))
{
writer.WriteAsCsv(source, getItem);
}
}
public static string ToCsv<T>(this IEnumerable<T> source, Func<T, object> getItem)
{
StringBuilder builder = new StringBuilder();
builder.AppendAsCsv(source, getItem);
return builder.ToString();
}
- A StreamWriter is a TextWriter that wraps a Stream object.
- A StringWriter is a TextWriter that wraps a StringBuilder object.
The gist of what I've done here is to decompose your original approach into separate pieces, making it easier to consume in multiple different ways.
$endgroup$
$begingroup$
Although efficiency in the particular case I'm using it for isn't an issue as I'm not dealing with 100's of lines etc it's still something I didn't take into account. thanks.
$endgroup$
– dreza
Jan 24 '12 at 20:15
$begingroup$
@dreza if you don't care to use an enumerator (I know it feels clunky, at least until you get used to it), you could avoid having to strip off the trailing comma by prepending the comma:if(builder.Length>0)builder.Append(", ");
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 20:20
$begingroup$
@dreza - See my answer on StackOverflow to a similar question.
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 20:54
add a comment |
$begingroup$
A couple of things:
Although you are using a StringBuilder (which is good in this case), you're generating a string for each row of data. If you're intending to use this to create multiple rows of data, then it will not quite as efficient as it could be. Maybe this is no big deal, although that might depend on how much data you're processing.
Although I don't feel it's a major concern, I don't like stripping off the trailing comma. I always like to avoid messing up my output in the first place, rather than having additional code at the end that fixes the problem. One thing to realize, though, is that your StringBuilder generates an entire string in memory to contain the row of data, but when you call TrimEnd, I believe that this will create yet another string containing the entire row of data. Not very efficient.
I imagine that in most cases, this might be used to output to a file, or perhaps some other kind of stream. Therefore, it would make more sense to me to rewrite this functionality to write to a stream.
I've provided an alternate implementation below. The core functionality is in the WriteAsCsv method for the TextWriter. There are a few overloads to make this functionality easy to use for someone who has access to a FileStream object, a StringBuilder object, or just the original IEnumerable object.
This approach uses an enumerator in order to avoid the issue with the trailing comma.
static void WriteCsvValue(System.IO.TextWriter writer, string str)
{
if (str.IndexOfAny(csvChars) > 0)
{
writer.Write(""");
writer.Write(str); // TODO: perform any necessary escaping
writer.Write(""");
}
else
{
writer.Write(str);
}
}
public static void WriteAsCsv<T>(this System.IO.TextWriter writer, IEnumerable<T> source, Func<T, object> getItem)
{
if ((source == null) || (getItem == null))
{
return;
}
var items = from item in source.Select(getItem)
where item != null
select item.ToString();
using (var enumerator = items.GetEnumerator())
{
if (enumerator.MoveNext())
{
WriteCsvValue(writer, enumerator.Current);
while (enumerator.MoveNext())
{
writer.Write(", ");
WriteCsvValue(writer, enumerator.Current);
}
}
}
}
public static void WriteAsCsv<T>(this System.IO.Stream stream, IEnumerable<T> source, Func<T, object> getItem)
{
if ((source == null) || (getItem == null))
{
return;
}
using (var writer = new System.IO.StreamWriter(stream))
{
writer.WriteAsCsv(source, getItem);
}
}
public static void AppendAsCsv<T>(this StringBuilder builder, IEnumerable<T> source, Func<T, object> getItem)
{
if ((source == null) || (getItem == null))
{
return;
}
using (var writer = new System.IO.StringWriter(builder))
{
writer.WriteAsCsv(source, getItem);
}
}
public static string ToCsv<T>(this IEnumerable<T> source, Func<T, object> getItem)
{
StringBuilder builder = new StringBuilder();
builder.AppendAsCsv(source, getItem);
return builder.ToString();
}
- A StreamWriter is a TextWriter that wraps a Stream object.
- A StringWriter is a TextWriter that wraps a StringBuilder object.
The gist of what I've done here is to decompose your original approach into separate pieces, making it easier to consume in multiple different ways.
$endgroup$
$begingroup$
Although efficiency in the particular case I'm using it for isn't an issue as I'm not dealing with 100's of lines etc it's still something I didn't take into account. thanks.
$endgroup$
– dreza
Jan 24 '12 at 20:15
$begingroup$
@dreza if you don't care to use an enumerator (I know it feels clunky, at least until you get used to it), you could avoid having to strip off the trailing comma by prepending the comma:if(builder.Length>0)builder.Append(", ");
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 20:20
$begingroup$
@dreza - See my answer on StackOverflow to a similar question.
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 20:54
add a comment |
$begingroup$
A couple of things:
Although you are using a StringBuilder (which is good in this case), you're generating a string for each row of data. If you're intending to use this to create multiple rows of data, then it will not quite as efficient as it could be. Maybe this is no big deal, although that might depend on how much data you're processing.
Although I don't feel it's a major concern, I don't like stripping off the trailing comma. I always like to avoid messing up my output in the first place, rather than having additional code at the end that fixes the problem. One thing to realize, though, is that your StringBuilder generates an entire string in memory to contain the row of data, but when you call TrimEnd, I believe that this will create yet another string containing the entire row of data. Not very efficient.
I imagine that in most cases, this might be used to output to a file, or perhaps some other kind of stream. Therefore, it would make more sense to me to rewrite this functionality to write to a stream.
I've provided an alternate implementation below. The core functionality is in the WriteAsCsv method for the TextWriter. There are a few overloads to make this functionality easy to use for someone who has access to a FileStream object, a StringBuilder object, or just the original IEnumerable object.
This approach uses an enumerator in order to avoid the issue with the trailing comma.
static void WriteCsvValue(System.IO.TextWriter writer, string str)
{
if (str.IndexOfAny(csvChars) > 0)
{
writer.Write(""");
writer.Write(str); // TODO: perform any necessary escaping
writer.Write(""");
}
else
{
writer.Write(str);
}
}
public static void WriteAsCsv<T>(this System.IO.TextWriter writer, IEnumerable<T> source, Func<T, object> getItem)
{
if ((source == null) || (getItem == null))
{
return;
}
var items = from item in source.Select(getItem)
where item != null
select item.ToString();
using (var enumerator = items.GetEnumerator())
{
if (enumerator.MoveNext())
{
WriteCsvValue(writer, enumerator.Current);
while (enumerator.MoveNext())
{
writer.Write(", ");
WriteCsvValue(writer, enumerator.Current);
}
}
}
}
public static void WriteAsCsv<T>(this System.IO.Stream stream, IEnumerable<T> source, Func<T, object> getItem)
{
if ((source == null) || (getItem == null))
{
return;
}
using (var writer = new System.IO.StreamWriter(stream))
{
writer.WriteAsCsv(source, getItem);
}
}
public static void AppendAsCsv<T>(this StringBuilder builder, IEnumerable<T> source, Func<T, object> getItem)
{
if ((source == null) || (getItem == null))
{
return;
}
using (var writer = new System.IO.StringWriter(builder))
{
writer.WriteAsCsv(source, getItem);
}
}
public static string ToCsv<T>(this IEnumerable<T> source, Func<T, object> getItem)
{
StringBuilder builder = new StringBuilder();
builder.AppendAsCsv(source, getItem);
return builder.ToString();
}
- A StreamWriter is a TextWriter that wraps a Stream object.
- A StringWriter is a TextWriter that wraps a StringBuilder object.
The gist of what I've done here is to decompose your original approach into separate pieces, making it easier to consume in multiple different ways.
$endgroup$
A couple of things:
Although you are using a StringBuilder (which is good in this case), you're generating a string for each row of data. If you're intending to use this to create multiple rows of data, then it will not quite as efficient as it could be. Maybe this is no big deal, although that might depend on how much data you're processing.
Although I don't feel it's a major concern, I don't like stripping off the trailing comma. I always like to avoid messing up my output in the first place, rather than having additional code at the end that fixes the problem. One thing to realize, though, is that your StringBuilder generates an entire string in memory to contain the row of data, but when you call TrimEnd, I believe that this will create yet another string containing the entire row of data. Not very efficient.
I imagine that in most cases, this might be used to output to a file, or perhaps some other kind of stream. Therefore, it would make more sense to me to rewrite this functionality to write to a stream.
I've provided an alternate implementation below. The core functionality is in the WriteAsCsv method for the TextWriter. There are a few overloads to make this functionality easy to use for someone who has access to a FileStream object, a StringBuilder object, or just the original IEnumerable object.
This approach uses an enumerator in order to avoid the issue with the trailing comma.
static void WriteCsvValue(System.IO.TextWriter writer, string str)
{
if (str.IndexOfAny(csvChars) > 0)
{
writer.Write(""");
writer.Write(str); // TODO: perform any necessary escaping
writer.Write(""");
}
else
{
writer.Write(str);
}
}
public static void WriteAsCsv<T>(this System.IO.TextWriter writer, IEnumerable<T> source, Func<T, object> getItem)
{
if ((source == null) || (getItem == null))
{
return;
}
var items = from item in source.Select(getItem)
where item != null
select item.ToString();
using (var enumerator = items.GetEnumerator())
{
if (enumerator.MoveNext())
{
WriteCsvValue(writer, enumerator.Current);
while (enumerator.MoveNext())
{
writer.Write(", ");
WriteCsvValue(writer, enumerator.Current);
}
}
}
}
public static void WriteAsCsv<T>(this System.IO.Stream stream, IEnumerable<T> source, Func<T, object> getItem)
{
if ((source == null) || (getItem == null))
{
return;
}
using (var writer = new System.IO.StreamWriter(stream))
{
writer.WriteAsCsv(source, getItem);
}
}
public static void AppendAsCsv<T>(this StringBuilder builder, IEnumerable<T> source, Func<T, object> getItem)
{
if ((source == null) || (getItem == null))
{
return;
}
using (var writer = new System.IO.StringWriter(builder))
{
writer.WriteAsCsv(source, getItem);
}
}
public static string ToCsv<T>(this IEnumerable<T> source, Func<T, object> getItem)
{
StringBuilder builder = new StringBuilder();
builder.AppendAsCsv(source, getItem);
return builder.ToString();
}
- A StreamWriter is a TextWriter that wraps a Stream object.
- A StringWriter is a TextWriter that wraps a StringBuilder object.
The gist of what I've done here is to decompose your original approach into separate pieces, making it easier to consume in multiple different ways.
answered Jan 24 '12 at 17:43
Dr. Wily's ApprenticeDr. Wily's Apprentice
88957
88957
$begingroup$
Although efficiency in the particular case I'm using it for isn't an issue as I'm not dealing with 100's of lines etc it's still something I didn't take into account. thanks.
$endgroup$
– dreza
Jan 24 '12 at 20:15
$begingroup$
@dreza if you don't care to use an enumerator (I know it feels clunky, at least until you get used to it), you could avoid having to strip off the trailing comma by prepending the comma:if(builder.Length>0)builder.Append(", ");
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 20:20
$begingroup$
@dreza - See my answer on StackOverflow to a similar question.
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 20:54
add a comment |
$begingroup$
Although efficiency in the particular case I'm using it for isn't an issue as I'm not dealing with 100's of lines etc it's still something I didn't take into account. thanks.
$endgroup$
– dreza
Jan 24 '12 at 20:15
$begingroup$
@dreza if you don't care to use an enumerator (I know it feels clunky, at least until you get used to it), you could avoid having to strip off the trailing comma by prepending the comma:if(builder.Length>0)builder.Append(", ");
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 20:20
$begingroup$
@dreza - See my answer on StackOverflow to a similar question.
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 20:54
$begingroup$
Although efficiency in the particular case I'm using it for isn't an issue as I'm not dealing with 100's of lines etc it's still something I didn't take into account. thanks.
$endgroup$
– dreza
Jan 24 '12 at 20:15
$begingroup$
Although efficiency in the particular case I'm using it for isn't an issue as I'm not dealing with 100's of lines etc it's still something I didn't take into account. thanks.
$endgroup$
– dreza
Jan 24 '12 at 20:15
$begingroup$
@dreza if you don't care to use an enumerator (I know it feels clunky, at least until you get used to it), you could avoid having to strip off the trailing comma by prepending the comma:
if(builder.Length>0)builder.Append(", ");
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 20:20
$begingroup$
@dreza if you don't care to use an enumerator (I know it feels clunky, at least until you get used to it), you could avoid having to strip off the trailing comma by prepending the comma:
if(builder.Length>0)builder.Append(", ");
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 20:20
$begingroup$
@dreza - See my answer on StackOverflow to a similar question.
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 20:54
$begingroup$
@dreza - See my answer on StackOverflow to a similar question.
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 20:54
add a comment |
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f8228%2fconverting-a-list-to-a-csv-string%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
$begingroup$
You are excluding null values. Shouldn't a null value still be represented in the CSV output? Depends on what you're using this for, I suppose.
$endgroup$
– Dr. Wily's Apprentice
Jan 24 '12 at 17:48