summaryrefslogtreecommitdiffstats
path: root/lib/src/csv.dart
blob: ab2f0be6854dc89913b335f4d65d551cb7900098 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/// A minimal RFC 4180 reader, for the `/csv` batch endpoint.
///
/// Values may contain commas and are then double-quoted with `""` escaping, so
/// splitting on `,` corrupts every row that carries a street name like
/// `Rua Um, Dois`. This is here because the Dart SDK ships no CSV parser and
/// this library keeps its dependency list to one entry.
library;

const String _bom = '\u{FEFF}';

/// Parses CSV text into records keyed by its header row.
///
/// A leading byte-order mark is dropped, `CRLF` and `LF` are both accepted, and
/// a row shorter than the header has its remaining columns filled with the
/// empty string. An empty input yields no records.
List<Map<String, String>> parseCsv(String text) {
  final rows = _parseRows(text.startsWith(_bom) ? text.substring(1) : text);
  if (rows.isEmpty) {
    return <Map<String, String>>[];
  }

  final header = rows.removeAt(0);
  final records = <Map<String, String>>[];
  for (final row in rows) {
    if (row.length == 1 && row.first.isEmpty) {
      continue;
    }
    final record = <String, String>{};
    for (var i = 0; i < header.length; i++) {
      record[header[i]] = i < row.length ? row[i] : '';
    }
    records.add(record);
  }
  return records;
}

List<List<String>> _parseRows(String text) {
  final rows = <List<String>>[];
  final field = StringBuffer();
  var row = <String>[];
  var quoted = false;
  var started = false;

  for (var i = 0; i < text.length; i++) {
    final char = text[i];

    if (quoted) {
      if (char == '"') {
        if (i + 1 < text.length && text[i + 1] == '"') {
          field.write('"');
          i++;
        } else {
          quoted = false;
        }
      } else {
        field.write(char);
      }
      continue;
    }

    if (char == '"') {
      quoted = true;
      started = true;
    } else if (char == ',') {
      row.add(field.toString());
      field.clear();
      started = true;
    } else if (char == '\n') {
      row.add(field.toString());
      field.clear();
      rows.add(row);
      row = <String>[];
      started = false;
    } else if (char != '\r') {
      field.write(char);
      started = true;
    }
  }

  if (started || field.isNotEmpty || row.isNotEmpty) {
    row.add(field.toString());
    rows.add(row);
  }

  return rows;
}