World's Smallest CSV Parser (C#)
github.com
github.com
p.s.: this one is unfortunately another parser that uses drain char-by-char into a list/vec/buffer parsing approach which is very inefficient and plagues many languages, which causes it to not take advantage of vectorized string.Split. But other than that, I'm happy more people are noticing .NET.
The use of a class-scoped `StringBuilder` that only one method uses, and `ReadQuotedColumn`/`ReadNonQuotedColumn` yielding one character at a time, rather than accepting a the builder isn't a good sign either (for efficiency). Or casting everything to a `char` (this won't support UTF8), or assuming an end quote followed by anything (:71) is valid way to end a field.
Having StringBuilder be a private field on the parser instance is not an issue either - it is simply reused.
.Net provides a TextElementEnumerator that will iterate over graphemes instead: https://learn.microsoft.com/en-us/dotnet/api/system.globaliz...
There's a fairly comprehensive guide to working with .net character encodings at https://learn.microsoft.com/en-us/dotnet/standard/base-types... .
All surrogate pairs will be drained into the StringBuilder, working correctly. Most implementations usually agree that torn UTF-16 surrogate pairs (which are strictly the code points outside of basic multilingual plane) may exist in the input and will be passed as is, which is different to what UTF-8 implementations choose (Rust is strict with this, Go lets you tear code points arbitrarily).
We, as a community, can do better than to jump to immediate criticism of this type.
If you (competing/learning) want to write the world's smallest (code golf style)... this isn't it, and has some weird superfluous lines (if that's your measure - per the original question).
If you (learning) want to write an efficient parser.. this isn't it. You don't need a StringBuilder, you can seek the Stream to collect the (already formed) strings directly from source vs char-by-char memory copy and rebuild. Yes; that limits your stream choices, but since the example/tests only use FileStreams (which are seekable) you might not come across other kinds. If you need to use un-seekable streams, then you'll need to use a large enough buffer.
[0]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.visua...
I'm not sure what is your point but it certainly misses the idea behind this HN submission and makes me sad as it would be nice to see words of encouragement in .NET submissions here instead.
What GP is saying is that, given that it is already included in the standard class library, it's always the cheapest option wrt size of your shipping app. So it should arguably be the default choice for any .NET dev unless they either need better performance or some more exotic requirements wrt input format.
Yes, there are existing implementations, many of which are incomparably better, one of which ships with default project SDK (even if it is effectively obsolete[0]). But surely offering a competitive implementation that intends to replace existing solutions wasn't the purpose of this?
Either way, I'm not the author of the code and have already spent enough (free) time in the last 8 months working on a string library which has performant parsing as one of the project goals[1].
[0] https://github.com/dotnet/runtime/tree/main/src/libraries/Mi...
This is not true.
It doesn’t matter for this API, but it is a code smell. It makes the class not reentrant.
Talking of the API, I would make it simpler to use and more idiomatic by making the entire public API
static IEnumerable<List<String>> parse(StreamReader sr)
That call would store the parser state (currently just the StreamReader and that reused StringBuilder) in a private inner class. There would not be a constructor of the publicly visible class, removing that code smell.It is forbidden.
https://learn.microsoft.com/en-us/dotnet/standard/exceptions...
> Exception ... None (use a derived class of this exception).
https://learn.microsoft.com/en-us/dotnet/standard/design-gui...
> DO NOT throw System.Exception or System.SystemException.
C# conventions for exceptions are admittedly a bit confusing. There are a handful of very specific scenarios where you're supposed to use a built-in exception (most commonly ArgumentException). For everything else, you want to define your own type.
You don't want to expose an implementation detail like some specific exception as part of your public API and have to worry about breaking that later.
You could overload some built in exception but IMO that's not the best practice. You muddy your API and a caller has to wrap your exception if they want to bubble it up and catch it specifically, anyway.
Mine, in C, is only 2807 characters (of which 198 are commentary):
https://github.com/semitrivial/csv_parser/blob/master/csv.c
Ahh, but the submission's main file is for parsing an entire .csv, whereas mine is only for parsing a single "line" (possibly including quote-escaped newlines). So the submission wins :)
See: https://github.com/semitrivial/csv_parser/blob/master/split....
Though I suppose that's the naive approach. You could combine the two into a single file by, like you say, wrapping the row-parser in a (clever, non-trivial) outer loop, and it probably wouldn't take anywhere near 1000 characters to do that...
In some variants of CSV. There isn’t agreement on the format. For example, https://www.ietf.org/rfc/rfc4180.txt says
“While there are various specifications and implementations for the CSV format (for ex. [4], [5], [6] and [7]), there is no formal specification in existence, which allows for a wide variety of interpretations of CSV files.”
That RFC doesn’t even agree with itself, saying
“1. Each record is located on a separate line, delimited by a line break (CRLF).”
but then following that up with:
“6. Fields containing line breaks (CRLF), double quotes, and commas should be enclosed in double-quotes”
This is the "mathematical a", which does not mean "exactly one" but "at least one, and we don't care how many, we already did the interesting work". Like in "this problem has a solution".
Btw. you are not handling escaped double quotes in strings at all, and if you would do that you'd also need to count the number of backspaces. Oh, and no need to escape double quotes in single quotes ('\" could just be '"').
it can be found here: https://www.autohotkey.com/board/topic/30102-how-can-i-parse...
it consists of some 30ish lines of code and 67 lines with comments and usage example
csv.split('\n").join('".split(\',\'));a.push("');
So that each line becomes:
a.push("foo,bar,baz".split(','));
And then we have an array of arrays.
If you have a lot of data (and space for it, e.g. in /dev/shm) you can also save all the converted data to a TSV file. That's now soundly "partitionable" at the "nearest ASCII newline to 1/N bytes" and you can then go core-parallel as well as SIMD within cores (but that N-wise pass with memory mapping or mem.views). Admittedly this is probably more helpful when you are doing more computation than just splits, like ASCII-to-binary conversion of fields or such.
Plus, someone might have actually exported from Excel (or whatever) into some sound TSV instead of weird quote-escaped-CSV that some think is standardized by rfc 4180 (which itself disavows being a "standard"). In that case, at least, you needn't convert at all.
So, I see at least 3 reasons to layer this part of a system as a convert-then-split: pipeline parallelism, file parallelism, and entire pass elision.
But now I’m wondering if a good approach might be to split on the escape character and then reassemble / parse from there, safe in the knowledge that every character has exactly one interpretation.
If every field is quoted you can indeed remove the first and last “, then split on “,“ and then replace “” with “ in the fields. Excuse my phone converting the quotes!
Nevertheless, if there is a way to escape anything at all, usually it is the quotation mark, and usually it is escaped by doubling. Pretty much any other scheme is very unlikely to be properly interpreted in this context.
“, is a cat”,”, is my boyfriend”,123
etc.I like it when CSV follows RFC 4180 too - but it’s descriptive not prescriptive.
I think it helps to think of the problem more like "How do I translate a complex syntax buffered input stream which 'most' of the time just translates ',' to '\t' into a buffered output stream that is "almost" as fast as a Unix `tr , \\t`?" If there were no escaping/quoting the output buffer could literally be the same memory as the input, just with the delimiter bytes changed.
The next step is realizing that you can still just do this byte translation if you "flush" the IO buffer opportunistically at syntactically relevant times. That gets you the "almost" performance. (Scare quotes on "almost" since you might do a few more IO-system calls with certain kinds of dense syntax, but unlike your "reassemble" there won't be any allocations. Various trade-offs, but a nifty design.)
There are other nice aspects to the "partitioned program design" mentioned a sibling-ish comment, but, all together, I think it is a pretty tidy solution.
If you want to enjoy all the strange escapery, extra commas, line breaks and wrapping quotes, you may describe it in code in the first and last 2 columns.
I just don’t see the flex in having a small csv parser.
The problem is that often, what you have to ingest is more properly described as "malformed CSV / bytes that loosely resembled CSV in some manner that I have no choice but to either try to shove into a parser, or write some custom junk for this hot garbage because it comes form a source that I cannot control".
A lot of parsers are fairly configurable precisely to account for the situation of "the other end is sending me ill-defined jank" and to be flexible enough that maybe, just maybe, it'll mostly work. But it's hardly "engineering" at that point.
It describes a common format.
I infer from the tone of your license that you intended it to give away the code to any "human" who wants to "use" it.
What if I modify it? Is modification "use"? (No.)
What if a shell script calls it? Is that a "human" using it, or a "computer"? Is linking "use"?
The result is probably a license which is non-free, which doesn't appear to be your intention.
Or maybe it’s a trap. Who can say?
(Writing it as a comment here is/was still useful because it may help others)
When I was switching from academia to industry, I decided, based on HN comments like this, that I should un-publish my CSV parser.
I was worried potential employers would tsk-tsk me for self-rolling.
I promptly got an email from the creator of Ruby asking me why I had un-published my CSV parser, which apparently was being used in Ruby at the time.
(...And then later I landed my current job, a dream job, a large part of which involves handling CSV files in finance!)
(case in point, that crazy workaround is only possible because of a large expenditure of effort by the authors of perl's Text::CSV which very few CSV parsers would have implemented)
Y'all are slacking on me.
column[1..^1] column[1..]
and it compiles down to a Substring call, and ranges are part of C# 8, so they exist since .NET Core 3.1. But even if the syntax is newer (e.g. collection expressions in C# 12) you can often also use features on older target frameworks if they don't require additional runtime support (and even that can often be retrofitted internally).Here’s one in c++ that uses only a single line of code (excluding function headers)
// How do I format this as code?
vector<string> split( const string& s) { return accumulate(s.begin(), s.end(), vector<string>(1), [=](auto acc, char c) { if (c == ‘,’){ acc.push_back(string()); } else { acc.back( ) += c; } return acc; } ); }
"Verne, Jules", "20,000 leagues under the sea"Plus it's not just defining CSV but actually a CSV MIME format (what?) and thus insists that since CSV is MIME wire data, it must use CR-LF line breaks, rather than assume that data can be converted to an operating system's text file format, and native line breaks.
You'd think that CSV could be defined without reference to MIME whatsoever, using abstract line breaks; and that it's a no-brainer that since it is text, it can be MIME-encoded as a plain text type.
1. What is the size of the install required to get C# to run on a computer without Windows or MacOS installed
2. What is the size of the install for this C# library
Just curious
dotnet publish -c Release -r linux-x64 -o output -p:PublishTrimmed=true
or 100MB without trimming