csv_plus v1.3.0
pub.dev GitHub

Dates and times

Dates stay text until you ask for them, because 03/04/2024 means two different days depending on where the file came from. Turn inference on, and say which order your file uses.

Install#

dart pub add csv_plus
import 'package:csv_plus/csv_plus.dart';

Turning it on#

final codec = CsvCodec(const CsvConfig(parseDates: true));

codec.decode('when,who\n2024-01-31,Alice');
// [[when, who], [DateTime(2024, 1, 31), Alice]]

It applies everywhere inference already applies: decode, decodeToTable, decodeToMaps, the streaming CsvDecoder, and bindBytes.

What is accepted#

TextResult
2024-01-31local midnight
2024-01-31T09:30:00local date and time
2024-01-31 09:30:00a space works as the separator
2024-01-31T09:30:00.123fractional seconds kept
2024-01-31T09:30:00ZUTC
2024-01-31T09:30:00+05:30offset applied, UTC returned

A value has to start with YYYY-MM-DD. A value with no offset reads as local time, one with an offset reads as UTC.

What stays text#

codec.decode('a,b,c,d\n03/04/2024,2024-13-45,20240131,"2024-01-31"');
// ['03/04/2024', '2024-13-45', 20240131, '2024-01-31']

Ambiguous locale formats are left alone. So are unpunctuated runs such as 20240131, which are far more likely to be identifiers than dates, and quoted fields, which always opt out of inference.

An impossible date stays a string#

2024-13-45 is the case worth knowing about. DateTime.parse accepts it and quietly rolls it over to 14 February 2025, so a typo in a source file becomes a real date that is simply wrong.

DateTime.parse('2024-13-45');                 // 2025-02-14  (!)
FastDecoder.tryParseIsoDateTime('2024-13-45'); // null

csv_plus range-checks the year, month, day, hour, minute and second before parsing, and honours leap years. 2024-02-29 parses; 2023-02-29 does not.

Numeric dates like 03/04/2024#

A CSV carries no locale, so that value is 3 April in most of the world and 4 March in the United States, and nothing in the file says which. csv_plus will not guess. Tell it the order your file uses with dateOrder and it reads them.

const au = CsvConfig(parseDates: true, dateOrder: CsvDateOrder.dayFirst);
const us = CsvConfig(parseDates: true, dateOrder: CsvDateOrder.monthFirst);

CsvCodec(au).decode('when\n03/04/2024');  // 3 April 2024
CsvCodec(us).decode('when\n03/04/2024');  // 4 March 2024

Slash, dash and dot all separate, day and month may be one or two digits, and a trailing HH:mm or HH:mm:ss is kept. A two-digit year follows the spreadsheet convention: up to 68 is this century, 69 and above the last one. Range checking still applies, so under monthFirst a value like 25/12/2024 has no 25th month and stays text. ISO 8601 is recognised whichever order you set, and the default CsvDateOrder.iso leaves ambiguous values alone.

Month names#

Setting either order also reads dates that name their month in English: 3 April 2024, April 3, 2024, 03-Apr-2024 and 1st Jan 24, with an optional time after them. The name makes the order explicit, so these read the same under dayFirst and monthFirst, and an impossible day such as 31 April stays text.

Month names in another language#

English names are built in. For any other language, hand csv_plus the names with monthNames rather than writing a whole transform. Keys are lowercase, and yours are checked before the English ones, so a spelling both languages use can mean what your file intends.

const fr = CsvConfig(
  parseDates: true,
  dateOrder: CsvDateOrder.dayFirst,
  monthNames: {'janvier': 1, 'fevrier': 2, 'mars': 3, 'avril': 4},
);

Other formats#

For anything the built-in forms do not cover, convert the column yourself with a decoderTransform. It runs on every data cell and receives the column header, so you can target one column.

final codec = CsvCodec(CsvConfig(
  hasHeader: true,
  decoderTransform: (value, index, header) {
    if (header != 'when' || value is! String) return value;
    final parts = value.split('/');            // 03/04/2024, day first
    if (parts.length != 3) return value;
    return DateTime(
      int.parse(parts[2]),
      int.parse(parts[1]),
      int.parse(parts[0]),
    );
  },
));

Writing dates back#

A DateTime encodes to a form that decodes to the same value, in both local and UTC, so a decode and encode round trip is lossless.