changes as per review
diff --git a/benchmark/fixed_datetime_formatter_benchmark.dart b/benchmark/fixed_datetime_formatter_benchmark.dart index 7ac4a2b..89b34a1 100644 --- a/benchmark/fixed_datetime_formatter_benchmark.dart +++ b/benchmark/fixed_datetime_formatter_benchmark.dart
@@ -8,10 +8,10 @@ /// This class tests the implementation speed of /// _DateFormatPatternField::nextInteger, which is assumed to be called often and /// thus being performance-critical. -class NewMethod extends BenchmarkBase { +class DecodeBenchmark extends BenchmarkBase { late String result; late FixedDateTimeFormatter fixedDateTimeFormatter; - NewMethod() : super('Parse a million strings to datetime'); + DecodeBenchmark() : super('Parse a million strings to datetime'); @override void setup() { @@ -27,5 +27,5 @@ } void main() { - NewMethod().report(); + DecodeBenchmark().report(); }
diff --git a/lib/src/fixed_datetime_formatter.dart b/lib/src/fixed_datetime_formatter.dart index d1bd7cb..04b9700 100644 --- a/lib/src/fixed_datetime_formatter.dart +++ b/lib/src/fixed_datetime_formatter.dart
@@ -9,15 +9,15 @@ /// the same result as calling `DateTime(1996, 4, 25, 5, 3, 22)`. /// /// The allowed characters are -/// * `Y` a digit used in the time scale component “calendar year” -/// * `M` a digit used in the time scale component “calendar month” -/// * `D` a digit used in the time scale component “calendar day” -/// * `E` a digit used in the time scale component “decade” -/// * `C` a digit used in the time scale component “century” -/// * `h` a digit used in the time scale component “clock hour” -/// * `m` a digit used in the time scale component “clock minute” -/// * `s` a digit used in the time scale component “clock second” -/// as specified in the ISO 8601 standard. +/// * `Y` for “calendar year” +/// * `M` for “calendar month” +/// * `D` for “calendar day” +/// * `E` for “decade” +/// * `C` for “century” +/// * `h` for “clock hour” +/// * `m` for “clock minute” +/// * `s` for “clock second” +/// * `S` for “fractional clock second” /// /// Non-allowed characters in the format [pattern] are ignored when decoding a /// string, in this case `YYYY kiwi MM` is the same format string as @@ -33,29 +33,46 @@ /// containing delimiters, as the parser would not know how many digits to take /// otherwise. class FixedDateTimeFormatter { - static final _validFormatCharacters = 'YMDEChms'.codeUnits; - static final yearCode = 'Y'.codeUnitAt(0); - static final monthCode = 'M'.codeUnitAt(0); - static final dayCode = 'D'.codeUnitAt(0); - static final decadeCode = 'E'.codeUnitAt(0); - static final centuryCode = 'C'.codeUnitAt(0); - static final hourCode = 'h'.codeUnitAt(0); - static final minuteCode = 'm'.codeUnitAt(0); - static final secondCode = 's'.codeUnitAt(0); + static const _powersOfTen = [0, 1, 10, 100, 1000, 10000, 100000]; + static const _validFormatCharacters = [ + _yearCode, + _monthCode, + _dayCode, + _decadeCode, + _centuryCode, + _hourCode, + _minuteCode, + _secondCode, + _fractionSecondCode, + ]; + static const _yearCode = 0x59; /*Y*/ + static const _monthCode = 0x4D; /*M*/ + static const _dayCode = 0x44; /*D*/ + static const _decadeCode = 0x45; /*E*/ + static const _centuryCode = 0x43; /*C*/ + static const _hourCode = 0x68; /*H*/ + static const _minuteCode = 0x6D; /*m*/ + static const _secondCode = 0x73; /*s*/ + static const _fractionSecondCode = 0x53; /*S*/ + ///Store publicly in case the user wants to retrieve it final String pattern; + + ///Whether to use UTC or the local time zone + final bool isUtc; final _blocks = _ParsedFormatBlocks(); /// Creates a new [FixedDateTimeFormatter] with the provided [pattern]. /// /// The [pattern] interprets the characters mentioned in /// [FixedDateTimeFormatter] to represent fields of a `DateTime` value. Other - /// characters are not special. + /// characters are not special. If [isUtc] is set to false, the DateTime is + /// constructed with respect to the local timezone. /// /// There must at most be one sequence of each special character to ensure a /// single source of truth when constructing the [DateTime], so a pattern of /// `"CCCC-MM-DD, CC"` is invalid, because it has two separate `C` sequences. - FixedDateTimeFormatter(this.pattern) { + FixedDateTimeFormatter(this.pattern, {this.isUtc = true}) { int? current; var start = 0; var characters = pattern.codeUnits; @@ -95,12 +112,18 @@ if (previousEnd < start) { buffer.write(pattern.substring(previousEnd, start)); } + var formatCharacter = _blocks.formatCharacters[i]; var number = _extractNumFromDateTime( - _blocks.formatCharacters[i], + formatCharacter, datetime, ).toString(); if (number.length > length) { - number = number.substring(number.length - length); + if (formatCharacter == _fractionSecondCode) { + //Special case, as we want fractional seconds to be the leading digits + number = number.substring(length); + } else { + number = number.substring(number.length - length); + } } else if (number.length < length) { number = number.padLeft(length, '0'); } @@ -115,23 +138,26 @@ return buffer.toString(); } - int _extractNumFromDateTime(int? formatChar, DateTime dt) { - if (formatChar == yearCode) { - return dt.year; - } else if (formatChar == centuryCode) { - return (dt.year / 100).floor(); - } else if (formatChar == decadeCode) { - return (dt.year / 10).floor(); - } else if (formatChar == monthCode) { - return dt.month; - } else if (formatChar == dayCode) { - return dt.day; - } else if (formatChar == hourCode) { - return dt.hour; - } else if (formatChar == minuteCode) { - return dt.minute; - } else if (formatChar == secondCode) { - return dt.second; + int _extractNumFromDateTime(int? formatChar, DateTime dateTime) { + switch (formatChar) { + case _yearCode: + return dateTime.year; + case _centuryCode: + return (dateTime.year / 100).floor(); + case _decadeCode: + return (dateTime.year / 10).floor(); + case _monthCode: + return dateTime.month; + case _dayCode: + return dateTime.day; + case _hourCode: + return dateTime.hour; + case _minuteCode: + return dateTime.minute; + case _secondCode: + return dateTime.second; + case _fractionSecondCode: + return dateTime.microsecond; } throw AssertionError("Unreachable, the key is checked in the constructor"); } @@ -139,13 +165,13 @@ /// Parse a string [formattedDateTime] to a local [DateTime] as specified in the /// [pattern], substituting missing values with a default. Throws an exception /// on failure to parse. - DateTime decode(String formattedDateTime, {bool isUtc = false}) { + DateTime decode(String formattedDateTime) { return _decode(formattedDateTime, isUtc, true); } /// Same as [decode], but will not throw on parsing erros, instead using /// the default value as if the format char was not present in the [pattern]. - DateTime tryDecode(String formattedDateTime, {bool isUtc = false}) { + DateTime tryDecode(String formattedDateTime) { return _decode(formattedDateTime, isUtc, false); } @@ -163,41 +189,83 @@ var hour = 0; var minute = 0; var second = 0; + var microsecond = 0; for (int i = 0; i < _blocks.length; i++) { var char = _blocks.formatCharacters[i]; - var num = _extractNumFromString(characters, i, throwOnError); - if (num != null) { - if (char == yearCode) { - year = num; - } else if (char == centuryCode) { - century = num; - } else if (char == decadeCode) { - decade = num; - } else if (char == monthCode) { - month = num; - } else if (char == dayCode) { - day = num; - } else if (char == hourCode) { - hour = num; - } else if (char == minuteCode) { - minute = num; - } else if (char == secondCode) { - second = num; + var number = _extractNumFromString(characters, i, throwOnError); + if (number != null) { + if (char == _fractionSecondCode) { + //Special case, as we want fractional seconds to be the leading digits + var numberLength = _blocks.ends[i] - _blocks.starts[i]; + number *= _powersOfTen[6 - numberLength + 1]; + } + switch (char) { + case _yearCode: + year = number; + break; + case _centuryCode: + century = number; + break; + case _decadeCode: + decade = number; + break; + case _monthCode: + month = number; + break; + case _dayCode: + day = number; + break; + case _hourCode: + hour = number; + break; + case _minuteCode: + minute = number; + break; + case _secondCode: + second = number; + break; + case _fractionSecondCode: + microsecond = number; + break; } } } var totalYear = year + 100 * century + 10 * decade; if (isUtc) { - return DateTime.utc(totalYear, month, day, hour, minute, second, 0, 0); + return DateTime.utc( + totalYear, + month, + day, + hour, + minute, + second, + 0, + microsecond, + ); } else { - return DateTime(totalYear, month, day, hour, minute, second, 0, 0); + return DateTime( + totalYear, + month, + day, + hour, + minute, + second, + 0, + microsecond, + ); } } int? _extractNumFromString( - List<int> characters, int index, bool throwOnError) { - var parsed = - tryParse(characters, _blocks.starts[index], _blocks.ends[index]); + List<int> characters, + int index, + bool throwOnError, + ) { + var parsed = tryParse( + characters, + _blocks.starts[index], + _blocks.ends[index], + ); if (parsed == null && throwOnError) { throw FormatException( '${String.fromCharCodes(characters)} should only contain digits'); @@ -205,13 +273,12 @@ return parsed; } - static final zeroCode = '0'.codeUnitAt(0); int? tryParse(List<int> characters, int start, int end) { int result = 0; for (var i = start; i < end; i++) { - var character = characters[i]; - if (character >= zeroCode && character < zeroCode + 10) { - result = result * 10 + (character - zeroCode); + var digit = characters[i] ^ 0x30; + if (digit <= 9) { + result = result * 10 + digit; } else { return null; }
diff --git a/test/fixed_datetime_formatter_test.dart b/test/fixed_datetime_formatter_test.dart index 2dd883a..7c7f0a5 100644 --- a/test/fixed_datetime_formatter_test.dart +++ b/test/fixed_datetime_formatter_test.dart
@@ -9,40 +9,60 @@ //decode test('Parse only year', () { var time = FixedDateTimeFormatter('YYYY').decode('1996'); - expect(time, DateTime(1996)); + expect(time, DateTime.utc(1996)); }); test('Escaped chars are ignored', () { var time = FixedDateTimeFormatter('YYYY kiwi MM').decode('1996 rnad 01'); - expect(time, DateTime(1996, 1)); + expect(time, DateTime.utc(1996, 1)); }); test('Parse two years throws', () { expect(() => FixedDateTimeFormatter('YYYY YYYY'), throwsException); }); test('Parse year and century', () { var time = FixedDateTimeFormatter('CCYY').decode('1996'); - expect(time, DateTime(1996)); + expect(time, DateTime.utc(1996)); }); test('Parse year, decade and century', () { var time = FixedDateTimeFormatter('CCEY').decode('1996'); - expect(time, DateTime(1996)); + expect(time, DateTime.utc(1996)); }); test('Parse year, century, month', () { var time = FixedDateTimeFormatter('CCYY MM').decode('1996 04'); - expect(time, DateTime(1996, 4)); + expect(time, DateTime.utc(1996, 4)); }); test('Parse year, century, month, day', () { var time = FixedDateTimeFormatter('CCYY MM-DD').decode('1996 04-25'); - expect(time, DateTime(1996, 4, 25)); + expect(time, DateTime.utc(1996, 4, 25)); }); test('Parse year, century, month, day, hour, minute, second', () { var time = FixedDateTimeFormatter('CCYY MM-DD hh:mm:ss') .decode('1996 04-25 05:03:22'); - expect(time, DateTime(1996, 4, 25, 5, 3, 22)); + expect(time, DateTime.utc(1996, 4, 25, 5, 3, 22)); }); - test('Parse YYYYMMDDhhmmss', () { + test('Parse YYYYMMDDhhmmssSSS', () { var time = - FixedDateTimeFormatter('YYYYMMDDhhmmss').decode('19960425050322'); - expect(time, DateTime(1996, 4, 25, 5, 3, 22)); + FixedDateTimeFormatter('YYYYMMDDhhmmssSSS').decode('19960425050322533'); + expect(time, DateTime.utc(1996, 4, 25, 5, 3, 22, 533)); + }); + test('Parse S', () { + var time = FixedDateTimeFormatter('S').decode('1'); + expect(time, DateTime.utc(0, 1, 1, 0, 0, 0, 100, 0)); + }); + test('Parse SS', () { + var time = FixedDateTimeFormatter('SS').decode('01'); + expect(time, DateTime.utc(0, 1, 1, 0, 0, 0, 10, 0)); + }); + test('Parse SSS', () { + var time = FixedDateTimeFormatter('SSS').decode('001'); + expect(time, DateTime.utc(0, 1, 1, 0, 0, 0, 1, 0)); + }); + test('Parse SSSSSS', () { + var time = FixedDateTimeFormatter('SSSSSS').decode('000001'); + expect(time, DateTime.utc(0, 1, 1, 0, 0, 0, 0, 1)); + }); + test('Parse SSSSSS 2', () { + var time = FixedDateTimeFormatter('SSSSSS').decode('001000'); + expect(time, DateTime.utc(0, 1, 1, 0, 0, 0, 1, 0)); }); test('Parse hex year throws', () { expect( @@ -53,32 +73,32 @@ //tryDecode test('Try parse year', () { var time = FixedDateTimeFormatter('YYYY').tryDecode('1996'); - expect(time, DateTime(1996)); + expect(time, DateTime.utc(1996)); }); test('Try parse hex year return default', () { var time = FixedDateTimeFormatter('YYYY').tryDecode('0xAB'); - expect(time, DateTime(0)); + expect(time, DateTime.utc(0)); }); test('Try parse invalid returns default', () { var time = FixedDateTimeFormatter('YYYY').tryDecode('1x96'); - expect(time, DateTime(0)); + expect(time, DateTime.utc(0)); }); //encode test('Format simple', () { - var time = DateTime(1996, 1); + var time = DateTime.utc(1996, 1); expect('1996 kiwi 01', FixedDateTimeFormatter('YYYY kiwi MM').encode(time)); }); test('Format YYYYMMDDhhmmss', () { var str = FixedDateTimeFormatter('YYYYMMDDhhmmss') - .encode(DateTime(1996, 4, 25, 5, 3, 22)); + .encode(DateTime.utc(1996, 4, 25, 5, 3, 22)); expect('19960425050322', str); }); test('Format CCEY-MM', () { - var str = FixedDateTimeFormatter('CCEY-MM').encode(DateTime(1996, 4)); + var str = FixedDateTimeFormatter('CCEY-MM').encode(DateTime.utc(1996, 4)); expect('1996-04', str); }); test('Format XCCEY-MMX', () { - var str = FixedDateTimeFormatter('XCCEY-MMX').encode(DateTime(1996, 4)); + var str = FixedDateTimeFormatter('XCCEY-MMX').encode(DateTime.utc(1996, 4)); expect('X1996-04X', str); }); }