From b6be6b96fb8ed2fb17d768ff4d803435b3ee068b Mon Sep 17 00:00:00 2001 From: guilherme Date: Wed, 20 Mar 2024 22:09:43 +1030 Subject: [PATCH 01/30] feat(deps): promot intl to main dep --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 29623c7..9ce7b1d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,11 +8,11 @@ environment: sdk: '>=2.12.0 <4.0.0' dependencies: + intl: ^0.18.1 meta: ^1.0.0 dev_dependencies: http_multi_server: ">=2.2.0" - intl: ^0.18.1 lean_extensions: ">=0.7.0 <1.0.0" test: ^1.16.0 very_good_analysis: any From 9fa85376f354e6c6944c47379fa0abbfff54ad2a Mon Sep 17 00:00:00 2001 From: guilherme Date: Wed, 20 Mar 2024 22:10:56 +1030 Subject: [PATCH 02/30] feat: initial implementation of locale support --- lib/src/any_date_base.dart | 19 +++- lib/src/extensions.dart | 9 ++ lib/src/locale_based_rules.dart | 176 ++++++++++++++++++++++++++++++++ test/any_date_test.dart | 4 +- test/any_date_time_test.dart | 4 +- test/locale_based_test.dart | 166 +++++++----------------------- test/rfc_test.dart | 19 +++- test/test_values.dart | 11 +- 8 files changed, 265 insertions(+), 143 deletions(-) create mode 100644 lib/src/locale_based_rules.dart diff --git a/lib/src/any_date_base.dart b/lib/src/any_date_base.dart index 885f366..30d48da 100644 --- a/lib/src/any_date_base.dart +++ b/lib/src/any_date_base.dart @@ -1,5 +1,7 @@ import 'package:any_date/src/any_date_rules.dart'; import 'package:any_date/src/any_date_rules_model.dart'; +import 'package:any_date/src/locale_based_rules.dart'; +import 'package:intl/locale.dart'; import 'package:meta/meta.dart'; /// Parameters passed to the parser @@ -240,6 +242,7 @@ class DateParserInfo { ], this.months = _allMonths, this.weekdays = _allWeekdays, + this.customRules = const [], }); /// interpret the first value in an ambiguous case (e.g. 01/01/01) @@ -265,6 +268,9 @@ class DateParserInfo { /// keywords to identify weekdays (to support multiple languages) final List weekdays; + /// allow passing extra rules to parse the timestamp + final Iterable customRules; + /// copy with DateParserInfo copyWith({ bool? dayFirst, @@ -272,6 +278,7 @@ class DateParserInfo { List? allowedSeparators, List? months, List? weekdays, + Iterable? customRules, }) { return DateParserInfo( dayFirst: dayFirst ?? this.dayFirst, @@ -279,6 +286,7 @@ class DateParserInfo { allowedSeparators: allowedSeparators ?? this.allowedSeparators, months: months ?? this.months, weekdays: weekdays ?? this.weekdays, + customRules: customRules ?? this.customRules, ); } @@ -286,7 +294,7 @@ class DateParserInfo { String toString() { return 'DateParserInfo(dayFirst: $dayFirst, yearFirst: $yearFirst, ' 'allowedSeparators: $allowedSeparators, months: $months, ' - 'weekdays: $weekdays)'; + 'weekdays: $weekdays, customRules: $customRules)'; } } @@ -295,6 +303,11 @@ class AnyDate { /// default constructor const AnyDate({DateParserInfo? info}) : _info = info; + /// factory constructor to create an [AnyDate] obj based on [locale] + factory AnyDate.fromLocale(Locale locale) { + return locale.anyDate; + } + /// settings for parsing and resolving ambiguous cases // final DateParserInfo? info; @@ -367,7 +380,9 @@ class AnyDate { ); yield rfcRules.apply(p); - // return; + // custom rules are only applied after rfc rules + // TODO(gbassisp): maybe custom rules to run before custom rules + yield MultipleRules(i.customRules.toList()).apply(p); yield ambiguousCase.apply(p); yield MultipleRules(i.dayFirst ? _yearLastDayFirst : _yearLast).apply(p); diff --git a/lib/src/extensions.dart b/lib/src/extensions.dart index 863a6d4..16d02ff 100644 --- a/lib/src/extensions.dart +++ b/lib/src/extensions.dart @@ -1,4 +1,5 @@ import 'package:any_date/any_date.dart'; +import 'package:meta/meta.dart'; /// a collection of extensions on [DateTime] extension DateTimeExtension on DateTime { @@ -134,3 +135,11 @@ extension StringParsers on String { return res; } } + +/// simple implementation of python-like iterator +@internal +Iterable range(int size) sync* { + for (var i = 0; i < size; i++) { + yield i; + } +} diff --git a/lib/src/locale_based_rules.dart b/lib/src/locale_based_rules.dart new file mode 100644 index 0000000..7960bb9 --- /dev/null +++ b/lib/src/locale_based_rules.dart @@ -0,0 +1,176 @@ +import 'package:any_date/src/any_date_base.dart'; +import 'package:any_date/src/any_date_rules_model.dart'; +import 'package:any_date/src/extensions.dart'; +import 'package:intl/intl.dart'; +import 'package:intl/locale.dart'; +import 'package:meta/meta.dart'; + +final _rulesCache = {}; + +@internal +extension LocaleExtensions on Locale { + AnyDate get anyDate { + _rulesCache.putIfAbsent(_t, () => _anyDate); + + return _rulesCache[_t] ?? _anyDate; + } + + String get _t => toLanguageTag(); + + AnyDate get _anyDate { + try { + return AnyDate( + info: DateParserInfo( + dayFirst: !usesMonthFirst, + yearFirst: usesYearFirst, + months: [...longMonths, ...shortMonths], + weekdays: [...longWeekdays, ...shortWeekdays], + customRules: _parsingRules, + ), + ); + } catch (_) { + return AnyDate( + info: DateParserInfo( + // error is likely to be on attempting to guess months and weekdays + // default to using english ones, but add custom rules + customRules: _parsingRules, + ), + ); + } + } + + static final _date = DateTime(1234, 5, 6, 7, 8, 9); + + String get _yMd => DateFormat.yMd(_t).format(_date); + + bool get usesNumericSymbols { + try { + usesMonthFirst; + usesYearFirst; + return true; + } catch (_) { + return false; + } + } + + bool get usesMonthFirst { + final formatted = _yMd; + final fields = formatted.split(RegExp(r'\D')) + ..removeWhere((element) => element.trim().tryToInt() == null); + final numbers = fields.map((e) => e.toInt()); + + assert( + numbers.contains(5), + 'could not find test date in $this: $formatted', + ); + + final monthIndex = numbers.indexOf(5); + final dayIndex = numbers.indexOf(6); + + assert( + monthIndex != null && dayIndex != null, + 'month and day must both be present in $this: $formatted', + ); + return monthIndex! < dayIndex!; + } + + bool get usesYearFirst { + final formatted = _yMd; + final fields = formatted.split(RegExp(r'\D')) + ..removeWhere((element) => element.trim().tryToInt() == null); + final numbers = fields.map((e) => e.toInt()); + + assert( + numbers.contains(1234), + 'could not find test date in $this: $formatted', + ); + + final yearIndex = numbers.indexOf(1234); + final monthIndex = numbers.indexOf(5); + + assert( + yearIndex != null && monthIndex != null, + 'month and year must both be present in $this: $formatted', + ); + return yearIndex! < monthIndex!; + } + + Iterable get longMonths sync* { + final format = DateFormat('MMMM', toString()); + for (final i in range(12)) { + final m = i + 1; + final d = DateTime(1234, m, 10); + yield Month(number: m, name: format.format(d)); + } + } + + Iterable get shortMonths sync* { + final format = DateFormat('MMM', toString()); + for (final i in range(12)) { + final m = i + 1; + final d = DateTime(1234, m, 10); + yield Month(number: m, name: format.format(d)); + } + } + + Iterable get longWeekdays sync* { + final format = DateFormat('EEEE', toString()); + for (final i in range(7)) { + final w = i + 1; + final d = DateTime(2023, 10, 8 + w); + yield Weekday(number: w, name: format.format(d)); + } + } + + Iterable get shortWeekdays sync* { + final format = DateFormat('EEE', toString()); + for (final i in range(7)) { + final w = i + 1; + final d = DateTime(2023, 10, 8 + w); + yield Weekday(number: w, name: format.format(d)); + } + } + + Iterable get _dateOnly sync* { + yield DateFormat.yMMMMEEEEd(_t); + yield DateFormat.yMMMMd(_t); + yield DateFormat.yMMMd(_t); + yield DateFormat.yMMMEd(_t); + yield DateFormat.yMEd(_t); + yield DateFormat.yMd(_t); + } + + Iterable get _dateTime sync* { + for (final f in _dateOnly) { + yield f.add_Hms(); + yield f.add_Hm(); + yield f.add_H(); + yield f.add_jms(); + yield f.add_jm(); + yield f.add_j(); + } + } + + Iterable get _parsingRules sync* { + for (final f in _dateTime) { + yield SimpleRule((params) => f.parseLoose(params.originalString)); + yield SimpleRule((params) => f.parseLoose(params.formattedString)); + } + for (final f in _dateOnly) { + yield SimpleRule((params) => f.parseLoose(params.originalString)); + yield SimpleRule((params) => f.parseLoose(params.formattedString)); + } + } +} + +extension _ListExtension on Iterable { + int? indexOf(T element) { + for (final i in range(length)) { + if (elementAt(i) == element) { + return i; + } + } + + return null; + } +} diff --git a/test/any_date_test.dart b/test/any_date_test.dart index 9663a4e..55fcdfa 100644 --- a/test/any_date_test.dart +++ b/test/any_date_test.dart @@ -34,7 +34,7 @@ void testRange( ..removeWhere((e) => e.isInvalid); const step = Duration(hours: 23, minutes: 13); - for (final date in (customRange ?? range).every(step)) { + for (final date in (customRange ?? dateRange).every(step)) { for (final a in seps) { for (final b in seps) { final f = formatter(date, a, b); @@ -77,7 +77,7 @@ void compare(DateFormat format, AnyDate anyDate, {bool randomDates = true}) { // for (final singleDate in range.every(step)) { for (final r in [ // range.days, - range.every(step), + dateRange.every(step), if (randomDates) getRandomDates(), ]) { for (final singleDate in r) { diff --git a/test/any_date_time_test.dart b/test/any_date_time_test.dart index b19da2d..014cc95 100644 --- a/test/any_date_time_test.dart +++ b/test/any_date_time_test.dart @@ -12,7 +12,7 @@ final r = DateTimeRange( end: DateTime(hugeRange ? 2100 : 902, 12, 31, 15, 16, 17, 18), ); -final _range = range; +final _range = dateRange; void main() { group('default AnyDate()', () { const parser = AnyDate(); @@ -63,7 +63,7 @@ void main() { parser, (date, sep1, sep2) => DateFormat('yyyy${sep1}MMMM${sep2}d H-m').format(date), - range, + _range, false, ); }, diff --git a/test/locale_based_test.dart b/test/locale_based_test.dart index 8a8b7e1..3eb1f42 100644 --- a/test/locale_based_test.dart +++ b/test/locale_based_test.dart @@ -1,5 +1,5 @@ import 'package:any_date/any_date.dart'; -import 'package:any_date/src/extensions.dart'; +import 'package:any_date/src/locale_based_rules.dart'; import 'package:intl/date_symbol_data_file.dart' show availableLocalesForDateFormatting; import 'package:intl/date_symbol_data_local.dart'; @@ -7,14 +7,22 @@ import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; import 'package:intl/locale.dart'; import 'package:lean_extensions/collection_extensions.dart'; -import 'package:lean_extensions/dart_essentials.dart'; import 'package:test/test.dart'; import 'rfc_test.dart'; +// import 'test_values.dart' hide range; + +Iterable _formatFactory(String locale) sync* { + yield DateFormat.yMMMMd(locale); +} final _locales = availableLocalesForDateFormatting.map((e) => e).toList() ..removeWhere((element) { - final unsupported = ['ar', 'as', 'bn', 'fa', 'mr', 'my', 'ne', 'ps']; + final unsupported = ['ar', 'as', 'bn', 'fa', 'mr', 'my', 'ne', 'ps'] + // maybe unsupported? + // ignore: prefer_inlined_adds + ..addAll(['am', 'be', 'bg', 'ca']) + ..clear(); for (final l in unsupported) { if (element.startsWith(l)) { return true; @@ -24,129 +32,6 @@ final _locales = availableLocalesForDateFormatting.map((e) => e).toList() }); final _localeCodes = _locales.map(Locale.tryParse).whereNotNull().toList(); -extension _ListExtension on Iterable { - int? indexOf(T element) { - for (final i in range(length)) { - if (elementAt(i) == element) { - return i; - } - } - - return null; - } -} - -// TODO(gbassisp): promote this to lib/ once we enable support for locale -extension _LocaleExtensions on Locale { - AnyDate get anyDate => AnyDate( - info: DateParserInfo( - dayFirst: !usesMonthFirst, - yearFirst: usesYearFirst, - months: [...longMonths, ...shortMonths], - weekdays: [...longWeekdays, ...shortWeekdays], - ), - ); - - static final _date = DateTime(1234, 5, 6, 7, 8, 9); - - String get _yMd => DateFormat.yMd(toString()).format(_date); - - // DateParserInfo get parserInfo => DateParserInfo( - // yearFirst: usesYearFirst, - // dayFirst: !usesMonthFirst, - // months: [...longMonths, ...shortMonths], - // weekdays: [...longWeekdays, ...shortWeekdays], - // ); - - bool get usesNumericSymbols { - try { - usesMonthFirst; - usesYearFirst; - return true; - } catch (_) { - return false; - } - } - - bool get usesMonthFirst { - final formatted = _yMd; - final fields = formatted.split(RegExp(r'\D')) - ..removeWhere((element) => element.trim().tryToInt() == null); - final numbers = fields.map((e) => e.toInt()); - - assert( - numbers.contains(5), - 'could not find test date in $this: $formatted', - ); - - final monthIndex = numbers.indexOf(5); - final dayIndex = numbers.indexOf(6); - - assert( - monthIndex != null && dayIndex != null, - 'month and day must both be present in $this: $formatted', - ); - return monthIndex! < dayIndex!; - } - - bool get usesYearFirst { - final formatted = _yMd; - final fields = formatted.split(RegExp(r'\D')) - ..removeWhere((element) => element.trim().tryToInt() == null); - final numbers = fields.map((e) => e.toInt()); - - assert( - numbers.contains(1234), - 'could not find test date in $this: $formatted', - ); - - final yearIndex = numbers.indexOf(1234); - final monthIndex = numbers.indexOf(5); - - assert( - yearIndex != null && monthIndex != null, - 'month and year must both be present in $this: $formatted', - ); - return yearIndex! < monthIndex!; - } - - Iterable get longMonths sync* { - final format = DateFormat('MMMM', toString()); - for (final i in range(12)) { - final m = i + 1; - final d = DateTime(1234, m, 10); - yield Month(number: m, name: format.format(d)); - } - } - - Iterable get shortMonths sync* { - final format = DateFormat('MMM', toString()); - for (final i in range(12)) { - final m = i + 1; - final d = DateTime(1234, m, 10); - yield Month(number: m, name: format.format(d)); - } - } - - Iterable get longWeekdays sync* { - final format = DateFormat('EEEE', toString()); - for (final i in range(7)) { - final w = i + 1; - final d = DateTime(2023, 10, 8 + w); - yield Weekday(number: w, name: format.format(d)); - } - } - - Iterable get shortWeekdays sync* { - final format = DateFormat('EEE', toString()); - for (final i in range(7)) { - final w = i + 1; - final d = DateTime(2023, 10, 8 + w); - yield Weekday(number: w, name: format.format(d)); - } - } -} - Future main() async { await initializeDateFormatting(); @@ -171,13 +56,32 @@ Future main() async { expect(count, greaterThan(0)); }); + }); + group('all locales support rfc formats', () { + for (final l in _localeCodes) { + final parser = l.anyDate; + rfcTests(parser); + } + }); - group('all locales support rfc formats', () { - for (final l in _localeCodes) { - final parser = l.anyDate; - rfcTests(parser); + group('all locales can parse text month formats', () { + final date = DateTime.now(); + for (final l in _localeCodes) { + final parser = AnyDate.fromLocale(l); + for (final format in _formatFactory(l.toLanguageTag())) { + final formatted = format.format(date); + final reason = '$formatted on $l with format ${format.pattern}'; + test(reason, () { + final result = parser.tryParse(formatted); + final expected = format.parse(formatted); + expect( + result, + equals(expected), + reason: '$reason resulted in $result, but expected $expected', + ); + }); } - }); + } }); group('locale tests', () { diff --git a/test/rfc_test.dart b/test/rfc_test.dart index 548c8e7..6bcea3d 100644 --- a/test/rfc_test.dart +++ b/test/rfc_test.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:any_date/src/any_date_base.dart'; import 'package:any_date/src/date_range.dart'; import 'package:test/test.dart'; @@ -22,7 +24,7 @@ void rfcTests(AnyDate parser) { ...DateTimeRange( start: DateTime(1801), end: DateTime(1969), - ).everyNowAndThen, + ).random(), DateTime(1901), DateTime(1969), // 1969-09-23 09:30:00.000 (lower limit of ambiguity) @@ -36,7 +38,7 @@ void rfcTests(AnyDate parser) { ...DateTimeRange( start: DateTime(1971), end: DateTime(2138), - ).everyNowAndThen, + ).random(), // out of original limit by 100y DateTime(1801), @@ -109,7 +111,7 @@ void rfcTests(AnyDate parser) { for (final d in DateTimeRange( start: DateTime(1801), end: DateTime(2138), - ).everyNowAndThen) { + ).random()) { final t = d.secondsSinceEpoch; final s = parser.tryParse(t.toString()); final expected = d; @@ -128,7 +130,7 @@ void rfcTests(AnyDate parser) { // mixed up. we start getting milliseconds parsed as seconds start: DateTime.fromMillisecondsSinceEpoch(-secondsLimit), end: DateTime.fromMillisecondsSinceEpoch(secondsLimit), - ).everyNowAndThen) { + ).random()) { final ms = d.millisecondsSinceEpoch; final us = d.microsecondsSinceEpoch; final ns = d.nanosecondsSinceEpoch; @@ -295,10 +297,17 @@ extension _UnixTime on DateTime { int get nanosecondsSinceEpoch => microsecondsSinceEpoch * 1000; } -const _duration = Duration(days: 2, hours: 19, minutes: 11, seconds: 13); +const _duration = Duration(days: 22, hours: 19, minutes: 11, seconds: 13); extension _IterableRange on DateTimeRange { Iterable get everyNowAndThen => every(_duration); + Iterable random([int amount = 100]) { + final all = everyNowAndThen.toList()..shuffle(); + + final count = min(all.length, amount); + + return all.sublist(0, count - 1); + } // Iterable get minutes => every(const Duration(minutes: 1)); // Iterable get seconds => every(const Duration(seconds: 1)); } diff --git a/test/test_values.dart b/test/test_values.dart index 051e5c2..41e7f47 100644 --- a/test/test_values.dart +++ b/test/test_values.dart @@ -16,7 +16,7 @@ const hugeRange = bool.fromEnvironment('huge'); final now = DateTime(2000); // DateTime.now(); const _span = 1; -final range = DateTimeRange( +final dateRange = DateTimeRange( start: DateTime(now.year - _span, 7), end: DateTime(now.year + _span - 1, 7), ); @@ -60,6 +60,15 @@ Iterable getRandomDates([int? count]) sync* { final singleDate = DateTime(2023, 1, 2, 3, 4, 5, 6, 7); +final allFormats = { + ...otherFormats, + ...mdyFormats, + ...dmyFormats, + ...ymdFormats, +}; + +final textMonthFormats = allFormats.where((element) => element.contains('MMM')); + const otherFormats = { 'EEEE, MMMM d, y', 'EEEE, MMMM d, y h:m:s.SS a', From 17d914239b0bb355c7b41734f563a2b5b583cdbf Mon Sep 17 00:00:00 2001 From: guilherme Date: Wed, 20 Mar 2024 22:17:46 +1030 Subject: [PATCH 03/30] test: added locale parsers to test_values.dart --- test/test_values.dart | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/test/test_values.dart b/test/test_values.dart index 41e7f47..116cf0b 100644 --- a/test/test_values.dart +++ b/test/test_values.dart @@ -2,12 +2,19 @@ import 'dart:math'; import 'package:any_date/any_date.dart'; import 'package:any_date/src/date_range.dart'; +import 'package:intl/locale.dart'; -const parsers = [ - AnyDate(), - AnyDate(info: DateParserInfo(dayFirst: true)), - AnyDate(info: DateParserInfo(yearFirst: true)), - AnyDate(info: DateParserInfo(dayFirst: true, yearFirst: true)), +final parsers = [ + const AnyDate(), + const AnyDate(info: DateParserInfo(dayFirst: true)), + const AnyDate(info: DateParserInfo(yearFirst: true)), + const AnyDate(info: DateParserInfo(dayFirst: true, yearFirst: true)), + AnyDate.fromLocale(Locale.parse('en')), + AnyDate.fromLocale(Locale.parse('en-US')), + AnyDate.fromLocale(Locale.parse('en-UK')), + AnyDate.fromLocale(Locale.parse('en-AU')), + AnyDate.fromLocale(Locale.parse('en-NZ')), + AnyDate.fromLocale(Locale.parse('en-CA')), ]; /// used to run tests on a wide range of dates From e5db796e37a91f9a551b8907fac287dfc460f70e Mon Sep 17 00:00:00 2001 From: guilherme Date: Thu, 21 Mar 2024 20:58:13 +1030 Subject: [PATCH 04/30] feat: allow any type of input on parse function --- lib/src/any_date_base.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/src/any_date_base.dart b/lib/src/any_date_base.dart index 30d48da..151cad4 100644 --- a/lib/src/any_date_base.dart +++ b/lib/src/any_date_base.dart @@ -322,9 +322,14 @@ class AnyDate { /// parses a string in any format into a [DateTime] object. DateTime parse( /// required string representation of a date to be parsed - String formattedString, + Object? formattedString, ) { - final res = _tryParse(formattedString); + DateTime? res; + if (formattedString != null) { + res = formattedString is DateTime + ? formattedString + : _tryParse(formattedString.toString()); + } if (res == null) { throw FormatException('Invalid date format', formattedString); } @@ -336,7 +341,7 @@ class AnyDate { /// Returns null if the string is not a valid date. /// /// Does not handle other exceptions. - DateTime? tryParse(String formattedString) { + DateTime? tryParse(Object? formattedString) { try { return parse(formattedString); } on FormatException { From 11e9a8456a2846368ddb9cb841f807d614b78c20 Mon Sep 17 00:00:00 2001 From: guilherme Date: Wed, 27 Mar 2024 21:02:16 +1030 Subject: [PATCH 05/30] refactor: avoid exposing date parsing rule model --- lib/any_date.dart | 5 ++++- lib/src/any_date_base.dart | 6 +++--- lib/src/any_date_rules_model.dart | 16 +++++++++++++--- lib/src/locale_based_rules.dart | 10 +++++----- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/lib/any_date.dart b/lib/any_date.dart index cb0e66e..6b5d10d 100644 --- a/lib/any_date.dart +++ b/lib/any_date.dart @@ -1,4 +1,7 @@ /// A package to parse date in any format with minimum dependencies. library any_date; -export 'src/any_date_base.dart' show AnyDate, DateParserInfo, Month, Weekday; +export 'src/any_date_base.dart' + show AnyDate, DateParserInfo, DateParsingParameters, Month, Weekday; + +export 'src/any_date_rules_model.dart' show DateParsingFunction; diff --git a/lib/src/any_date_base.dart b/lib/src/any_date_base.dart index 151cad4..4d0a6be 100644 --- a/lib/src/any_date_base.dart +++ b/lib/src/any_date_base.dart @@ -269,7 +269,7 @@ class DateParserInfo { final List weekdays; /// allow passing extra rules to parse the timestamp - final Iterable customRules; + final Iterable customRules; /// copy with DateParserInfo copyWith({ @@ -278,7 +278,7 @@ class DateParserInfo { List? allowedSeparators, List? months, List? weekdays, - Iterable? customRules, + Iterable? customRules, }) { return DateParserInfo( dayFirst: dayFirst ?? this.dayFirst, @@ -387,7 +387,7 @@ class AnyDate { yield rfcRules.apply(p); // custom rules are only applied after rfc rules // TODO(gbassisp): maybe custom rules to run before custom rules - yield MultipleRules(i.customRules.toList()).apply(p); + yield MultipleRules.fromFunctions(i.customRules).apply(p); yield ambiguousCase.apply(p); yield MultipleRules(i.dayFirst ? _yearLastDayFirst : _yearLast).apply(p); diff --git a/lib/src/any_date_rules_model.dart b/lib/src/any_date_rules_model.dart index 207ee40..7090951 100644 --- a/lib/src/any_date_rules_model.dart +++ b/lib/src/any_date_rules_model.dart @@ -1,7 +1,11 @@ -// ignore_for_file: public_member_api_docs - import 'package:any_date/src/any_date_base.dart'; +import 'package:meta/meta.dart'; + +/// A function that takes a [DateParsingParameters] object and tries to convert +/// to a [DateTime] object. +typedef DateParsingFunction = DateTime? Function(DateParsingParameters params); +@internal abstract class DateParsingRule { DateParsingRule(this.rules); final List rules; @@ -9,9 +13,10 @@ abstract class DateParsingRule { DateTime? apply(DateParsingParameters parameters); } +@internal class SimpleRule extends DateParsingRule { SimpleRule(this._rule, {this.validate = true}) : super([]); - final DateTime? Function(DateParsingParameters params) _rule; + final DateParsingFunction _rule; final bool validate; @override @@ -42,9 +47,14 @@ class SimpleRule extends DateParsingRule { } } +@internal class MultipleRules extends DateParsingRule { MultipleRules(List rules) : super(rules); + factory MultipleRules.fromFunctions(Iterable functions) { + return MultipleRules(functions.map((e) => SimpleRule(e)).toList()); + } + @override DateTime? apply(DateParsingParameters parameters) { return _applyAll(parameters).firstWhere( diff --git a/lib/src/locale_based_rules.dart b/lib/src/locale_based_rules.dart index 7960bb9..ade17ad 100644 --- a/lib/src/locale_based_rules.dart +++ b/lib/src/locale_based_rules.dart @@ -151,14 +151,14 @@ extension LocaleExtensions on Locale { } } - Iterable get _parsingRules sync* { + Iterable get _parsingRules sync* { for (final f in _dateTime) { - yield SimpleRule((params) => f.parseLoose(params.originalString)); - yield SimpleRule((params) => f.parseLoose(params.formattedString)); + yield (params) => f.parseLoose(params.originalString); + yield (params) => f.parseLoose(params.formattedString); } for (final f in _dateOnly) { - yield SimpleRule((params) => f.parseLoose(params.originalString)); - yield SimpleRule((params) => f.parseLoose(params.formattedString)); + yield (params) => f.parseLoose(params.originalString); + yield (params) => f.parseLoose(params.formattedString); } } } From c598bb63cac35fffde12859bcf34217ff5633fd3 Mon Sep 17 00:00:00 2001 From: guilherme Date: Wed, 27 Mar 2024 21:39:42 +1030 Subject: [PATCH 06/30] chore: ensure formatting after make fix --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 7f4128a..6cf0638 100644 --- a/Makefile +++ b/Makefile @@ -71,6 +71,7 @@ analyze: fix: $(DART_CMD) format . $(DART_CMD) fix --apply + $(DART_CMD) format . .PHONY: version version: From 8c5bc02c54f27f195748e7b88d91addf8913c33a Mon Sep 17 00:00:00 2001 From: guilherme Date: Wed, 27 Mar 2024 21:40:16 +1030 Subject: [PATCH 07/30] fix(test): ensure date formatting initialised --- test/locale_based_test.dart | 3 +-- test/rfc_test.dart | 4 +++- test/test_values.dart | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/test/locale_based_test.dart b/test/locale_based_test.dart index 3eb1f42..7f19eec 100644 --- a/test/locale_based_test.dart +++ b/test/locale_based_test.dart @@ -2,7 +2,6 @@ import 'package:any_date/any_date.dart'; import 'package:any_date/src/locale_based_rules.dart'; import 'package:intl/date_symbol_data_file.dart' show availableLocalesForDateFormatting; -import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; import 'package:intl/locale.dart'; @@ -10,7 +9,7 @@ import 'package:lean_extensions/collection_extensions.dart'; import 'package:test/test.dart'; import 'rfc_test.dart'; -// import 'test_values.dart' hide range; +import 'test_values.dart'; Iterable _formatFactory(String locale) sync* { yield DateFormat.yMMMMd(locale); diff --git a/test/rfc_test.dart b/test/rfc_test.dart index 6bcea3d..19314b3 100644 --- a/test/rfc_test.dart +++ b/test/rfc_test.dart @@ -6,7 +6,8 @@ import 'package:test/test.dart'; import 'test_values.dart'; -void main() { +Future main() async { + await initializeDateFormatting(); group('main RFC tests', () { for (final parser in parsers) { rfcTests(parser); @@ -15,6 +16,7 @@ void main() { } void rfcTests(AnyDate parser) { + ensureDateFormattingInitialized(); final parse = parser.parse; final info = parser.info; final nameSuffix = info.toString(); diff --git a/test/test_values.dart b/test/test_values.dart index 116cf0b..16a2b9a 100644 --- a/test/test_values.dart +++ b/test/test_values.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:any_date/any_date.dart'; import 'package:any_date/src/date_range.dart'; +import 'package:intl/date_symbol_data_local.dart' as intl; import 'package:intl/locale.dart'; final parsers = [ @@ -17,6 +18,23 @@ final parsers = [ AnyDate.fromLocale(Locale.parse('en-CA')), ]; +bool _hasInitialized = false; +Future initializeDateFormatting() async { + if (!_hasInitialized) { + await intl.initializeDateFormatting(); + _hasInitialized = true; + } + ensureDateFormattingInitialized(); +} + +void ensureDateFormattingInitialized() { + assert( + _hasInitialized, + 'locale aware tests must initalize date formatting ' + 'by calling initializeDateFormatting on main()', + ); +} + /// used to run tests on a wide range of dates const exhaustiveTests = bool.fromEnvironment('exhaustive', defaultValue: true); const hugeRange = bool.fromEnvironment('huge'); From dfbf3d47d0a06c9f829842d413303f6e043bfe1f Mon Sep 17 00:00:00 2001 From: guilherme Date: Wed, 27 Mar 2024 22:00:31 +1030 Subject: [PATCH 08/30] test: added some locale specific cases --- test/locale_based_test.dart | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/locale_based_test.dart b/test/locale_based_test.dart index 7f19eec..32469aa 100644 --- a/test/locale_based_test.dart +++ b/test/locale_based_test.dart @@ -15,6 +15,9 @@ Iterable _formatFactory(String locale) sync* { yield DateFormat.yMMMMd(locale); } +final _englishLocales = _localeCodes.map((e) => e).toList() + ..removeWhere((element) => !element.toLanguageTag().startsWith('en')); + final _locales = availableLocalesForDateFormatting.map((e) => e).toList() ..removeWhere((element) { final unsupported = ['ar', 'as', 'bn', 'fa', 'mr', 'my', 'ne', 'ps'] @@ -117,4 +120,30 @@ Future main() async { expect(locale.shortWeekdays, containsAllInOrder(shortWeekdays)); }); }); + + group('locale specific cases', () { + const unambiguousEnglish = { + 'March 27, 2024', + 'March 27 2024', + '27 March 2024', + 'Mar 27, 2024', + 'Mar 27 2024', + '27 Mar 2024', + }; + final expected = DateTime(2024, 3, 27); + + for (final l in _englishLocales) { + test('simple english date in any english locale $l', () { + for (final d in unambiguousEnglish) { + final p = AnyDate.fromLocale(l); + final res = p.tryParse(d); + expect( + res, + equals(expected), + reason: 'expected $expected for $d with locale $l', + ); + } + }); + } + }); } From 088bee0ff7cd0f2a96923de781d3885377dbdcd9 Mon Sep 17 00:00:00 2001 From: guilherme Date: Wed, 27 Mar 2024 22:20:09 +1030 Subject: [PATCH 09/30] chore: improve docs --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1d9901b..b5b0e60 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,19 @@ and the Flutter guide for Package to improve DateTime manipulation, especially by allowing parsing any format. Heavily inspired by python's [dateutil](https://dateutil.readthedocs.io/en/stable/parser.html) package. -## Usage + +## Summary + +In a glance, these are the features: + +1. Easy parsing of a `String` with **many** date formats into a `DateTime` object +2. Same flexibility supported in almost any `Locale`. Not just English, but any language and culture +3. Always compliant with `ISO 8601` and major `RFC`s (822, 2822, 1123, 1036 and 3339), regardless of `Locale` +4. Supports UNIX time in either `seconds`, `milliseconds`, `microseconds`, or `nanoseconds` since epoch + + + +## Basic usage Usage is simple, use the `AnyDate()` constructor to create a parser with the desired settings, and use it to parse any `String` into `DateTime`, regardless of the format. @@ -27,6 +39,26 @@ final stillTheSame = parser.parser('2023, August 13'); // in all cases date is parsed as DateTime(2023, 08, 13) ``` + +However, you may notice that the example above is in English. What if you want a different `Locale`? You can use the `AnyDate.fromLocale()` factory method to get the desired parser: + +```dart +// American English +final parser1 = AnyDate.fromLocale('en-US'); +final date1 = parser.parse('August 13, 2023'); +// note that even if formatting is unusual for locale, it can still understand unambiguous dates +final sameDate = parser.parse('13 August 2023'); // this is not common for US locale, but it still parses normally + + +// Brazilian Portuguese +final parser2 = AnyDate.fromLocale('pt-BR'); +final date2 = parser.parse('13 de Agosto de 2023'); + +// again, they all resolve to same DateTime value +``` + +## Solving ambiguous cases + Moreover, the parser can be used to solve ambiguous cases. Look at the following example: ```dart @@ -50,4 +82,23 @@ const parser3 = AnyDate(info: info); final case3 = a.parse(ambiguousDate); // results in DateTime(2001, 2, 3); ``` -It currently has basic support for time component as well, but there is still some work in progress. Feedback appreciated. + + +Using a `Locale` based parser also allows you to solve ambiguity based on that culture: + +```dart +// same example: +const ambiguousDate = '01/02/03'; + +// American English +final parser1 = AnyDate.fromLocale('en-US'); +final date1 = parser.parse(ambiguousDate); // the ambiguous date results in Jan 2, 2003 (mm/dd/yy) + + +// Brazilian Portuguese +final parser2 = AnyDate.fromLocale('pt-BR'); +final date2 = parser.parse(ambiguousDate); // the ambiguous date results in Feb 1, 2003 (dd/mm/yy) +``` + + +Feedback appreciated 💙 From d15644d67631570a6b770c5c3e019710b5e9fe0c Mon Sep 17 00:00:00 2001 From: guilherme Date: Wed, 27 Mar 2024 22:31:38 +1030 Subject: [PATCH 10/30] chore: update todos --- lib/src/any_date_base.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/any_date_base.dart b/lib/src/any_date_base.dart index 4d0a6be..c550780 100644 --- a/lib/src/any_date_base.dart +++ b/lib/src/any_date_base.dart @@ -350,11 +350,12 @@ class AnyDate { } DateTime? _tryParse(String formattedString) { - // TODO(gbassip): allow the following: - // missing components will be assumed to default value: - // e.g. 'Jan 2023' becomes DateTime(2023, 1), which is 1 Jan 2023 - // if year is missing, the closest result to today is chosen. - + /* + TODO(gbassisp): allow the following: + missing components will be assumed to default value: + e.g. 'Jan 2023' becomes DateTime(2023, 1), which is 1 Jan 2023 + if year is missing, the closest result to today is chosen. + */ return _applyRules(formattedString).firstWhere( (e) => e != null, orElse: () => null, @@ -386,7 +387,6 @@ class AnyDate { yield rfcRules.apply(p); // custom rules are only applied after rfc rules - // TODO(gbassisp): maybe custom rules to run before custom rules yield MultipleRules.fromFunctions(i.customRules).apply(p); yield ambiguousCase.apply(p); From 3d52a595b4d18e761c0a1dcd148abbddd1fbeb22 Mon Sep 17 00:00:00 2001 From: guilherme Date: Wed, 27 Mar 2024 22:31:57 +1030 Subject: [PATCH 11/30] chore: bump major version --- CHANGELOG.md | 15 +++++++++++++++ pubspec.yaml | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0792068..5608a81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,19 @@ +## 1.0.0 + +Stable version release + +### Features + +- Supports any type of `Object` for parsing, such as `int` for unix epoch +- Allows passing custom parsing rules to `AnyDate(customRules: ...)` +- Added `AnyDate.fromLocale()` factory to support other languages. Essentially, any language can be used + +### Breaking changes + +- Requires `intl` dependency to support languages other than English + + ## 0.1.13 - Added test cases to ensure RFC 822, 2822, 1036, 1123, and 3339 are supported diff --git a/pubspec.yaml b/pubspec.yaml index 9ce7b1d..4aa0b77 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: any_date -description: A package for parsing String into DateTime in any format. Inspired by python's dateutil.parser, while also making it compatible with DateTime.parse. -version: 0.1.13 +description: A package for parsing String into DateTime in any format. Supports any formats in any language while always respecting ISO and RFC formats. +version: 1.0.0 repository: https://github.com/gbassisp/any_date homepage: https://github.com/gbassisp/any_date From 48383aede13453e5072e80ec7f09c30379c18152 Mon Sep 17 00:00:00 2001 From: guilherme Date: Wed, 27 Mar 2024 22:42:56 +1030 Subject: [PATCH 12/30] feat: allow any input for AnyDate.fromLocale() --- lib/src/any_date_base.dart | 17 +++++++++++++++-- test/locale_based_test.dart | 8 ++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/lib/src/any_date_base.dart b/lib/src/any_date_base.dart index c550780..6d449a0 100644 --- a/lib/src/any_date_base.dart +++ b/lib/src/any_date_base.dart @@ -304,8 +304,21 @@ class AnyDate { const AnyDate({DateParserInfo? info}) : _info = info; /// factory constructor to create an [AnyDate] obj based on [locale] - factory AnyDate.fromLocale(Locale locale) { - return locale.anyDate; + factory AnyDate.fromLocale(Object? locale) { + if (locale is Locale) { + return locale.anyDate; + } + + final localeString = locale?.toString(); + if (localeString != null) { + final parsedLocale = Locale.tryParse(localeString); + if (parsedLocale != null) { + return parsedLocale.anyDate; + } + } + + // TODO(gbassisp): add logging function to warn about invalid Locale + return const AnyDate(); } /// settings for parsing and resolving ambiguous cases diff --git a/test/locale_based_test.dart b/test/locale_based_test.dart index 32469aa..108ee94 100644 --- a/test/locale_based_test.dart +++ b/test/locale_based_test.dart @@ -64,13 +64,17 @@ Future main() async { final parser = l.anyDate; rfcTests(parser); } + + // invalid locale + rfcTests(AnyDate.fromLocale(null)); }); group('all locales can parse text month formats', () { final date = DateTime.now(); - for (final l in _localeCodes) { + for (final l in [..._locales, ..._localeCodes]) { final parser = AnyDate.fromLocale(l); - for (final format in _formatFactory(l.toLanguageTag())) { + for (final format + in _formatFactory(l is Locale ? l.toLanguageTag() : l.toString())) { final formatted = format.format(date); final reason = '$formatted on $l with format ${format.pattern}'; test(reason, () { From 81db55b9d5bcd6a4d213ac55cbcf965c2a2981b3 Mon Sep 17 00:00:00 2001 From: guilherme Date: Tue, 2 Apr 2024 20:51:28 +1030 Subject: [PATCH 13/30] refactor: simplified public interface --- lib/any_date.dart | 3 +-- lib/src/any_date_rules_model.dart | 24 ++++++++++++++++++++---- lib/src/locale_based_rules.dart | 6 ++---- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/any_date.dart b/lib/any_date.dart index 6b5d10d..9c07738 100644 --- a/lib/any_date.dart +++ b/lib/any_date.dart @@ -1,7 +1,6 @@ /// A package to parse date in any format with minimum dependencies. library any_date; -export 'src/any_date_base.dart' - show AnyDate, DateParserInfo, DateParsingParameters, Month, Weekday; +export 'src/any_date_base.dart' show AnyDate, DateParserInfo, Month, Weekday; export 'src/any_date_rules_model.dart' show DateParsingFunction; diff --git a/lib/src/any_date_rules_model.dart b/lib/src/any_date_rules_model.dart index 7090951..ec15037 100644 --- a/lib/src/any_date_rules_model.dart +++ b/lib/src/any_date_rules_model.dart @@ -1,9 +1,16 @@ import 'package:any_date/src/any_date_base.dart'; import 'package:meta/meta.dart'; -/// A function that takes a [DateParsingParameters] object and tries to convert +/// A function that takes a [String] and tries to convert /// to a [DateTime] object. -typedef DateParsingFunction = DateTime? Function(DateParsingParameters params); +typedef DateParsingFunction = DateTime? Function(String params); + +/// A function that takes the entire [DateParsingParameters] and converts to +/// a [DateTime] object. +@internal +typedef CompleteDateParsingFunction = DateTime? Function( + DateParsingParameters params, +); @internal abstract class DateParsingRule { @@ -16,7 +23,7 @@ abstract class DateParsingRule { @internal class SimpleRule extends DateParsingRule { SimpleRule(this._rule, {this.validate = true}) : super([]); - final DateParsingFunction _rule; + final CompleteDateParsingFunction _rule; final bool validate; @override @@ -52,7 +59,16 @@ class MultipleRules extends DateParsingRule { MultipleRules(List rules) : super(rules); factory MultipleRules.fromFunctions(Iterable functions) { - return MultipleRules(functions.map((e) => SimpleRule(e)).toList()); + return MultipleRules( + functions + .map( + (e) => MultipleRules([ + SimpleRule((params) => e(params.originalString)), + SimpleRule((params) => e(params.formattedString)), + ]), + ) + .toList(), + ); } @override diff --git a/lib/src/locale_based_rules.dart b/lib/src/locale_based_rules.dart index ade17ad..c888cbe 100644 --- a/lib/src/locale_based_rules.dart +++ b/lib/src/locale_based_rules.dart @@ -153,12 +153,10 @@ extension LocaleExtensions on Locale { Iterable get _parsingRules sync* { for (final f in _dateTime) { - yield (params) => f.parseLoose(params.originalString); - yield (params) => f.parseLoose(params.formattedString); + yield f.parseLoose; } for (final f in _dateOnly) { - yield (params) => f.parseLoose(params.originalString); - yield (params) => f.parseLoose(params.formattedString); + yield f.parseLoose; } } } From 03621028acc916fc04022a29844d4bf92deb2a55 Mon Sep 17 00:00:00 2001 From: guilherme Date: Tue, 2 Apr 2024 21:37:54 +1030 Subject: [PATCH 14/30] refactor: simplify interface values --- lib/src/any_date_base.dart | 96 ++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/lib/src/any_date_base.dart b/lib/src/any_date_base.dart index 6d449a0..dc3d515 100644 --- a/lib/src/any_date_base.dart +++ b/lib/src/any_date_base.dart @@ -5,6 +5,7 @@ import 'package:intl/locale.dart'; import 'package:meta/meta.dart'; /// Parameters passed to the parser +@internal class DateParsingParameters { /// default constructor const DateParsingParameters({ @@ -54,12 +55,11 @@ class DateParsingParameters { } } -/// A month, with its number and name. Used to support multiple languages -/// without adding another dependency. -/// -/// It seems far fetched to have this class here, but it follows a similar -/// approach to the Python dateutil package. See: -/// https://dateutil.readthedocs.io/en/stable/_modules/dateutil/parser/_parser.html#parserinfo +/// A month, with its number and name. +/// It seems far fetched to have this class, but it follows a similar +/// approach to the Python dateutil package, without relying on a list index. +/// See: +/// [https://dateutil.readthedocs.io/en/stable/_modules/dateutil/parser/_parser.html](https://dateutil.readthedocs.io/en/stable/_modules/dateutil/parser/_parser.html#parserinfo) @immutable class Month { /// default constructor @@ -83,21 +83,17 @@ class Month { } } -/// A weekday with its number and name. Used to support multiple languages -/// without adding another dependency. -/// -/// Must match DateTime().weekday -/// +/// A weekday with its number and name Must match DateTime.weekday; /// Reason for this is the same as for [Month] @immutable class Weekday { /// default constructor const Weekday({required this.number, required this.name}); - /// month number + /// weekday number that matches DateTime.weekday final int number; - /// month name + /// weekday name final String name; @override @@ -189,11 +185,6 @@ String _removeWeekday(DateParsingParameters parameters) { return _removeExcessiveSeparators( parameters.copyWith(formattedString: formattedString), ); - // if (parameters.formattedString != formattedString) { - // print('removed weekday: ${parameters.formattedString} ' - // '-> $formattedString'); - // } - // return formattedString; } String _removeExcessiveSeparators(DateParsingParameters parameters) { @@ -226,24 +217,18 @@ String _trimSeparators(String formattedString, Iterable separators) { class DateParserInfo { /// default constructor const DateParserInfo({ - this.dayFirst = false, - this.yearFirst = false, - // TODO(gbassisp): avoid messing up regex with special chars - this.allowedSeparators = const [ - ' ', - 't', - 'T', - ':', - '.', - ',', - '_', - '/', - '-', - ], - this.months = _allMonths, - this.weekdays = _allWeekdays, - this.customRules = const [], - }); + bool? dayFirst, + bool? yearFirst, + List? allowedSeparators, + List? months, + List? weekdays, + Iterable? customRules, + }) : dayFirst = dayFirst ?? false, + yearFirst = yearFirst ?? false, + allowedSeparators = allowedSeparators ?? _defaultSeparators, + months = months ?? _allMonths, + weekdays = weekdays ?? _allWeekdays, + customRules = customRules ?? const []; /// interpret the first value in an ambiguous case (e.g. 01/01/01) /// as day true or month false. @@ -275,17 +260,17 @@ class DateParserInfo { DateParserInfo copyWith({ bool? dayFirst, bool? yearFirst, - List? allowedSeparators, - List? months, - List? weekdays, + Iterable? allowedSeparators, + Iterable? months, + Iterable? weekdays, Iterable? customRules, }) { return DateParserInfo( dayFirst: dayFirst ?? this.dayFirst, yearFirst: yearFirst ?? this.yearFirst, - allowedSeparators: allowedSeparators ?? this.allowedSeparators, - months: months ?? this.months, - weekdays: weekdays ?? this.weekdays, + allowedSeparators: allowedSeparators?.toList() ?? this.allowedSeparators, + months: months?.toList() ?? this.months, + weekdays: weekdays?.toList() ?? this.weekdays, customRules: customRules ?? this.customRules, ); } @@ -335,16 +320,14 @@ class AnyDate { /// parses a string in any format into a [DateTime] object. DateTime parse( /// required string representation of a date to be parsed - Object? formattedString, + Object? timestamp, ) { DateTime? res; - if (formattedString != null) { - res = formattedString is DateTime - ? formattedString - : _tryParse(formattedString.toString()); + if (timestamp != null) { + res = timestamp is DateTime ? timestamp : _tryParse(timestamp.toString()); } if (res == null) { - throw FormatException('Invalid date format', formattedString); + throw FormatException('Invalid date format', timestamp); } return res; } @@ -354,9 +337,9 @@ class AnyDate { /// Returns null if the string is not a valid date. /// /// Does not handle other exceptions. - DateTime? tryParse(Object? formattedString) { + DateTime? tryParse(Object? timestamp) { try { - return parse(formattedString); + return parse(timestamp); } on FormatException { return null; } @@ -492,3 +475,16 @@ const _shortWeekdays = [ ]; const _allWeekdays = [..._weekdays, ..._shortWeekdays]; + +// TODO(gbassisp): avoid messing up regex with special chars +const _defaultSeparators = [ + ' ', + 't', + 'T', + ':', + '.', + ',', + '_', + '/', + '-', +]; From 7ef1d333719a9ec4ece35e21cb57405d6070d1f0 Mon Sep 17 00:00:00 2001 From: guilherme Date: Mon, 15 Apr 2024 20:38:02 +0930 Subject: [PATCH 15/30] test: remove order constraint in months for a given locale --- test/locale_based_test.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/locale_based_test.dart b/test/locale_based_test.dart index 108ee94..2419f4e 100644 --- a/test/locale_based_test.dart +++ b/test/locale_based_test.dart @@ -104,24 +104,24 @@ Future main() async { expect(locale.usesMonthFirst, isTrue); expect(locale.usesYearFirst, isFalse); - expect(locale.longMonths, containsAllInOrder(longMonths)); - expect(locale.shortMonths, containsAllInOrder(shortMonths)); - expect(locale.longWeekdays, containsAllInOrder(longWeekdays)); - expect(locale.shortWeekdays, containsAllInOrder(shortWeekdays)); + expect(longMonths, containsAll(locale.longMonths)); + expect(shortMonths, containsAll(locale.shortMonths)); + expect(longWeekdays, containsAll(locale.longWeekdays)); + expect(shortWeekdays, containsAll(locale.shortWeekdays)); }); test('english speaking - normal format', () { final locale = Locale.fromSubtags(languageCode: 'en', countryCode: 'AU'); final longMonths = englishMonths.sublist(0, 12); - final shortMorhts = englishMonths.sublist(12) + final shortMonths = englishMonths.sublist(12) ..removeWhere((element) => element.name == 'Sep'); expect(locale.usesMonthFirst, isFalse); expect(locale.usesYearFirst, isFalse); - expect(locale.longMonths, containsAllInOrder(longMonths)); - expect(locale.shortMonths, containsAllInOrder(shortMorhts)); - expect(locale.longWeekdays, containsAllInOrder(longWeekdays)); - expect(locale.shortWeekdays, containsAllInOrder(shortWeekdays)); + expect(longMonths, containsAll(locale.longMonths)); + expect(shortMonths, containsAll(locale.shortMonths)); + expect(longWeekdays, containsAll(locale.longWeekdays)); + expect(shortWeekdays, containsAll(locale.shortWeekdays)); }); }); From 6154efeeb062430402180e7335e57c007e66c5a8 Mon Sep 17 00:00:00 2001 From: guilherme Date: Mon, 15 Apr 2024 20:43:14 +0930 Subject: [PATCH 16/30] feat: added 4-letter month to MMM values --- lib/src/any_date_base.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/any_date_base.dart b/lib/src/any_date_base.dart index dc3d515..430e39d 100644 --- a/lib/src/any_date_base.dart +++ b/lib/src/any_date_base.dart @@ -438,6 +438,8 @@ const _shortMonths = [ Month(number: 5, name: 'May'), Month(number: 6, name: 'Jun'), Month(number: 7, name: 'Jul'), + Month(number: 6, name: 'June'), + Month(number: 7, name: 'July'), Month(number: 8, name: 'Aug'), Month(number: 9, name: 'Sep'), Month(number: 9, name: 'Sept'), From 5932c5f5e6d4398fdfb592c9762dea4a45a63ead Mon Sep 17 00:00:00 2001 From: guilherme Date: Mon, 15 Apr 2024 20:46:15 +0930 Subject: [PATCH 17/30] feat: added support for intl 0.19 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 4aa0b77..4996fb8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ environment: sdk: '>=2.12.0 <4.0.0' dependencies: - intl: ^0.18.1 + intl: ">=0.18.1 <0.20.0" meta: ^1.0.0 dev_dependencies: From bc1b2a5926324c5599d789da5d2158495952f0a7 Mon Sep 17 00:00:00 2001 From: guilherme Date: Mon, 15 Apr 2024 20:49:34 +0930 Subject: [PATCH 18/30] chore: bump fix version --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5608a81..a307999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ +## 1.0.1 + +- Added support for `intl` 0.19 + ## 1.0.0 Stable version release diff --git a/pubspec.yaml b/pubspec.yaml index 4996fb8..06b8ea0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: any_date description: A package for parsing String into DateTime in any format. Supports any formats in any language while always respecting ISO and RFC formats. -version: 1.0.0 +version: 1.0.1 repository: https://github.com/gbassisp/any_date homepage: https://github.com/gbassisp/any_date From 6377acb2c7ce9b4c0680ce48c38efa53c5569fd6 Mon Sep 17 00:00:00 2001 From: guilherme Date: Mon, 15 Apr 2024 21:02:54 +0930 Subject: [PATCH 19/30] fix(test): rename conflicted member --- test/any_date_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/any_date_test.dart b/test/any_date_test.dart index 55fcdfa..0653b67 100644 --- a/test/any_date_test.dart +++ b/test/any_date_test.dart @@ -58,7 +58,7 @@ void testRange( } extension _TryParse on DateFormat { - DateTime? tryParse(String input) { + DateTime? _tryParse(String input) { try { return parse(input); } catch (_) { @@ -85,7 +85,7 @@ void compare(DateFormat format, AnyDate anyDate, {bool randomDates = true}) { final d = f.format(singleDate); final a = anyDate; final config = a.info; - final e = f.tryParse(d); + final e = f._tryParse(d); // expect(e, isNotNull, reason: 'DateFormat failed: $reason'); final r = a.tryParse(d); final reason = 'format: ${format.pattern}\n ' From 933cf67b2a3e3c115dee0ef1287c2da79d58e0ac Mon Sep 17 00:00:00 2001 From: guilherme Date: Mon, 15 Apr 2024 21:57:11 +0930 Subject: [PATCH 20/30] chore: update package description --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 06b8ea0..8420ccd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: any_date -description: A package for parsing String into DateTime in any format. Supports any formats in any language while always respecting ISO and RFC formats. -version: 1.0.1 +description: A package for parsing String into DateTime in any format. Easy way to parse a date in any format and in any language, while always respecting ISO and RFC formats. +version: 1.0.2 repository: https://github.com/gbassisp/any_date homepage: https://github.com/gbassisp/any_date From 9aa771b9d7b07a3f0c9b889f11d2661d9d1c5600 Mon Sep 17 00:00:00 2001 From: guilherme Date: Tue, 16 Apr 2024 15:09:47 +0930 Subject: [PATCH 21/30] chore: added entry on changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a307999..ce4d925 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ +## 1.0.2 + +- Minor change to package description + ## 1.0.1 - Added support for `intl` 0.19 From 632827e2019de17ea15060f7568e8caca1c5303d Mon Sep 17 00:00:00 2001 From: gbassisp Date: Sat, 20 Apr 2024 16:48:14 +0930 Subject: [PATCH 22/30] chore: bump patch number --- CHANGELOG.md | 5 +++++ pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa0ddb5..90244f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ +## 1.0.3 + +- Merged in `0.1.14`: + Added support for non-sense formats; at the moment only `yyyyMMdd'T'hhmmss` with no separator, including time variants + ## 1.0.2 - Minor change to package description diff --git a/pubspec.yaml b/pubspec.yaml index 8420ccd..9c1a792 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: any_date description: A package for parsing String into DateTime in any format. Easy way to parse a date in any format and in any language, while always respecting ISO and RFC formats. -version: 1.0.2 +version: 1.0.3 repository: https://github.com/gbassisp/any_date homepage: https://github.com/gbassisp/any_date From b6e0aa1cd057a4aea0e240a23ef4699a01aecab1 Mon Sep 17 00:00:00 2001 From: guilherme Date: Thu, 9 May 2024 22:46:53 +0930 Subject: [PATCH 23/30] fix: merge conflicts --- lib/src/any_date_base.dart | 4 ++-- test/time_zone_test.dart | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/any_date_base.dart b/lib/src/any_date_base.dart index bc3a9b8..fac383a 100644 --- a/lib/src/any_date_base.dart +++ b/lib/src/any_date_base.dart @@ -152,8 +152,8 @@ class DateParserInfo { }) : dayFirst = dayFirst ?? false, yearFirst = yearFirst ?? false, allowedSeparators = allowedSeparators ?? _defaultSeparators, - months = months ?? _allMonths, - weekdays = weekdays ?? _allWeekdays, + months = months ?? allMonths, + weekdays = weekdays ?? allWeekdays, customRules = customRules ?? const []; /// interpret the first value in an ambiguous case (e.g. 01/01/01) diff --git a/test/time_zone_test.dart b/test/time_zone_test.dart index 58b9d0c..51390b1 100644 --- a/test/time_zone_test.dart +++ b/test/time_zone_test.dart @@ -5,7 +5,8 @@ import 'package:test/test.dart'; import 'test_values.dart'; -void main() { +Future main() async { + await initializeDateFormatting(); const parser = AnyDate(); const utcSymbol = 'Z'; group('utc with iso format', () { From 0f5726d034a9e41152369d4fbc3f73af1d85a718 Mon Sep 17 00:00:00 2001 From: guilherme Date: Wed, 15 May 2024 22:12:30 +0930 Subject: [PATCH 24/30] test: added case for datetime extensions --- test/extensions_test.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 test/extensions_test.dart diff --git a/test/extensions_test.dart b/test/extensions_test.dart new file mode 100644 index 0000000..7408716 --- /dev/null +++ b/test/extensions_test.dart @@ -0,0 +1,16 @@ +import 'package:any_date/src/extensions.dart'; +import 'package:test/test.dart'; + +void main() { + group('DateTime extensions', () { + test('safeCopyWith - invalid', () { + final date = DateTime(2000); + + expect( + () => date.safeCopyWith(microsecond: 1234), + throwsA(isA()), + reason: 'the new microseconds would result in a change in milliseconds', + ); + }); + }); +} From 3fa6e5e5399d9a2e0ac437999c81db00e79ce1df Mon Sep 17 00:00:00 2001 From: guilherme Date: Wed, 15 May 2024 22:13:40 +0930 Subject: [PATCH 25/30] refactor: call tryToDateTime in utc test --- test/any_date_time_test.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/any_date_time_test.dart b/test/any_date_time_test.dart index 014cc95..01e7af6 100644 --- a/test/any_date_time_test.dart +++ b/test/any_date_time_test.dart @@ -89,16 +89,16 @@ void main() { millisecond: 13, ); s = d.toString(); - expect(s.toDateTime(), d); + expect(s.tryToDateTime(), d); s = d.toString(); - expect(s.toDateTime(utc: true), d.toUtc()); + expect(s.tryToDateTime(utc: true), d.toUtc()); s = d.toIso8601String(); - expect(s.toDateTime(), d); + expect(s.tryToDateTime(), d); s = d.toLocal().toString(); - expect(s.toDateTime(), d); + expect(s.tryToDateTime(), d); s = d.toUtc().toString(); - expect(s.toDateTime().isUtc, true); - expect(s.toDateTime(), d.toUtc()); + expect(s.tryToDateTime()?.isUtc, isTrue); + expect(s.tryToDateTime(), d.toUtc()); } }); }); From 313b0645a3ac00ef76536b6ea7e673d8381d1894 Mon Sep 17 00:00:00 2001 From: guilherme Date: Sat, 31 Aug 2024 15:11:05 +0930 Subject: [PATCH 26/30] test: allow 'sep' and 'sept' short version of september --- test/locale_based_test.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/locale_based_test.dart b/test/locale_based_test.dart index 2419f4e..f1053b6 100644 --- a/test/locale_based_test.dart +++ b/test/locale_based_test.dart @@ -94,13 +94,11 @@ Future main() async { final englishMonths = AnyDate.defaultSettings.months; final englishWeekdays = AnyDate.defaultSettings.weekdays; final longWeekdays = englishWeekdays.sublist(0, 7); - final shortWeekdays = englishWeekdays.sublist(7) - ..removeWhere((element) => element.name == 'Sept'); + final shortWeekdays = englishWeekdays.sublist(7); test('english speaking - american format', () { final locale = Locale.fromSubtags(languageCode: 'en', countryCode: 'US'); final longMonths = englishMonths.sublist(0, 12); - final shortMonths = englishMonths.sublist(12) - ..removeWhere((element) => element.name == 'Sept'); + final shortMonths = englishMonths.sublist(12); expect(locale.usesMonthFirst, isTrue); expect(locale.usesYearFirst, isFalse); @@ -113,8 +111,7 @@ Future main() async { test('english speaking - normal format', () { final locale = Locale.fromSubtags(languageCode: 'en', countryCode: 'AU'); final longMonths = englishMonths.sublist(0, 12); - final shortMonths = englishMonths.sublist(12) - ..removeWhere((element) => element.name == 'Sep'); + final shortMonths = englishMonths.sublist(12); expect(locale.usesMonthFirst, isFalse); expect(locale.usesYearFirst, isFalse); From 1f618787e6539f8da5cc659d8db32bd9fde22d61 Mon Sep 17 00:00:00 2001 From: guilherme Date: Sat, 31 Aug 2024 16:58:57 +0930 Subject: [PATCH 27/30] test: added case for failing japanese format --- test/locale_based_test.dart | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/locale_based_test.dart b/test/locale_based_test.dart index f1053b6..eae0440 100644 --- a/test/locale_based_test.dart +++ b/test/locale_based_test.dart @@ -123,6 +123,26 @@ Future main() async { }); group('locale specific cases', () { + // this is to ensure 日 is not mis-interpreted between day of the week and + // day of the month + test( + '2024年8月31日 on ja with format y年M月d日 resulted in null, ' + 'but expected 2024-08-31 00:00:00.000', () { + const locale = 'ja'; + const formatted = '2024年8月31日'; + const format = 'y年M月d日'; + final formatter = DateFormat(format); + final expected = DateTime.parse('2024-08-31 00:00:00.000'); + final parser = AnyDate.fromLocale(locale); + + expect( + formatter.parseLoose(formatted), + equals(expected), + reason: 'sanity check that DateFormat $format can parse $formatted', + ); + expect(parser.tryParse(formatted), equals(expected)); + }); + const unambiguousEnglish = { 'March 27, 2024', 'March 27 2024', From fbdb08c1fd2649bb7fbc57d8a3a9e4586568bad4 Mon Sep 17 00:00:00 2001 From: guilherme Date: Sat, 31 Aug 2024 16:59:46 +0930 Subject: [PATCH 28/30] fix: issue with day of the month and weekday being mixed up --- lib/src/param_cleanup_rules.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/src/param_cleanup_rules.dart b/lib/src/param_cleanup_rules.dart index 4f0fe51..9d52a48 100644 --- a/lib/src/param_cleanup_rules.dart +++ b/lib/src/param_cleanup_rules.dart @@ -133,13 +133,15 @@ Weekday? _expectWeekday(DateParsingParameters parameters) { var weekday = parameters.parserInfo.weekdays .where( (element) => timestamp.startsWith(element.name.toLowerCase()), + // (element) => timestamp + // .contains(RegExp('\\D${element.name}', caseSensitive: false)), ) .firstOrNullExtension; if (weekday != null) return weekday; - weekday = parameters.parserInfo.weekdays - .where((element) => timestamp.endsWith(element.name.toLowerCase())) - .firstOrNullExtension; - if (weekday != null) return weekday; +// weekday = parameters.parserInfo.weekdays +// .where((element) => timestamp.endsWith(element.name.toLowerCase())) +// .firstOrNullExtension; +// if (weekday != null) return weekday; // english weekday = allWeekdays From d4e0062993672ab6dfa6c58c30548df921788e24 Mon Sep 17 00:00:00 2001 From: guilherme Date: Sat, 31 Aug 2024 17:14:12 +0930 Subject: [PATCH 29/30] fix(test): deprecation warning .whereNotNull() --- test/locale_based_test.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/locale_based_test.dart b/test/locale_based_test.dart index 2419f4e..fc886e7 100644 --- a/test/locale_based_test.dart +++ b/test/locale_based_test.dart @@ -5,7 +5,6 @@ import 'package:intl/date_symbol_data_file.dart' import 'package:intl/intl.dart'; import 'package:intl/locale.dart'; -import 'package:lean_extensions/collection_extensions.dart'; import 'package:test/test.dart'; import 'rfc_test.dart'; @@ -32,7 +31,17 @@ final _locales = availableLocalesForDateFormatting.map((e) => e).toList() } return false; }); -final _localeCodes = _locales.map(Locale.tryParse).whereNotNull().toList(); +final _localeCodes = _locales.map(Locale.tryParse).whereIsNotNull().toList(); + +/// taken from collection package to avoid deprecation warning and conflict +/// with dart sdk +extension _IterableNullableExtension on Iterable { + Iterable whereIsNotNull() sync* { + for (final element in this) { + if (element != null) yield element; + } + } +} Future main() async { await initializeDateFormatting(); From cb0e011be56b4558ad94eea3d0eac57d90d9c9fd Mon Sep 17 00:00:00 2001 From: guilherme Date: Sat, 31 Aug 2024 17:47:11 +0930 Subject: [PATCH 30/30] chore: bump minor version --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90244f9..6960e4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ +## 1.0.4 + +- Added support for git non-sense format, e.g., `Thu May 16 10:18:07 2024 +0930` and `Thu May 16 10:18:07pm 2024 +0930` + ## 1.0.3 - Merged in `0.1.14`: diff --git a/pubspec.yaml b/pubspec.yaml index 9c1a792..94f5fa7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: any_date description: A package for parsing String into DateTime in any format. Easy way to parse a date in any format and in any language, while always respecting ISO and RFC formats. -version: 1.0.3 +version: 1.0.4 repository: https://github.com/gbassisp/any_date homepage: https://github.com/gbassisp/any_date