Skip to content

Commit

Permalink
Merge pull request #2 from gbassisp/feature/locale_aware_rules
Browse files Browse the repository at this point in the history
Feature/locale aware rules
  • Loading branch information
gbassisp authored Apr 3, 2024
2 parents 143cd69 + 0362102 commit af78ae0
Show file tree
Hide file tree
Showing 14 changed files with 635 additions and 320 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ analyze:
fix:
$(DART_CMD) format .
$(DART_CMD) fix --apply
$(DART_CMD) format .

.PHONY: version
version:
Expand Down
55 changes: 53 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand All @@ -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 💙
2 changes: 2 additions & 0 deletions lib/any_date.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
library any_date;

export 'src/any_date_base.dart' show AnyDate, DateParserInfo, Month, Weekday;

export 'src/any_date_rules_model.dart' show DateParsingFunction;
135 changes: 82 additions & 53 deletions lib/src/any_date_base.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
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
@internal
class DateParsingParameters {
/// default constructor
const DateParsingParameters({
Expand Down Expand Up @@ -52,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
Expand All @@ -81,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
Expand Down Expand Up @@ -187,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) {
Expand Down Expand Up @@ -224,23 +217,18 @@ String _trimSeparators(String formattedString, Iterable<String> 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,
});
bool? dayFirst,
bool? yearFirst,
List<String>? allowedSeparators,
List<Month>? months,
List<Weekday>? weekdays,
Iterable<DateParsingFunction>? 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.
Expand All @@ -265,28 +253,33 @@ class DateParserInfo {
/// keywords to identify weekdays (to support multiple languages)
final List<Weekday> weekdays;

/// allow passing extra rules to parse the timestamp
final Iterable<DateParsingFunction> customRules;

/// copy with
DateParserInfo copyWith({
bool? dayFirst,
bool? yearFirst,
List<String>? allowedSeparators,
List<Month>? months,
List<Weekday>? weekdays,
Iterable<String>? allowedSeparators,
Iterable<Month>? months,
Iterable<Weekday>? weekdays,
Iterable<DateParsingFunction>? 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,
);
}

@override
String toString() {
return 'DateParserInfo(dayFirst: $dayFirst, yearFirst: $yearFirst, '
'allowedSeparators: $allowedSeparators, months: $months, '
'weekdays: $weekdays)';
'weekdays: $weekdays, customRules: $customRules)';
}
}

Expand All @@ -295,6 +288,24 @@ class AnyDate {
/// default constructor
const AnyDate({DateParserInfo? info}) : _info = info;

/// factory constructor to create an [AnyDate] obj based on [locale]
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
// final DateParserInfo? info;

Expand All @@ -309,11 +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
String formattedString,
Object? timestamp,
) {
final res = _tryParse(formattedString);
DateTime? res;
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;
}
Expand All @@ -323,20 +337,21 @@ class AnyDate {
/// Returns null if the string is not a valid date.
///
/// Does not handle other exceptions.
DateTime? tryParse(String formattedString) {
DateTime? tryParse(Object? timestamp) {
try {
return parse(formattedString);
return parse(timestamp);
} on FormatException {
return null;
}
}

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,
Expand Down Expand Up @@ -367,7 +382,8 @@ class AnyDate {
);

yield rfcRules.apply(p);
// return;
// custom rules are only applied after rfc rules
yield MultipleRules.fromFunctions(i.customRules).apply(p);

yield ambiguousCase.apply(p);
yield MultipleRules(i.dayFirst ? _yearLastDayFirst : _yearLast).apply(p);
Expand Down Expand Up @@ -459,3 +475,16 @@ const _shortWeekdays = [
];

const _allWeekdays = [..._weekdays, ..._shortWeekdays];

// TODO(gbassisp): avoid messing up regex with special chars
const _defaultSeparators = [
' ',
't',
'T',
':',
'.',
',',
'_',
'/',
'-',
];
Loading

0 comments on commit af78ae0

Please sign in to comment.