csv
csv copied to clipboard
[Feature request] Support for parsing the header
The current API of accesing fields values is:
List<List<dynamic>> rows = const CsvToListConverter(
eol: '\n',
shouldParseNumbers: false,
).convert(fileContents);
// Row zero has the headers; discard it.
String idColumnValue = rows[1][0];
Which works if the file columns are guranteed to be in a static order.
It would be great if we could access columns values by their name:
List<List<dynamic>> rows = const CsvToListConverter(
eol: '\n',
shouldParseNumbers: false,
parseHeaders: true, // New config option
).convert(fileContents);
// Row zero had headers but was parsed and dropped.
String idColumnValue = rows[0]["id"];
I know this is an old issue and you may not still be looking for a solution, but this caught my interest. Perhaps it can be useful to others looking for a solution.
Here is a very simple wrapper class for using the Map interface to access the underlying CSV data.
import 'dart:collection';
class CsvMap with MapBase<String, List> {
CsvMap(this.data);
List<List<dynamic>> data;
List<String> get headers => data[0].cast();
@override
List<dynamic>? operator [](Object? key) {
if (key is! String) return null;
final hIndex = headers.indexOf(key);
return data.skip(1).map((row) => row[hIndex]).toList();
}
@override
void operator []=(String key, List value) {
final hIndex = headers.indexOf(key);
if (hIndex < 0) {
headers.add(key);
for (final (i, row) in data.skip(1).indexed) {
row.add(value[i]);
}
} else {
for (final (i, row) in data.skip(1).indexed) {
row[hIndex] = value[i];
}
}
}
@override
void clear() => data = [];
@override
Iterable<String> get keys => headers;
@override
List? remove(Object? key) {
if (key is! String) return null;
final hIndex = headers.indexOf(key);
if (hIndex < 0) return null;
return List.generate(data.length, (index) => data[index].removeAt(hIndex));
}
}
void main() {
final csv = [
["a", "b", "c"],
[1, 2, 3],
[4, 5, 6],
];
final wrapped = CsvMap(csv);
print(wrapped["a"]); // [1, 4]
wrapped.remove("c");
print(wrapped.data); // [[a, b], [1, 2], [4, 5]]
wrapped["c"] = [7, 8];
print(wrapped.data); // [[a, b, c], [1, 2, 7], [4, 5, 8]]
}
Performance considerations:
- A new list is created every time a key is accessed. For large data sets, you should either:
- Cache the column, or
- Change the signature to
class CsvMap with MapBase<String, Iterable>and return lazy views of the data.
- If you do not need the underlying data in its original format, consider instead converting the data to a map directly (though this can be an expensive one-time penalty). Here's an example using an extension type to construct a
CsvMapfrom the raw CSV data.
extension type CsvMap(Map<String, List> data) implements Map<String, List> {
factory CsvMap.fromCsv(List<List> data) => CsvMap(
Map<String, List>.fromIterable(
data[0].cast<String>(),
value: (key) {
key as String;
final hIndex = data[0].indexOf(key);
return List.generate(
data.length - 1,
(index) => data[index + 1][hIndex],
);
},
),
);
}
void main() {
final csv = [
["a", "b", "c"],
[1, 2, 3],
[4, 5, 6],
];
final wrapped = CsvMap.fromCsv(csv);
print(wrapped["a"]); // [1, 4]
wrapped.remove("c");
print(wrapped.data); // {a: [1, 4], b: [2, 5]}
wrapped["c"] = [7, 8];
print(wrapped.data); // {a: [1, 4], b: [2, 5], c: [7, 8]}
wrapped["b"] = [9, 10];
print(wrapped.data); // {a: [1, 4], b: [9, 10], c: [7, 8]}
}