using System.Text; namespace Cepis; /// /// An RFC 4180 reader for the batch endpoint. /// /// /// Values may contain commas, quotes and newlines, so they arrive /// double-quoted with "" 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. /// internal static class Csv { /// /// Parse a CSV document whose first record is the header. /// /// /// Each record becomes a dictionary from column name to value. A record /// with fewer fields than the header has the missing ones filled with /// ""; extra fields are dropped. /// /// the document /// one dictionary per data record, in order internal static List> Parse(string text) { var records = Split(text); var rows = new List>(); 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(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> Split(string text) { var records = new List>(); var record = new List(); 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; } }