diff --git a/lib/csv.dart b/lib/csv.dart index 10fd0e6..3d26643 100644 --- a/lib/csv.dart +++ b/lib/csv.dart @@ -8,6 +8,7 @@ import 'csv_settings_autodetection.dart'; part 'csv_to_list_converter.dart'; part 'list_to_csv_converter.dart'; +part 'map_to_csv_converter.dart'; /// The RFC conform default value for field delimiter. const String defaultFieldDelimiter = ','; diff --git a/lib/map_to_csv_converter.dart b/lib/map_to_csv_converter.dart new file mode 100644 index 0000000..4aab40d --- /dev/null +++ b/lib/map_to_csv_converter.dart @@ -0,0 +1,88 @@ +part of csv; + +enum ExtrasActions { raise, ignore } + +/// Converts rows -- a [List] of [Map]s into a csv String. +/// +/// Usage: +/// var sb = new StringBuffer(); +/// +/// var writer = MapToCsvConverter(sb, field_names); +/// writer.writeheader(); +/// writer.writerows(data_list); +/// +/// print(f.toString()); +/// +class MapToCsvConverter { + StringBuffer sb; + List fieldNames; + ExtrasActions extrasAction; + + ListToCsvConverter _listConverter; + + /// This relies on [ListToCsvConverter] to do the actual converting. + /// + /// Required arguments: + /// + /// [sb] is [StringBuffer] to write results to + /// [fieldNames] is a list of [Map] keys in the desired output order + /// + /// Optional arguments: + /// + /// [extrasAction] determines whether or not it is ok for the data + /// to have fields not present in [fieldNames]. Will raise an exception + /// by default. + /// + /// Can also receive any arguments that [ListToCsvConverter] takes. + /// + MapToCsvConverter( + this.sb, + this.fieldNames, { + this.extrasAction: ExtrasActions.raise, + String fieldDelimiter: defaultFieldDelimiter, + String textDelimiter: defaultTextDelimiter, + String textEndDelimiter, + String eol: defaultEol, + bool delimitAllFields: defaultDelimitAllFields, + }) { + _listConverter = new ListToCsvConverter( + fieldDelimiter: fieldDelimiter, + textDelimiter: textDelimiter, + textEndDelimiter: textEndDelimiter, + eol: eol, + delimitAllFields: delimitAllFields, + ); + } + + void writerow(Map rowdict) { + _listConverter.convertSingleRow(sb, _mapToList(rowdict)); + sb.write(_listConverter.eol); + } + + void writerows(List rowdicts) { + rowdicts.forEach((row_dict) { + writerow(row_dict); + }); + } + + void writeheader() { + var header_map = new Map.fromIterable(fieldNames, + key: (field) => field, value: (field) => field); + writerow(header_map); + } + + List _mapToList(Map rowdict) { + if (extrasAction == ExtrasActions.raise) { + var extraFields = _findExtraFields(rowdict); + if (extraFields.length > 0) { + throw new Exception( + 'Map contains fields not in fieldNames: "${extraFields.join(",")}"'); + } + } + + return fieldNames.map((field) => rowdict[field]).toList(); + } + + List _findExtraFields(Map data) => + data.keys.toList()..removeWhere((e) => fieldNames.contains(e)); +} diff --git a/test/map_to_csv_test.dart b/test/map_to_csv_test.dart new file mode 100644 index 0000000..a87d189 --- /dev/null +++ b/test/map_to_csv_test.dart @@ -0,0 +1,134 @@ +import 'package:test/test.dart'; +import "package:csv/csv.dart"; + +void main() { + var sb = new StringBuffer(); + + tearDown(() { + sb.clear(); + }); + + group("MapToCsvConverter Tests", () { + test("writes header row", () { + var fieldNames = ["one", "two", "three"]; + var writer = new MapToCsvConverter(sb, fieldNames); + writer.writeheader(); + + expect(sb.toString(), equals("one,two,three${defaultEol}")); + }); + + test("writes regular row", () { + var fieldNames = ["one", "two", "three"]; + var writer = new MapToCsvConverter(sb, fieldNames); + + var data = {"one": "this", "two": "thing", "three": "works"}; + writer.writerow(data); + + expect(sb.toString(), equals("this,thing,works${defaultEol}")); + }); + + test("writes multiple rows", () { + var fieldNames = ["one", "two", "three"]; + var writer = new MapToCsvConverter(sb, fieldNames); + + var data = [ + {"one": "this", "two": "thing", "three": "works"}, + {"one": "second", "two": "works", "three": "too"} + ]; + writer.writerows(data); + + expect( + sb.toString(), + equals("this,thing,works${defaultEol}" + "second,works,too${defaultEol}")); + }); + + test("ignores when extra fields present in data", () { + var fieldNames = ["one", "two", "three"]; + var writer = new MapToCsvConverter(sb, fieldNames, + extrasAction: ExtrasActions.ignore); + + var data = { + "one": "this", + "two": "thing", + "three": "works", + "extra": "ignore" + }; + writer.writerow(data); + + expect(sb.toString(), equals("this,thing,works${defaultEol}")); + }); + + test("raises exception when extra fields present in data", () { + var fieldNames = ["one", "two", "three"]; + var writer = new MapToCsvConverter(sb, fieldNames, + extrasAction: ExtrasActions.raise); + + var data = { + "one": "this", + "two": "thing", + "three": "works", + "extra": "raise error", + "extra_two": "other", + }; + + var expectedMessage = + 'Map contains fields not in fieldNames: "extra,extra_two"'; + expect(() => writer.writerow(data), + throwsA(predicate((e) => e.message == expectedMessage))); + }); + + test("can customize field delimiter", () { + var fieldNames = ["one", "two", "three"]; + var writer = new MapToCsvConverter(sb, fieldNames, fieldDelimiter: "|"); + + var data = [ + {"one": "this", "two": "thing", "three": "works"}, + {"one": "second", "two": "works", "three": "too"} + ]; + writer.writerows(data); + + expect( + sb.toString(), + equals("this|thing|works${defaultEol}" + "second|works|too${defaultEol}")); + }); + + test("can customize text delimiter", () { + var fieldNames = ["one", "two", "three"]; + var writer = new MapToCsvConverter(sb, fieldNames, textDelimiter: "'"); + + var data = [ + {"one": "that's", "two": "thing", "three": "works"}, + ]; + writer.writerows(data); + + expect(sb.toString(), equals("'that''s',thing,works${defaultEol}")); + }); + + test("can customize eol delimiter", () { + var fieldNames = ["one", "two", "three"]; + var writer = new MapToCsvConverter(sb, fieldNames, eol: "***"); + + var data = [ + {"one": "this", "two": "thing", "three": "works"}, + ]; + writer.writerows(data); + + expect(sb.toString(), equals("this,thing,works***")); + }); + + test("can customize text end delimiter", () { + var fieldNames = ["one", "two", "three"]; + var writer = + new MapToCsvConverter(sb, fieldNames, textEndDelimiter: "&"); + + var data = [ + {"one": '"air quotes"', "two": "thing", "three": "works"}, + ]; + writer.writerows(data); + + expect(sb.toString(), equals('""air quotes"&,thing,works$defaultEol')); + }); + }); +}