Skip to content

Commit b35923e

Browse files
committed
feat: dynamic typing
1 parent f02d7de commit b35923e

7 files changed

Lines changed: 115 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# 7.1.0
2+
Implement dynamic typing for CSV.
3+
14
# 7.0.0
25
Complete rewrite of the library, now compatible with `dart:convert`.
36
Automatic delimiter detection and BOM support.

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ please consult [doc/README-v6.md](doc/README-v6.md) and continue using version 6
1414
- **Auto-detection**: Smartly detects delimiters and line endings.
1515
- **Robust Parsing**: Handles quoted fields, escaped quotes, and even malformed CSVs graciously (similar to PapaParse).
1616
- **Performance**: Optimized for speed and low memory usage.
17+
- **Dynamic Typing**: Optional automatic parsing of numbers and booleans (similar to PapaParse).
1718

1819

1920
### Delimiters
@@ -86,6 +87,28 @@ void main() {
8687
}
8788
```
8889

90+
### Dynamic Typing
91+
92+
Automatically convert values that look like numbers or booleans into their respective Dart types.
93+
94+
> [!NOTE]
95+
> Enabling `dynamicTyping` adds a small performance overhead (approx. 15% in benchmarks). For maximum performance on large files where you know the schema, using a manual `decoderTransform` or processing rows after decoding is faster.
96+
97+
```dart
98+
import 'package:csv/csv.dart';
99+
100+
void main() {
101+
final input = 'Name,Age,Active\nAlice,30,true';
102+
103+
// With dynamic typing enabled
104+
final codec = CsvCodec(dynamicTyping: true);
105+
final rows = codec.decode(input);
106+
107+
print(rows[0][1].runtimeType); // int (30)
108+
print(rows[0][2].runtimeType); // bool (true)
109+
}
110+
```
111+
89112
### Advanced: Field Transformations
90113

91114
You can use the `encoderTransform` and `decoderTransform` hooks to process fields based on their value, column index, or header name to for example trim text, change decimal separators or format dates.

benchmark/benchmark_csv.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ void main() async {
55
print('--- CSV Benchmark ---');
66

77
await runBenchmark('Default CSV', csv);
8+
await runBenchmark('Dynamic Typing CSV', CsvCodec(dynamicTyping: true));
89
await runBenchmark('Excel CSV', excel);
910
await runBenchmark('Tab CSV', CsvCodec(fieldDelimiter: '\t'));
1011

lib/src/csv_codec.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class CsvCodec extends Codec<List<List<dynamic>>, String> {
2222
/// [parseHeaders]: Whether to treat the first row as headers and return [CsvRow] objects (default: false).
2323
/// [encoderTransform]: A function to transform fields before encoding.
2424
/// [decoderTransform]: A function to transform fields after decoding.
25+
/// [dynamicTyping]: Whether to automatically parse numbers and booleans (default: false).
2526
CsvCodec({
2627
String fieldDelimiter = ',',
2728
String lineDelimiter = '\r\n',
@@ -34,6 +35,7 @@ class CsvCodec extends Codec<List<List<dynamic>>, String> {
3435
bool parseHeaders = false,
3536
dynamic Function(dynamic field, int index, String? header)? encoderTransform,
3637
dynamic Function(dynamic field, int index, String? header)? decoderTransform,
38+
bool dynamicTyping = false,
3739
}) : _encoder = CsvEncoder(
3840
fieldDelimiter: fieldDelimiter,
3941
lineDelimiter: lineDelimiter,
@@ -50,6 +52,7 @@ class CsvCodec extends Codec<List<List<dynamic>>, String> {
5052
skipEmptyLines: skipEmptyLines,
5153
parseHeaders: parseHeaders,
5254
fieldTransform: decoderTransform,
55+
dynamicTyping: dynamicTyping,
5356
);
5457

5558
/// Creates a [CsvCodec] configured for Excel.

lib/src/csv_decoder.dart

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ class CsvDecoder extends Converter<String, List<List<dynamic>>> {
2323
/// Whether to parse the first row as headers and return [CsvRow]s.
2424
final bool parseHeaders;
2525

26+
/// Whether to automatically parse numbers and booleans.
27+
final bool dynamicTyping;
28+
2629
/// Creates a [CsvDecoder].
2730
///
2831
/// [fieldDelimiter] can be null for auto-detection.
@@ -36,6 +39,7 @@ class CsvDecoder extends Converter<String, List<List<dynamic>>> {
3639
this.skipEmptyLines = true,
3740
this.fieldTransform,
3841
this.parseHeaders = false,
42+
this.dynamicTyping = false,
3943
}) : assert(
4044
quoteCharacter.length == 1,
4145
'quoteCharacter must be a single character',
@@ -73,6 +77,7 @@ class CsvDecoder extends Converter<String, List<List<dynamic>>> {
7377
skipEmptyLines,
7478
fieldTransform,
7579
parseHeaders,
80+
dynamicTyping,
7681
);
7782
}
7883
}
@@ -86,6 +91,7 @@ class _CsvDecoderSink extends StringConversionSink {
8691
final dynamic Function(dynamic field, int index, String? header)?
8792
_fieldTransform;
8893
final bool _parseHeaders;
94+
final bool _dynamicTyping;
8995

9096
String? _delimiter;
9197
bool _inQuotes = false;
@@ -104,6 +110,7 @@ class _CsvDecoderSink extends StringConversionSink {
104110
this._skipEmptyLines,
105111
this._fieldTransform,
106112
this._parseHeaders,
113+
this._dynamicTyping,
107114
) {
108115
_delimiter = _presetDelimiter;
109116
if (_delimiter == null) {
@@ -437,13 +444,33 @@ class _CsvDecoderSink extends StringConversionSink {
437444
}
438445

439446
dynamic _transform(String field) {
447+
dynamic value = field;
448+
if (_dynamicTyping) {
449+
if (field == 'true') {
450+
value = true;
451+
} else if (field == 'false') {
452+
value = false;
453+
} else {
454+
// Try parsing numbers
455+
final asInt = int.tryParse(field);
456+
if (asInt != null) {
457+
value = asInt;
458+
} else {
459+
final asDouble = double.tryParse(field);
460+
if (asDouble != null) {
461+
value = asDouble;
462+
}
463+
}
464+
}
465+
}
466+
440467
final transform = _fieldTransform;
441-
if (transform == null) return field;
468+
if (transform == null) return value;
442469

443470
// Safely gets the header or null if index is out of bounds or list is null
444471
final header = _indexToHeader?.elementAtOrNull(_fieldIndex);
445472

446-
return transform(field, _fieldIndex, header);
473+
return transform(value, _fieldIndex, header);
447474
}
448475

449476
@override

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: csv
2-
version: 7.0.0
2+
version: 7.1.0
33
description: |-
44
A codec to transform between a string and a list of values.
55

test/dynamic_typing_test.dart

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
2+
import 'package:csv/csv.dart';
3+
import 'package:test/test.dart';
4+
5+
void main() {
6+
group('Dynamic Typing Tests', () {
7+
test('Parse integers', () {
8+
final input = '123,-456';
9+
final decoder = CsvDecoder(dynamicTyping: true);
10+
final result = decoder.convert(input);
11+
expect(result[0][0], equals(123));
12+
expect(result[0][1], equals(-456));
13+
expect(result[0][0], isA<int>());
14+
});
15+
16+
test('Parse doubles', () {
17+
final input = '1.23,1e10,.5';
18+
final decoder = CsvDecoder(dynamicTyping: true);
19+
final result = decoder.convert(input);
20+
expect(result[0][0], equals(1.23));
21+
expect(result[0][1], equals(1e10));
22+
expect(result[0][2], equals(0.5));
23+
expect(result[0][0], isA<double>());
24+
});
25+
26+
test('Parse booleans', () {
27+
final input = 'true,false';
28+
final decoder = CsvDecoder(dynamicTyping: true);
29+
final result = decoder.convert(input);
30+
expect(result[0][0], isTrue);
31+
expect(result[0][1], isFalse);
32+
});
33+
34+
test('Mixed data and types', () {
35+
final input = '123,true,hello,1.5';
36+
final decoder = CsvDecoder(dynamicTyping: true);
37+
final result = decoder.convert(input);
38+
expect(result[0], equals([123, true, 'hello', 1.5]));
39+
});
40+
41+
test('Quoted values are also typed (PapaParse semantics)', () {
42+
final input = '"123","true","3.14"';
43+
final decoder = CsvDecoder(dynamicTyping: true);
44+
final result = decoder.convert(input);
45+
expect(result[0], equals([123, true, 3.14]));
46+
});
47+
48+
test('Booleans must be exact match (PapaParse semantics)', () {
49+
final input = 'TRUE,False,true';
50+
final decoder = CsvDecoder(dynamicTyping: true);
51+
final result = decoder.convert(input);
52+
expect(result[0], equals(['TRUE', 'False', true]));
53+
});
54+
});
55+
}

0 commit comments

Comments
 (0)