Skip to content

Commit

Permalink
Merge branch 'release/1.x' into release/0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
gbassisp authored Dec 15, 2024
2 parents 1ec7c5d + 9ff66dc commit c8d1bb4
Show file tree
Hide file tree
Showing 14 changed files with 526 additions and 96 deletions.
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,35 @@

## 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`:
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

## 1.0.1

- Added support for `intl` 0.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.16

- Fix conflicts with version 1.x
Expand Down
61 changes: 48 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,19 @@ There are no new classes to represent `DateTime`. Don't reinvent the wheel, just

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 @@ -35,6 +47,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 @@ -58,19 +90,22 @@ 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)
```


[dart_install_link]: https://dart.dev/get-dart
[github_actions_link]: https://docs.github.com/en/actions/learn-github-actions
[license_badge]: https://img.shields.io/badge/license-BSD3-blue.svg
[license_link]: https://opensource.org/licenses/BSD-3
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis
[very_good_coverage_link]: https://github.com/marketplace/actions/very-good-coverage
[very_good_ventures_link]: https://verygood.ventures
[very_good_ventures_link_light]: https://verygood.ventures#gh-light-mode-only
[very_good_ventures_link_dark]: https://verygood.ventures#gh-dark-mode-only
[very_good_workflows_link]: https://github.com/VeryGoodOpenSource/very_good_workflows
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;
129 changes: 79 additions & 50 deletions lib/src/any_date_base.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
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:any_date/src/nonsense_formats.dart';
import 'package:any_date/src/param_cleanup_rules.dart';
import 'package:intl/locale.dart';
import 'package:meta/meta.dart';

/// Parameters passed to the parser
Expand Down Expand Up @@ -84,12 +86,11 @@ 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 @@ -113,21 +114,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 All @@ -146,23 +143,18 @@ class Weekday {
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 @@ -187,28 +179,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 @@ -217,6 +214,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 @@ -231,16 +246,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;
}
Expand All @@ -250,20 +263,21 @@ 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;
}
}

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 All @@ -290,6 +304,8 @@ DateParsingRule _entryPoint(DateParserInfo i) {
basicSetup,
rfcRules,
cleanupRules,
// custom rules are only applied after rfc rules
MultipleRules.fromFunctions(i.customRules),
nonsenseRules,
ambiguousCase,
MultipleRules(i.dayFirst ? _yearLastDayFirst : _yearLast),
Expand Down Expand Up @@ -383,3 +399,16 @@ const _shortWeekdays = [
/// internal base values for all weekdays in english
@internal
const allWeekdays = [..._weekdays, ..._shortWeekdays];

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

0 comments on commit c8d1bb4

Please sign in to comment.