diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:42:38 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 13:10:34 -0300 |
| commit | a2b10fd494fb389e556e40134031eb86e56e11e7 (patch) | |
| tree | 08b6df8a06f2c7c15488b90d57a6d8286cd57a22 /src/Cepis/Csv.cs | |
| download | csharp-a2b10fd494fb389e556e40134031eb86e56e11e7.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'src/Cepis/Csv.cs')
| -rw-r--r-- | src/Cepis/Csv.cs | 122 |
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; + } +} |