summaryrefslogtreecommitdiffstats
path: root/src/Cepis/Csv.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Cepis/Csv.cs')
-rw-r--r--src/Cepis/Csv.cs122
1 files changed, 122 insertions, 0 deletions
diff --git a/src/Cepis/Csv.cs b/src/Cepis/Csv.cs
new file mode 100644
index 0000000..ca11558
--- /dev/null
+++ b/src/Cepis/Csv.cs
@@ -0,0 +1,122 @@
+using System.Text;
+
+namespace Cepis;
+
+/// <summary>
+/// An RFC 4180 reader for the batch endpoint.
+/// </summary>
+/// <remarks>
+/// Values may contain commas, quotes and newlines, so they arrive
+/// double-quoted with <c>""</c> escaping; splitting on commas would corrupt
+/// exactly the addresses that need care. A leading byte order mark is dropped,
+/// and CR, LF and CRLF all end a record. The BCL ships no CSV reader, and the
+/// shape this endpoint returns is small enough not to justify a dependency.
+/// </remarks>
+internal static class Csv
+{
+ /// <summary>
+ /// Parse a CSV document whose first record is the header.
+ /// </summary>
+ /// <remarks>
+ /// Each record becomes a dictionary from column name to value. A record
+ /// with fewer fields than the header has the missing ones filled with
+ /// <c>""</c>; extra fields are dropped.
+ /// </remarks>
+ /// <param name="text">the document</param>
+ /// <returns>one dictionary per data record, in order</returns>
+ internal static List<Dictionary<string, string>> Parse(string text)
+ {
+ var records = Split(text);
+ var rows = new List<Dictionary<string, string>>();
+ if (records.Count == 0)
+ {
+ return rows;
+ }
+
+ var header = records[0];
+ for (var r = 1; r < records.Count; r++)
+ {
+ var record = records[r];
+ var row = new Dictionary<string, string>(header.Count, StringComparer.Ordinal);
+ for (var i = 0; i < header.Count; i++)
+ {
+ row[header[i]] = i < record.Count ? record[i] : "";
+ }
+
+ rows.Add(row);
+ }
+
+ return rows;
+ }
+
+ private static List<List<string>> Split(string text)
+ {
+ var records = new List<List<string>>();
+ var record = new List<string>();
+ var field = new StringBuilder();
+ var quoted = false;
+ var pending = false;
+
+ var i = text.Length > 0 && text[0] == '\uFEFF' ? 1 : 0;
+ for (; i < text.Length; i++)
+ {
+ var c = text[i];
+ if (quoted)
+ {
+ if (c != '"')
+ {
+ field.Append(c);
+ }
+ else if (i + 1 < text.Length && text[i + 1] == '"')
+ {
+ field.Append('"');
+ i++;
+ }
+ else
+ {
+ quoted = false;
+ }
+
+ continue;
+ }
+
+ switch (c)
+ {
+ case '"':
+ quoted = true;
+ pending = true;
+ break;
+ case ',':
+ record.Add(field.ToString());
+ field.Clear();
+ pending = true;
+ break;
+ case '\r':
+ case '\n':
+ if (c == '\r' && i + 1 < text.Length && text[i + 1] == '\n')
+ {
+ i++;
+ }
+
+ record.Add(field.ToString());
+ field.Clear();
+ records.Add(record);
+ record = [];
+ pending = false;
+ break;
+ default:
+ field.Append(c);
+ pending = true;
+ break;
+ }
+ }
+
+ if (pending || field.Length > 0)
+ {
+ record.Add(field.ToString());
+ records.Add(record);
+ }
+
+ return records;
+ }
+}