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);
});
}