csv icon indicating copy to clipboard operation
csv copied to clipboard

[Feature request] Support for parsing the header

Open orkun1675 opened this issue 1 year ago • 1 comments

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"];

orkun1675 avatar Jul 13 '24 19:07 orkun1675

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:

  1. 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.
  1. 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 CsvMap from 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]}
}

PvtPuddles avatar Jul 14 '25 18:07 PvtPuddles