├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── analysis_options.yaml
├── example
└── main.dart
├── lib
├── chrono_dart.dart
└── src
│ ├── calculation
│ ├── mergingCalculation.dart
│ └── years.dart
│ ├── chrono.dart
│ ├── common
│ ├── abstract_refiners.dart
│ ├── calculation
│ │ └── weekdays.dart
│ ├── casual_references.dart
│ ├── parsers
│ │ ├── AbstractParserWithWordBoundary.dart
│ │ ├── AbstractTimeExpressionParser.dart
│ │ ├── ISOFormatParser.dart
│ │ └── SlashDateFormatParser.dart
│ └── refiners
│ │ ├── AbstractMergeDateRangeRefiner.dart
│ │ ├── AbstractMergeDateTimeRefiner.dart
│ │ ├── ExtractTimezoneAbbrRefiner.dart
│ │ ├── ExtractTimezoneOffsetRefiner.dart
│ │ ├── ForwardDateRefiner.dart
│ │ ├── MergeWeekdayComponentRefiner.dart
│ │ ├── OverlapRemovalRefiner.dart
│ │ └── UnlikelyFormatFilter.dart
│ ├── configurations.dart
│ ├── debugging.dart
│ ├── locales
│ └── en
│ │ ├── configuration.dart
│ │ ├── constants.dart
│ │ ├── en.dart
│ │ ├── parsers
│ │ ├── ENCasualDateParser.dart
│ │ ├── ENCasualTimeParser.dart
│ │ ├── ENCasualYearMonthDayParser.dart
│ │ ├── ENMonthNameLittleEndianParser.dart
│ │ ├── ENMonthNameMiddleEndianParser.dart
│ │ ├── ENMonthNameParser.dart
│ │ ├── ENRelativeDateFormatParser.dart
│ │ ├── ENSlashMonthFormatParser.dart
│ │ ├── ENTimeExpressionParser.dart
│ │ ├── ENTimeUnitAgoFormatParser.dart
│ │ ├── ENTimeUnitCasualRelativeFormatParser.dart
│ │ ├── ENTimeUnitLaterFormatParser.dart
│ │ ├── ENTimeUnitWithinFormatParser.dart
│ │ └── ENWeekdayParser.dart
│ │ └── refiners
│ │ ├── ENMergeDateRangeRefiner.dart
│ │ ├── ENMergeDateTimeRefiner.dart
│ │ └── ENMergeRelativeDateRefiner.dart
│ ├── results.dart
│ ├── timezone.dart
│ ├── types.dart
│ └── utils
│ ├── day.dart
│ ├── pattern.dart
│ └── timeunits.dart
├── pubspec.lock
├── pubspec.yaml
└── test
├── calculation_test.dart
├── debugging_test.dart
├── result_test.dart
├── system_test.dart
├── system_timezone_test.dart
└── test_util.dart
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 |
12 | # IntelliJ related
13 | *.iml
14 | *.ipr
15 | *.iws
16 | .idea/
17 |
18 | .vscode/
19 |
20 | # Flutter/Dart/Pub related
21 | **/doc/api/
22 | **/ios/Flutter/.last_build_id
23 | .dart_tool/
24 | .flutter-plugins
25 | .flutter-plugins-dependencies
26 | .packages
27 | .pub-cache/
28 | .pub/
29 | /build/
30 |
31 | # Symbolication related
32 | app.*.symbols
33 |
34 | # Obfuscation related
35 | app.*.map.json
36 |
37 | # Android Studio will place build artifacts here
38 | /android/app/debug
39 | /android/app/profile
40 | /android/app/release
41 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 | –
10 |
11 | ## [2.0.2] - 2024-08-13
12 | Null-safety crash fix (incoming PR).
13 |
14 |
15 | ## [2.0.1] - 2023-10-18
16 | Prevent an additional month from being added in certain cases.
17 |
18 |
19 | ## [2.0.0] - 2023-10-14
20 |
21 | • Breaking change. Refactoring of the main methods – exposed `Chrono` as an abstract public class with the main methods the package provides.
22 | • Added an example.
23 | • Minor changes.
24 |
25 |
26 | ## [1.0.0] - 2023-10-06
27 |
28 | Release
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Original package copyright (c) 2014, Wanasit Tanakitrungruang (github.com/wanasit/chrono) - The MIT License
4 |
5 | Copyright (c) 2023, github.com/g-30
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in
15 | all copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | THE SOFTWARE.
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chrono - Date parser for Dart (Flutter)
2 |
3 | [](https://pub.dev/packages/chrono_dart)
4 | [](https://opensource.org/licenses/MIT)
5 |
6 | A natural language date parser in Dart. Finds and extracts dates from user-generated text content.
7 |
8 | Example use case – WhatsApp-like date parsing in dialogues:
9 |
10 |
11 |
12 | It is designed to handle most date/time formats and extract information from any given text:
13 |
14 | * Today, Tomorrow, Yesterday, Last Friday, etc
15 | * 17 August 2013 - 19 August 2013
16 | * This Friday from 13:00 - 16.00
17 | * 5 days ago
18 | * 2 weeks from now
19 | * Sat Aug 17 2013 18:40:39 GMT+0900 (JST)
20 | * 2014-11-30T08:15:30-05:30
21 |
22 | # Usage
23 | 1. Install manually or via pub - `dart pub add chrono_dart`
24 | 2. Simply pass a string to functions Chrono.parseDate or Chrono.parse.
25 |
26 | ```dart
27 | import 'package:chrono_dart/chrono_dart.dart' show Chrono;
28 |
29 | Chrono.parseDate('An appointment on Sep 12');
30 | // DateTime('2023-09-12 12:00:00.000Z')
31 |
32 | Chrono.parse('An appointment on Sep 12');
33 | /* [{
34 | index: 18,
35 | text: 'Sep 12',
36 | date() => DateTime('2023-09-12T12:00:00'),
37 | ...
38 | }] */
39 | ```
40 |
41 | Only English is supported in this version. Feel free to add PRs with any other languages – the package is designed with extendability in mind.
42 |
43 | -----------
44 | Port of Chrono to Dart lang.
45 | For extended API information see [the original package](https://github.com/wanasit/chrono/).
46 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the static analysis results for your project (errors,
2 | # warnings, and lints).
3 | #
4 | # This enables the 'recommended' set of lints from `package:lints`.
5 | # This set helps identify many issues that may lead to problems when running
6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic
7 | # style and format.
8 | #
9 | # If you want a smaller set of lints you can change this to specify
10 | # 'package:lints/core.yaml'. These are just the most critical lints
11 | # (the recommended set includes the core lints).
12 | # The core lints are also what is used by pub.dev for scoring packages.
13 |
14 | include: package:lints/recommended.yaml
15 |
16 | # Uncomment the following section to specify additional rules.
17 |
18 | linter:
19 | rules:
20 | camel_case_types: false
21 | prefer_adjacent_string_concatenation: false
22 | constant_identifier_names: false
23 | non_constant_identifier_names: false
24 | file_names: false
25 |
26 |
27 | # analyzer:
28 | # exclude:
29 | # - path/to/excluded/files/**
30 |
31 | # For more information about the core and recommended set of lints, see
32 | # https://dart.dev/go/core-lints
33 |
34 | # For additional information about configuring this file, see
35 | # https://dart.dev/guides/language/analysis-options
36 |
--------------------------------------------------------------------------------
/example/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:chrono_dart/chrono_dart.dart' show Chrono;
2 |
3 | void main() {
4 | final dateOrNull = Chrono.parseDate('An appointment on Sep 12');
5 | print('Found date: $dateOrNull');
6 |
7 | final results = Chrono.parse('An appointment on Sep 12');
8 | print('Found dates: $results');
9 | }
--------------------------------------------------------------------------------
/lib/chrono_dart.dart:
--------------------------------------------------------------------------------
1 | /// Date parser for user-generated text. https://github.com/g-30/chrono_dart
2 | library;
3 |
4 | import './src/chrono.dart' show ChronoInstance;
5 | import './src/locales/en/en.dart' as en;
6 | import './src/types.dart' show ParsedResult, ParsingOption, ParsingReference;
7 |
8 | export './src/chrono.dart';
9 | export './src/types.dart';
10 | export './src/results.dart';
11 |
12 | abstract class Chrono {
13 | /// A shortcut for {@link en | chrono.en.strict}
14 | static final strict = en.strict;
15 |
16 | /// A shortcut for {@link en | chrono.en.casual}
17 | static final casual = en.casual;
18 |
19 | /// Default instance with en.casual config.
20 | static final instance = ChronoInstance();
21 |
22 | /// A shortcut for {@link en | chrono.en.casual.parse()}
23 | static List parse(String text,
24 | {dynamic ref, ParsingOption? option}) {
25 | assert(ref == null || ref is ParsingReference || ref is DateTime,
26 | 'ref must be either null, DateTime or ParsingReference');
27 | return casual.parse(text, ref, option);
28 | }
29 |
30 | /// A shortcut for {@link en | chrono.en.casual.parseDate()}
31 | static DateTime? parseDate(String text,
32 | {dynamic ref, ParsingOption? option}) {
33 | assert(ref == null || ref is ParsingReference || ref is DateTime,
34 | 'ref must be either null, DateTime or ParsingReference');
35 | return casual.parseDate(text, ref, option);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/src/calculation/mergingCalculation.dart:
--------------------------------------------------------------------------------
1 | import '../types.dart' show Component, Meridiem;
2 | import '../results.dart' show ParsingComponents, ParsingResult;
3 | import '../utils/day.dart' show assignSimilarDate, implySimilarDate;
4 |
5 | ParsingResult mergeDateTimeResult(
6 | ParsingResult dateResult, ParsingResult timeResult) {
7 | final result = dateResult.clone();
8 | final beginDate = dateResult.start;
9 | final beginTime = timeResult.start;
10 |
11 | result.start = mergeDateTimeComponent(beginDate, beginTime);
12 | if (dateResult.end != null || timeResult.end != null) {
13 | final endDate = dateResult.end ?? dateResult.start;
14 | final endTime = timeResult.end ?? timeResult.start;
15 | final endDateTime = mergeDateTimeComponent(endDate, endTime);
16 |
17 | if (dateResult.end == null &&
18 | endDateTime.date().millisecondsSinceEpoch < result.start.date().millisecondsSinceEpoch) {
19 | // For example, "Tuesday 9pm - 1am" the ending should actually be 1am on the next day.
20 | // We need to add to ending by another day.
21 | final nextDayJs = endDateTime.dayjs().add(1, 'd')!;
22 | if (endDateTime.isCertain(Component.day)) {
23 | assignSimilarDate(endDateTime, nextDayJs);
24 | } else {
25 | implySimilarDate(endDateTime, nextDayJs);
26 | }
27 | }
28 |
29 | result.end = endDateTime;
30 | }
31 |
32 | return result;
33 | }
34 |
35 | ParsingComponents mergeDateTimeComponent(
36 | ParsingComponents dateComponent, ParsingComponents timeComponent) {
37 | final dateTimeComponent = dateComponent.clone();
38 |
39 | if (timeComponent.isCertain(Component.hour)) {
40 | dateTimeComponent.assign(
41 | Component.hour, timeComponent.get(Component.hour)!);
42 | dateTimeComponent.assign(
43 | Component.minute, timeComponent.get(Component.minute)!);
44 |
45 | if (timeComponent.isCertain(Component.second)) {
46 | dateTimeComponent.assign(
47 | Component.second, timeComponent.get(Component.second)!);
48 |
49 | if (timeComponent.isCertain(Component.millisecond)) {
50 | dateTimeComponent.assign(
51 | Component.millisecond, timeComponent.get(Component.millisecond)!);
52 | } else {
53 | dateTimeComponent.imply(
54 | Component.millisecond, timeComponent.get(Component.millisecond)!);
55 | }
56 | } else {
57 | dateTimeComponent.imply(
58 | Component.second, timeComponent.get(Component.second)!);
59 | dateTimeComponent.imply(
60 | Component.millisecond, timeComponent.get(Component.millisecond)!);
61 | }
62 | } else {
63 | dateTimeComponent.imply(Component.hour, timeComponent.get(Component.hour)!);
64 | dateTimeComponent.imply(
65 | Component.minute, timeComponent.get(Component.minute)!);
66 | dateTimeComponent.imply(
67 | Component.second, timeComponent.get(Component.second)!);
68 | dateTimeComponent.imply(
69 | Component.millisecond, timeComponent.get(Component.millisecond)!);
70 | }
71 |
72 | if (timeComponent.isCertain(Component.timezoneOffset)) {
73 | dateTimeComponent.assign(
74 | Component.timezoneOffset, timeComponent.get(Component.timezoneOffset)!);
75 | }
76 |
77 | if (timeComponent.isCertain(Component.meridiem)) {
78 | dateTimeComponent.assign(
79 | Component.meridiem, timeComponent.get(Component.meridiem)!);
80 | } else if (timeComponent.get(Component.meridiem) != null &&
81 | dateTimeComponent.get(Component.meridiem) == null) {
82 | dateTimeComponent.imply(
83 | Component.meridiem, timeComponent.get(Component.meridiem)!);
84 | }
85 |
86 | if (dateTimeComponent.get(Component.meridiem) == Meridiem.PM.id &&
87 | dateTimeComponent.get(Component.hour)! < 12) {
88 | if (timeComponent.isCertain(Component.hour)) {
89 | dateTimeComponent.assign(
90 | Component.hour, dateTimeComponent.get(Component.hour)! + 12);
91 | } else {
92 | dateTimeComponent.imply(
93 | Component.hour, dateTimeComponent.get(Component.hour)! + 12);
94 | }
95 | }
96 |
97 | dateTimeComponent.addTags(dateComponent.tags());
98 | dateTimeComponent.addTags(timeComponent.tags());
99 | return dateTimeComponent;
100 | }
101 |
--------------------------------------------------------------------------------
/lib/src/calculation/years.dart:
--------------------------------------------------------------------------------
1 | import 'package:day/day.dart' as dayjs;
2 |
3 | /// Find the most likely year, from a raw number. For example:
4 | /// 1997 => 1997
5 | /// 97 => 1997
6 | /// 12 => 2012
7 | int findMostLikelyADYear(int yearNumber) {
8 | if (yearNumber < 100) {
9 | if (yearNumber > 50) {
10 | yearNumber = yearNumber + 1900;
11 | } else {
12 | yearNumber = yearNumber + 2000;
13 | }
14 | }
15 |
16 | return yearNumber;
17 | }
18 |
19 | int findYearClosestToRef(DateTime refDate, int day, int month) {
20 | //Find the most appropriated year
21 | final refMoment = dayjs.Day.fromDateTime(refDate);
22 | var dateMoment = refMoment;
23 | dateMoment = dateMoment.month(month - 1);
24 | dateMoment = dateMoment.date(day);
25 | dateMoment = dateMoment.year(refMoment.year());
26 |
27 | final nextYear = dateMoment.add(1, "y")!;
28 | final lastYear = dateMoment.add(-1, "y")!;
29 | if (nextYear.diff(refMoment).abs() < dateMoment.diff(refMoment).abs()) {
30 | dateMoment = nextYear;
31 | } else if (lastYear.diff(refMoment).abs() <
32 | dateMoment.diff(refMoment).abs()) {
33 | dateMoment = lastYear;
34 | }
35 |
36 | return dateMoment.year();
37 | }
38 |
--------------------------------------------------------------------------------
/lib/src/chrono.dart:
--------------------------------------------------------------------------------
1 | import './results.dart'
2 | show ReferenceWithTimezone, ParsingComponents, ParsingResult;
3 | import './types.dart'
4 | show
5 | Component,
6 | ParsedResult,
7 | ParsingOption,
8 | ParsingReference,
9 | RegExpChronoMatch;
10 | import './debugging.dart' show AsyncDebugBlock, DebugHandler;
11 | import './locales/en/configuration.dart';
12 |
13 | /// Chrono configuration.
14 | /// It is simply an ordered list of parsers and refiners
15 | class Configuration {
16 | final List parsers;
17 | final List refiners;
18 |
19 | Configuration({required this.parsers, required this.refiners});
20 | }
21 |
22 | /// An abstraction for Chrono *Parser*.
23 | ///
24 | /// Each parser should recognize and handle a certain date format.
25 | /// Chrono uses multiple parses (and refiners) together for parsing the input.
26 | ///
27 | /// The parser implementation must provide {@Link pattern | pattern()} for the date format.
28 | ///
29 | /// The {@Link extract | extract()} method is called with the pattern's *match*.
30 | /// The matching and extracting is controlled and adjusted to avoid for overlapping results.
31 | abstract class Parser {
32 | RegExp pattern(ParsingContext context);
33 |
34 | /// @returns ParsingComponents | ParsingResult | { [c in Component]?: number } | null
35 | dynamic extract(ParsingContext context, RegExpChronoMatch match);
36 | }
37 |
38 | /// A abstraction for Chrono *Refiner*.
39 | ///
40 | /// Each refiner takes the list of results (from parsers or other refiners) and returns another list of results.
41 | /// Chrono applies each refiner in order and return the output from the last refiner.
42 | abstract class Refiner {
43 | List refine(
44 | ParsingContext context, List results);
45 | }
46 |
47 | /// The Chrono object.
48 | class ChronoInstance {
49 | List parsers;
50 | List refiners;
51 |
52 | static const defaultConfig = ENDefaultConfiguration();
53 |
54 | ChronoInstance([Configuration? configuration])
55 | : parsers = (configuration ?? defaultConfig.createCasualConfiguration())
56 | .parsers,
57 | refiners = (configuration ?? defaultConfig.createCasualConfiguration())
58 | .refiners;
59 |
60 | /// Create a shallow copy of the Chrono object with the same configuration (`parsers` and `refiners`)
61 | ChronoInstance clone() {
62 | return ChronoInstance(Configuration(
63 | parsers: [...parsers],
64 | refiners: [...refiners],
65 | ));
66 | }
67 |
68 | /// A shortcut for calling {@Link parse | parse() } then transform the result into Dart's DateTime object
69 | /// @return DateTime object created from the first parse result
70 | DateTime? parseDate(String text,
71 | [dynamic referenceDate, ParsingOption? option]) {
72 | assert(referenceDate == null ||
73 | referenceDate is ParsingReference ||
74 | referenceDate is DateTime);
75 | final results = parse(text, referenceDate, option);
76 | return results.isNotEmpty ? results[0].start.date() : null;
77 | }
78 |
79 | List parse(String text,
80 | [dynamic referenceDate, ParsingOption? option]) {
81 | assert(referenceDate == null ||
82 | referenceDate is ParsingReference ||
83 | referenceDate is DateTime);
84 |
85 | final context = ParsingContext(text, referenceDate, option);
86 |
87 | List results = [];
88 | for (final parser in parsers) {
89 | final parsedResults = ChronoInstance._executeParser(context, parser);
90 | results = [...results, ...parsedResults];
91 | }
92 |
93 | results.sort((a, b) => a.index - b.index);
94 |
95 | for (final refiner in refiners) {
96 | results = refiner.refine(context, results);
97 | }
98 |
99 | return results;
100 | }
101 |
102 | static List _executeParser(
103 | ParsingContext context, Parser parser) {
104 | final List results = [];
105 | final pattern = parser.pattern(context);
106 |
107 | final originalText = context.text;
108 | var remainingText = context.text;
109 | var match =
110 | RegExpChronoMatch.matchOrNull(pattern.firstMatch(remainingText));
111 |
112 | while (match != null) {
113 | // Calculate match index on the full text;
114 | final index = match.index + originalText.length - remainingText.length;
115 | match.index = index;
116 |
117 | final result = parser.extract(context, match);
118 | if (result == null) {
119 | // If fails, move on by 1
120 | remainingText = match.index + 1 < originalText.length
121 | ? originalText.substring(match.index + 1)
122 | : '';
123 | match =
124 | RegExpChronoMatch.matchOrNull(pattern.firstMatch(remainingText));
125 | continue;
126 | }
127 |
128 | ParsingResult parsedResult;
129 | if (result is ParsingResult) {
130 | parsedResult = result;
131 | } else if (result is ParsingComponents) {
132 | parsedResult = context.createParsingResult(match.index, match[0]);
133 | parsedResult.start = result;
134 | } else {
135 | parsedResult =
136 | context.createParsingResult(match.index, match[0], result);
137 | }
138 |
139 | final parsedIndex = parsedResult.index;
140 | final parsedText = parsedResult.text;
141 | context.debug(() {
142 | print(
143 | "${parser.runtimeType} extracted (at index=$parsedIndex) '$parsedText'");
144 | });
145 |
146 | results.add(parsedResult);
147 | remainingText = originalText.substring(parsedIndex + parsedText.length);
148 | match = RegExpChronoMatch.matchOrNull(pattern.firstMatch(remainingText));
149 | }
150 |
151 | return results;
152 | }
153 | }
154 |
155 | class ParsingContext implements DebugHandler {
156 | final String text;
157 | final ParsingOption option;
158 | final ReferenceWithTimezone reference;
159 |
160 | @Deprecated('Use `reference.instant` instead.')
161 | late DateTime refDate;
162 |
163 | ParsingContext(this.text, dynamic irefDate, ParsingOption? option)
164 | : assert(irefDate == null ||
165 | irefDate is ParsingReference ||
166 | irefDate is DateTime),
167 | option = option ?? ParsingOption(),
168 | reference = ReferenceWithTimezone(irefDate) {
169 | // ignore: deprecated_member_use_from_same_package
170 | refDate = reference.instant;
171 | }
172 |
173 | ParsingComponents createParsingComponents([dynamic components]) {
174 | assert(components == null ||
175 | components is ParsingComponents ||
176 | components is Map);
177 | if (components is ParsingComponents) {
178 | return components;
179 | }
180 |
181 | /// TODO: WARNING: forcing double to int; needs research.
182 | final cmps = components == null
183 | ? null
184 | : Map.from(components)
185 | .map((key, val) => MapEntry(key, val.toInt()));
186 |
187 | return ParsingComponents(reference, cmps);
188 | }
189 |
190 | ParsingResult createParsingResult(
191 | int index,
192 | dynamic textOrEndIndex, [
193 | dynamic startComponents,
194 | dynamic endComponents,
195 | ]) {
196 | assert(textOrEndIndex is int || textOrEndIndex is String);
197 | assert(startComponents == null ||
198 | startComponents is ParsingComponents ||
199 | startComponents is Map);
200 | assert(endComponents == null ||
201 | endComponents is ParsingComponents ||
202 | endComponents is Map);
203 | final text = textOrEndIndex is String
204 | ? textOrEndIndex
205 | : this.text.substring(index, textOrEndIndex);
206 |
207 | final start = startComponents != null
208 | ? createParsingComponents(startComponents)
209 | : null;
210 | final end =
211 | endComponents != null ? createParsingComponents(endComponents) : null;
212 |
213 | return ParsingResult(reference, index, text, start, end);
214 | }
215 |
216 | @override
217 | void debug(AsyncDebugBlock block) {
218 | if (option.debug != null) {
219 | if (option.debug is Function) {
220 | option.debug(block);
221 | } else {
222 | final handler = option.debug as DebugHandler;
223 | handler.debug(block);
224 | }
225 | }
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/lib/src/common/abstract_refiners.dart:
--------------------------------------------------------------------------------
1 | import '../chrono.dart' show ParsingContext, Refiner;
2 | import '../results.dart' show ParsingResult;
3 |
4 | /// A special type of {@link Refiner} to filter the results
5 | abstract class Filter implements Refiner {
6 | bool isValid(ParsingContext context, ParsingResult result);
7 |
8 | @override
9 | List refine(
10 | ParsingContext context, List results) {
11 | return results.where((r) => isValid(context, r)).toList();
12 | }
13 | }
14 |
15 | /// A special type of {@link Refiner} to merge consecutive results
16 | abstract class MergingRefiner implements Refiner {
17 | bool shouldMergeResults(String textBetween, ParsingResult currentResult,
18 | ParsingResult nextResult, ParsingContext context);
19 |
20 | ParsingResult mergeResults(String textBetween, ParsingResult currentResult,
21 | ParsingResult nextResult, ParsingContext context);
22 |
23 | @override
24 | List refine(
25 | ParsingContext context, List results) {
26 | if (results.length < 2) {
27 | return results;
28 | }
29 |
30 | final List mergedResults = [];
31 | ParsingResult? curResult = results[0];
32 | ParsingResult? nextResult;
33 |
34 | for (int i = 1; i < results.length; i++) {
35 | nextResult = results[i];
36 |
37 | var iA = curResult!.index + curResult.text.length;
38 | var iB = nextResult.index;
39 | if (iA > iB) {
40 | (iA, iB) = (iB, iA); // swap
41 | }
42 | final textBetween = context.text.substring(iA, iB);
43 | if (!shouldMergeResults(textBetween, curResult, nextResult, context)) {
44 | mergedResults.add(curResult);
45 | curResult = nextResult;
46 | } else {
47 | final left = curResult;
48 | final right = nextResult;
49 | final mergedResult = mergeResults(textBetween, left, right, context);
50 | context.debug(() {
51 | print("$runtimeType merged $left and $right into $mergedResult");
52 | });
53 |
54 | curResult = mergedResult;
55 | }
56 | }
57 |
58 | if (curResult != null) {
59 | mergedResults.add(curResult);
60 | }
61 |
62 | return mergedResults;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/lib/src/common/calculation/weekdays.dart:
--------------------------------------------------------------------------------
1 | import '../../types.dart' show Component, Weekday;
2 | import '../../results.dart' show ParsingComponents, ReferenceWithTimezone;
3 | import '../../utils/timeunits.dart' show addImpliedTimeUnits;
4 |
5 | /// Returns the parsing components at the weekday (considering the modifier). The time and timezone is assume to be
6 | /// similar to the reference.
7 | /// @param reference
8 | /// @param weekday
9 | /// @param modifier "this", "next", "last" modifier word. If empty, returns the weekday closest to the `refDate`.
10 | ParsingComponents createParsingComponentsAtWeekday(
11 | ReferenceWithTimezone reference, Weekday weekday,
12 | [String? modifier]) {
13 | final refDate = reference.getDateWithAdjustedTimezone();
14 | final daysToWeekday = getDaysToWeekday(refDate, weekday, modifier);
15 |
16 | var components = ParsingComponents(reference);
17 | components = addImpliedTimeUnits(components, {"d": daysToWeekday});
18 | components.assign(Component.weekday, weekday.id);
19 |
20 | return components;
21 | }
22 |
23 | /// Returns number of days from refDate to the weekday. The refDate date and timezone information is used.
24 | /// @param refDate
25 | /// @param weekday
26 | /// @param modifier "this", "next", "last" modifier word. If empty, returns the weekday closest to the `refDate`.
27 | int getDaysToWeekday(DateTime refDate, Weekday weekday, [String? modifier]) {
28 | assert(modifier == null ||
29 | modifier == "this" ||
30 | modifier == "next" ||
31 | modifier == "last");
32 | final refWeekday = Weekday.weekById(refDate.weekday);
33 | switch (modifier) {
34 | case "this":
35 | return getDaysForwardToWeekday(refDate, weekday);
36 | case "last":
37 | return getBackwardDaysToWeekday(refDate, weekday);
38 | case "next":
39 | // From Sunday, the next Sunday is 7 days later.
40 | // Otherwise, next Mon is 1 days later, next Tues is 2 days later, and so on..., (return enum value)
41 | if (refWeekday == Weekday.SUNDAY) {
42 | return weekday == Weekday.SUNDAY ? 7 : weekday.id;
43 | }
44 | // From Saturday, the next Saturday is 7 days later, the next Sunday is 8-days later.
45 | // Otherwise, next Mon is (1 + 1) days later, next Tues is (1 + 2) days later, and so on...,
46 | // (return, 2 + [enum value] days)
47 | if (refWeekday == Weekday.SATURDAY) {
48 | if (weekday == Weekday.SATURDAY) return 7;
49 | if (weekday == Weekday.SUNDAY) return 8;
50 | return 1 + weekday.id;
51 | }
52 | // From weekdays, next Mon is the following week's Mon, next Tues the following week's Tues, and so on...
53 | // If the week's weekday already passed (weekday < refWeekday), we simply count forward to next week
54 | // (similar to 'this'). Otherwise, count forward to this week, then add another 7 days.
55 | if (weekday.id < refWeekday.id && weekday != Weekday.SUNDAY) {
56 | return getDaysForwardToWeekday(refDate, weekday);
57 | } else {
58 | return getDaysForwardToWeekday(refDate, weekday) + 7;
59 | }
60 | }
61 | return getDaysToWeekdayClosest(refDate, weekday);
62 | }
63 |
64 | int getDaysToWeekdayClosest(DateTime refDate, Weekday weekday) {
65 | final backward = getBackwardDaysToWeekday(refDate, weekday);
66 | final forward = getDaysForwardToWeekday(refDate, weekday);
67 |
68 | return forward < -backward ? forward : backward;
69 | }
70 |
71 | int getDaysForwardToWeekday(DateTime refDate, Weekday weekday) {
72 | final refWeekday = Weekday.weekById(refDate.weekday);
73 | var forwardCount = weekday.id - refWeekday.id;
74 | if (forwardCount < 0) {
75 | forwardCount += 7;
76 | }
77 | return forwardCount;
78 | }
79 |
80 | int getBackwardDaysToWeekday(DateTime refDate, Weekday weekday) {
81 | final refWeekday = Weekday.weekById(refDate.weekday);
82 | var backwardCount = weekday.id - refWeekday.id;
83 | if (backwardCount >= 0) {
84 | backwardCount -= 7;
85 | }
86 | return backwardCount;
87 | }
88 |
--------------------------------------------------------------------------------
/lib/src/common/casual_references.dart:
--------------------------------------------------------------------------------
1 | import '../results.dart' show ParsingComponents, ReferenceWithTimezone;
2 | import 'package:day/day.dart' as dayjs;
3 | import '../utils/day.dart'
4 | show
5 | assignSimilarDate,
6 | assignSimilarTime,
7 | implySimilarTime,
8 | implyTheNextDay;
9 | import '../types.dart' show Meridiem, Component;
10 |
11 | ParsingComponents now(ReferenceWithTimezone reference) {
12 | final targetDate = dayjs.Day.fromDateTime(reference.instant);
13 | final component = ParsingComponents(reference, {});
14 | assignSimilarDate(component, targetDate);
15 | assignSimilarTime(component, targetDate);
16 | if (reference.timezoneOffset != null) {
17 | component.assign(
18 | Component.timezoneOffset, targetDate.timeZoneOffset.inMinutes);
19 | }
20 | component.addTag("casualReference/now");
21 | return component;
22 | }
23 |
24 | ParsingComponents today(ReferenceWithTimezone reference) {
25 | final targetDate = dayjs.Day.fromDateTime(reference.instant);
26 | final component = ParsingComponents(reference, {});
27 | assignSimilarDate(component, targetDate);
28 | implySimilarTime(component, targetDate);
29 | component.addTag("casualReference/today");
30 | return component;
31 | }
32 |
33 | /// The previous day. Imply the same time.
34 | ParsingComponents yesterday(ReferenceWithTimezone reference) {
35 | return theDayBefore(reference, 1).addTag("casualReference/yesterday");
36 | }
37 |
38 | ParsingComponents theDayBefore(ReferenceWithTimezone reference, int numDay) {
39 | return theDayAfter(reference, -numDay);
40 | }
41 |
42 | /// The following day with dayjs.assignTheNextDay()
43 | ParsingComponents tomorrow(ReferenceWithTimezone reference) {
44 | return theDayAfter(reference, 1).addTag("casualReference/tomorrow");
45 | }
46 |
47 | ParsingComponents theDayAfter(ReferenceWithTimezone reference, int nDays) {
48 | var targetDate = dayjs.Day.fromDateTime(reference.instant);
49 | final component = ParsingComponents(reference, {});
50 | targetDate = targetDate.add(nDays, 'd')!;
51 | assignSimilarDate(component, targetDate);
52 | implySimilarTime(component, targetDate);
53 | return component;
54 | }
55 |
56 | ParsingComponents tonight(ReferenceWithTimezone reference,
57 | [int implyHour = 22]) {
58 | final targetDate = dayjs.Day.fromDateTime(reference.instant);
59 | final component = ParsingComponents(reference, {});
60 | assignSimilarDate(component, targetDate);
61 | component.imply(Component.hour, implyHour);
62 | component.imply(Component.meridiem, Meridiem.PM.id);
63 | component.addTag("casualReference/tonight");
64 | return component;
65 | }
66 |
67 | ParsingComponents lastNight(ReferenceWithTimezone reference,
68 | [int implyHour = 0]) {
69 | var targetDate = dayjs.Day.fromDateTime(reference.instant);
70 | final component = ParsingComponents(reference, {});
71 | if (targetDate.hour() < 6) {
72 | targetDate = targetDate.add(-1, 'd')!;
73 | }
74 | assignSimilarDate(component, targetDate);
75 | component.imply(Component.hour, implyHour);
76 | return component;
77 | }
78 |
79 | ParsingComponents evening(ReferenceWithTimezone reference,
80 | [int implyHour = 20]) {
81 | final component = ParsingComponents(reference, {});
82 | component.imply(Component.meridiem, Meridiem.PM.id);
83 | component.imply(Component.hour, implyHour);
84 | component.addTag("casualReference/evening");
85 | return component;
86 | }
87 |
88 | ParsingComponents yesterdayEvening(ReferenceWithTimezone reference,
89 | [int implyHour = 20]) {
90 | var targetDate = dayjs.Day.fromDateTime(reference.instant);
91 | final component = ParsingComponents(reference, {});
92 | targetDate = targetDate.add(-1, 'd')!;
93 | assignSimilarDate(component, targetDate);
94 | component.imply(Component.hour, implyHour);
95 | component.imply(Component.meridiem, Meridiem.PM.id);
96 | component.addTag("casualReference/yesterday");
97 | component.addTag("casualReference/evening");
98 | return component;
99 | }
100 |
101 | ParsingComponents midnight(ReferenceWithTimezone reference) {
102 | final component = ParsingComponents(reference, {});
103 | final targetDate = dayjs.Day.fromDateTime(reference.instant);
104 | if (targetDate.hour() > 2) {
105 | // Unless it's very early morning (0~2AM), we assume the midnight is the coming midnight.
106 | // Thus, increasing the day by 1.
107 | implyTheNextDay(component, targetDate);
108 | }
109 | component.assign(Component.hour, 0);
110 | component.imply(Component.minute, 0);
111 | component.imply(Component.second, 0);
112 | component.imply(Component.millisecond, 0);
113 | component.addTag("casualReference/midnight");
114 | return component;
115 | }
116 |
117 | ParsingComponents morning(ReferenceWithTimezone reference,
118 | [int implyHour = 6]) {
119 | final component = ParsingComponents(reference, {});
120 | component.imply(Component.meridiem, Meridiem.AM.id);
121 | component.imply(Component.hour, implyHour);
122 | component.imply(Component.minute, 0);
123 | component.imply(Component.second, 0);
124 | component.imply(Component.millisecond, 0);
125 | component.addTag("casualReference/morning");
126 | return component;
127 | }
128 |
129 | ParsingComponents afternoon(ReferenceWithTimezone reference,
130 | [int implyHour = 15]) {
131 | final component = ParsingComponents(reference, {});
132 | component.imply(Component.meridiem, Meridiem.PM.id);
133 | component.imply(Component.hour, implyHour);
134 | component.imply(Component.minute, 0);
135 | component.imply(Component.second, 0);
136 | component.imply(Component.millisecond, 0);
137 | component.addTag("casualReference/afternoon");
138 | return component;
139 | }
140 |
141 | ParsingComponents noon(ReferenceWithTimezone reference) {
142 | final component = ParsingComponents(reference, {});
143 | component.imply(Component.meridiem, Meridiem.AM.id);
144 | component.imply(Component.hour, 12);
145 | component.imply(Component.minute, 0);
146 | component.imply(Component.second, 0);
147 | component.imply(Component.millisecond, 0);
148 | component.addTag("casualReference/noon");
149 | return component;
150 | }
151 |
--------------------------------------------------------------------------------
/lib/src/common/parsers/AbstractParserWithWordBoundary.dart:
--------------------------------------------------------------------------------
1 | import '../../chrono.dart' show Parser, ParsingContext;
2 | import '../../types.dart' show RegExpChronoMatch;
3 |
4 | abstract class AbstractParserWithWordBoundaryChecking implements Parser {
5 | RegExp innerPattern(ParsingContext context);
6 |
7 | /// @returns ParsingComponents | ParsingResult | Map | null
8 | dynamic innerExtract(
9 | ParsingContext context,
10 | RegExpChronoMatch match,
11 | );
12 |
13 | RegExp? _cachedInnerPattern;
14 | RegExp? _cachedPattern;
15 |
16 | String patternLeftBoundary() {
17 | return "(\\W|^)";
18 | }
19 |
20 | @override
21 | RegExp pattern(ParsingContext context) {
22 | final innerPattern = this.innerPattern(context);
23 | if (innerPattern.pattern == _cachedInnerPattern?.pattern) {
24 | return _cachedPattern!;
25 | }
26 |
27 | _cachedPattern = RegExp("${patternLeftBoundary()}${innerPattern.pattern}",
28 | caseSensitive: innerPattern.isCaseSensitive,
29 | multiLine: innerPattern.isMultiLine,
30 | dotAll: innerPattern.isDotAll);
31 | _cachedInnerPattern = innerPattern;
32 | return _cachedPattern!;
33 | }
34 |
35 | @override
36 | extract(ParsingContext context, RegExpChronoMatch match) {
37 | final header = match[1] ?? "";
38 | match.index = match.index + header.length;
39 | match[0] = match[0]!.substring(header.length);
40 | for (var i = 2; i <= match.groupCount; i++) {
41 | match[i - 1] = match[i];
42 | }
43 |
44 | return innerExtract(context, match);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/lib/src/common/parsers/AbstractTimeExpressionParser.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_interpolation_to_compose_strings
2 |
3 | import '../../chrono.dart' show Parser, ParsingContext;
4 | import '../../results.dart' show ParsingComponents, ParsingResult;
5 | import '../../types.dart' show Meridiem, Component, RegExpChronoMatch;
6 |
7 | // prettier-ignore
8 | RegExp primaryTimePattern(
9 | String leftBoundary, String primaryPrefix, String primarySuffix,
10 | [String flags = '']) {
11 | return RegExp(
12 | leftBoundary +
13 | primaryPrefix +
14 | "(\\d{1,4})" +
15 | "(?:" +
16 | "(?:\\.|:|:)" +
17 | "(\\d{1,2})" +
18 | "(?:" +
19 | "(?::|:)" +
20 | "(\\d{2})" +
21 | "(?:\\.(\\d{1,6}))?" +
22 | ")?" +
23 | ")?" +
24 | "(?:\\s*(a\\.m\\.|p\\.m\\.|am?|pm?))?" +
25 | primarySuffix,
26 | caseSensitive: !flags.toLowerCase().contains('i'),
27 | );
28 | }
29 |
30 | // prettier-ignore
31 | RegExp followingTimePatten(String followingPhase, String followingSuffix) {
32 | return RegExp(
33 | "^($followingPhase)" +
34 | "(\\d{1,4})" +
35 | "(?:" +
36 | "(?:\\.|\\:|\\:)" +
37 | "(\\d{1,2})" +
38 | "(?:" +
39 | "(?:\\.|\\:|\\:)" +
40 | "(\\d{1,2})(?:\\.(\\d{1,6}))?" +
41 | ")?" +
42 | ")?" +
43 | "(?:\\s*(a\\.m\\.|p\\.m\\.|am?|pm?))?" +
44 | followingSuffix,
45 | caseSensitive: false,
46 | );
47 | }
48 |
49 | const _HOUR_GROUP = 2;
50 | const _MINUTE_GROUP = 3;
51 | const _SECOND_GROUP = 4;
52 | const _MILLI_SECOND_GROUP = 5;
53 | const _AM_PM_HOUR_GROUP = 6;
54 |
55 | abstract class AbstractTimeExpressionParser implements Parser {
56 | String primaryPrefix();
57 | String followingPhase();
58 | final bool strictMode;
59 |
60 | AbstractTimeExpressionParser([this.strictMode = false]);
61 |
62 | String patternFlags() {
63 | return "i";
64 | }
65 |
66 | String primaryPatternLeftBoundary() {
67 | return "(^|\\s|T|\\b)";
68 | }
69 |
70 | String primarySuffix() {
71 | return "(?!/)(?=\\W|\$)";
72 | }
73 |
74 | String followingSuffix() {
75 | return "(?!/)(?=\\W|\$)";
76 | }
77 |
78 | @override
79 | RegExp pattern(ParsingContext context) {
80 | return getPrimaryTimePatternThroughCache();
81 | }
82 |
83 | @override
84 | ParsingResult? extract(ParsingContext context, RegExpChronoMatch match) {
85 | final startComponents =
86 | extractPrimaryTimeComponents(context, match.original);
87 | if (startComponents == null) {
88 | match.index +=
89 | match[0]!.length; // Skip over potential overlapping pattern
90 | return null;
91 | }
92 |
93 | final index = match.index + match[1]!.length;
94 | final text = match[0]!.substring(match[1]!.length);
95 | final result = context.createParsingResult(index, text, startComponents);
96 | match.index += match[0]!.length; // Skip over potential overlapping pattern
97 |
98 | final remainingText = context.text.substring(match.index);
99 | final followingPattern = getFollowingTimePatternThroughCache();
100 | final followingMatch = followingPattern.firstMatch(remainingText);
101 |
102 | // Pattern "456-12", "2022-12" should not be time without proper context
103 | if (RegExp(r'^\d{3,4}').hasMatch(text) &&
104 | followingMatch != null &&
105 | RegExp(r'^\s*([+-])\s*\d{2,4}$').hasMatch(followingMatch[0]!)) {
106 | return null;
107 | }
108 |
109 | if (followingMatch == null ||
110 | // Pattern "YY.YY -XXXX" is more like timezone offset
111 | RegExp(r'^\s*([+-])\s*[0-9:]{3,5}$').hasMatch(followingMatch[0]!)) {
112 | /// TODO: WARNING: check regex; changed the 'official' version to also catch negative timezones; needs testing.
113 | return _checkAndReturnWithoutFollowingPattern(result);
114 | }
115 |
116 | result.end =
117 | extractFollowingTimeComponents(context, followingMatch, result);
118 | if (result.end != null) {
119 | result.text += followingMatch[0]!;
120 | }
121 |
122 | return _checkAndReturnWithFollowingPattern(result);
123 | }
124 |
125 | ParsingComponents? extractPrimaryTimeComponents(
126 | ParsingContext context, RegExpMatch match,
127 | [bool strict = false]) {
128 | final components = context.createParsingComponents();
129 | var minute = 0;
130 | Meridiem? meridiem;
131 |
132 | // ----- Hours
133 | var hour = int.parse(match[_HOUR_GROUP]!);
134 | if (hour > 100) {
135 | if (strictMode || match[_MINUTE_GROUP] != null) {
136 | return null;
137 | }
138 |
139 | minute = hour % 100;
140 | hour = (hour / 100).floor();
141 | }
142 |
143 | if (hour > 24) {
144 | return null;
145 | }
146 |
147 | // ----- Minutes
148 | if (match[_MINUTE_GROUP] != null) {
149 | if (match[_MINUTE_GROUP]!.length == 1 &&
150 | match[_AM_PM_HOUR_GROUP] == null) {
151 | // Skip single digit minute e.g. "at 1.1 xx"
152 | return null;
153 | }
154 |
155 | minute = int.parse(match[_MINUTE_GROUP]!);
156 | }
157 |
158 | if (minute >= 60) {
159 | return null;
160 | }
161 |
162 | if (hour > 12) {
163 | meridiem = Meridiem.PM;
164 | }
165 |
166 | // ----- AM & PM
167 | if (match[_AM_PM_HOUR_GROUP] != null) {
168 | if (hour > 12) return null;
169 | final ampm = match[_AM_PM_HOUR_GROUP]![0].toLowerCase();
170 | if (ampm == "a") {
171 | meridiem = Meridiem.AM;
172 | if (hour == 12) {
173 | hour = 0;
174 | }
175 | }
176 |
177 | if (ampm == "p") {
178 | meridiem = Meridiem.PM;
179 | if (hour != 12) {
180 | hour += 12;
181 | }
182 | }
183 | }
184 |
185 | components.assign(Component.hour, hour);
186 | components.assign(Component.minute, minute);
187 |
188 | if (meridiem != null) {
189 | components.assign(Component.meridiem, meridiem.id);
190 | } else {
191 | if (hour < 12) {
192 | components.imply(Component.meridiem, Meridiem.AM.id);
193 | } else {
194 | components.imply(Component.meridiem, Meridiem.PM.id);
195 | }
196 | }
197 |
198 | // ----- Millisecond
199 | if (match[_MILLI_SECOND_GROUP] != null) {
200 | final millisecond =
201 | int.parse(match[_MILLI_SECOND_GROUP]!.substring(0, 3));
202 | if (millisecond >= 1000) return null;
203 |
204 | components.assign(Component.millisecond, millisecond);
205 | }
206 |
207 | // ----- Second
208 | if (match[_SECOND_GROUP] != null) {
209 | final second = int.parse(match[_SECOND_GROUP]!);
210 | if (second >= 60) return null;
211 |
212 | components.assign(Component.second, second);
213 | }
214 |
215 | return components;
216 | }
217 |
218 | ParsingComponents? extractFollowingTimeComponents(
219 | ParsingContext context,
220 | RegExpMatch match,
221 | ParsingResult result,
222 | ) {
223 | final components = context.createParsingComponents();
224 |
225 | // ----- Millisecond
226 | if (match[_MILLI_SECOND_GROUP] != null) {
227 | final millisecond =
228 | int.parse(match[_MILLI_SECOND_GROUP]!.substring(0, 3));
229 | if (millisecond >= 1000) return null;
230 |
231 | components.assign(Component.millisecond, millisecond);
232 | }
233 |
234 | // ----- Second
235 | if (match[_SECOND_GROUP] != null) {
236 | final second = int.parse(match[_SECOND_GROUP]!);
237 | if (second >= 60) return null;
238 |
239 | components.assign(Component.second, second);
240 | }
241 |
242 | var hour = int.parse(match[_HOUR_GROUP]!);
243 | var minute = 0;
244 | var meridiem = -1;
245 |
246 | // ----- Minute
247 | if (match[_MINUTE_GROUP] != null) {
248 | minute = int.parse(match[_MINUTE_GROUP]!);
249 | } else if (hour > 100) {
250 | minute = hour % 100;
251 | hour = (hour / 100).floor();
252 | }
253 |
254 | if (minute >= 60 || hour > 24) {
255 | return null;
256 | }
257 |
258 | if (hour >= 12) {
259 | meridiem = Meridiem.PM.id;
260 | }
261 |
262 | // ----- AM & PM
263 | if (match[_AM_PM_HOUR_GROUP] != null) {
264 | if (hour > 12) {
265 | return null;
266 | }
267 |
268 | final ampm = match[_AM_PM_HOUR_GROUP]![0].toLowerCase();
269 | if (ampm == "a") {
270 | meridiem = Meridiem.AM.id;
271 | if (hour == 12) {
272 | hour = 0;
273 | if (!components.isCertain(Component.day)) {
274 | components.imply(Component.day, components.get(Component.day)! + 1);
275 | }
276 | }
277 | }
278 |
279 | if (ampm == "p") {
280 | meridiem = Meridiem.PM.id;
281 | if (hour != 12) hour += 12;
282 | }
283 |
284 | if (!result.start.isCertain(Component.meridiem)) {
285 | if (meridiem == Meridiem.AM.id) {
286 | result.start.imply(Component.meridiem, Meridiem.AM.id);
287 |
288 | if (result.start.get(Component.hour) == 12) {
289 | result.start.assign(Component.hour, 0);
290 | }
291 | } else {
292 | result.start.imply(Component.meridiem, Meridiem.PM.id);
293 |
294 | if (result.start.get(Component.hour) != 12) {
295 | result.start
296 | .assign(Component.hour, result.start.get(Component.hour)! + 12);
297 | }
298 | }
299 | }
300 | }
301 |
302 | components.assign(Component.hour, hour);
303 | components.assign(Component.minute, minute);
304 |
305 | if (meridiem >= 0) {
306 | components.assign(Component.meridiem, meridiem);
307 | } else {
308 | final startAtPM = result.start.isCertain(Component.meridiem) &&
309 | result.start.get(Component.hour)! > 12;
310 | if (startAtPM) {
311 | if (result.start.get(Component.hour)! - 12 > hour) {
312 | // 10pm - 1 (am)
313 | components.imply(Component.meridiem, Meridiem.AM.id);
314 | } else if (hour <= 12) {
315 | components.assign(Component.hour, hour + 12);
316 | components.assign(Component.meridiem, Meridiem.PM.id);
317 | }
318 | } else if (hour > 12) {
319 | components.imply(Component.meridiem, Meridiem.PM.id);
320 | } else if (hour <= 12) {
321 | components.imply(Component.meridiem, Meridiem.AM.id);
322 | }
323 | }
324 |
325 | if (components.date().millisecondsSinceEpoch <
326 | result.start.date().millisecondsSinceEpoch) {
327 | components.imply(Component.day, components.get(Component.day)! + 1);
328 | }
329 |
330 | return components;
331 | }
332 |
333 | T? _checkAndReturnWithoutFollowingPattern(T result) {
334 | // Single digit (e.g "1") should not be counted as time expression (without proper context)
335 | if (RegExp(r'^\d$').hasMatch(result.text)) {
336 | return null;
337 | }
338 |
339 | // Three or more digit (e.g. "203", "2014") should not be counted as time expression (without proper context)
340 | if (RegExp(r'^\d\d\d+$').hasMatch(result.text)) {
341 | return null;
342 | }
343 |
344 | // Instead of "am/pm", it ends with "a" or "p" (e.g "1a", "123p"), this seems unlikely
345 | if (RegExp(r'\d[apAP]$').hasMatch(result.text)) {
346 | return null;
347 | }
348 |
349 | // If it ends only with numbers or dots
350 | final endingWithNumbers =
351 | RegExp(r'[^\d:.](\d[\d.]+)$').firstMatch(result.text);
352 | if (endingWithNumbers != null) {
353 | final String endingNumbers = endingWithNumbers[1]!;
354 |
355 | // In strict mode (e.g. "at 1" or "at 1.2"), this should not be accepted
356 | if (strictMode) {
357 | return null;
358 | }
359 |
360 | // If it ends only with dot single digit, e.g. "at 1.2"
361 | if (endingNumbers.contains(".") &&
362 | !RegExp(r'\d(\.\d{2})+$').hasMatch(endingNumbers)) {
363 | return null;
364 | }
365 |
366 | // If it ends only with numbers above 24, e.g. "at 25"
367 | final endingNumberVal = int.tryParse(endingNumbers);
368 | if ((endingNumberVal ?? 0) > 24) {
369 | return null;
370 | }
371 | }
372 |
373 | return result;
374 | }
375 |
376 | T? _checkAndReturnWithFollowingPattern(T result) {
377 | if (RegExp(r'^\d+-\d+$').hasMatch(result.text)) {
378 | return null;
379 | }
380 |
381 | // If it ends only with numbers or dots
382 | final endingWithNumbers =
383 | RegExp(r'[^\d:.](\d[\d.]+)\s*-\s*(\d[\d.]+)$').firstMatch(result.text);
384 | if (endingWithNumbers != null) {
385 | // In strict mode (e.g. "at 1-3" or "at 1.2 - 2.3"), this should not be accepted
386 | if (strictMode) {
387 | return null;
388 | }
389 |
390 | final String startingNumbers = endingWithNumbers[1]!;
391 | final String endingNumbers = endingWithNumbers[2]!;
392 | // If it ends only with dot single digit, e.g. "at 1.2"
393 | if (endingNumbers.contains(".") &&
394 | !RegExp(r'\d(\.\d{2})+$').hasMatch(endingNumbers)) {
395 | return null;
396 | }
397 |
398 | // If it ends only with numbers above 24, e.g. "at 25"
399 | final endingNumberVal = int.parse(endingNumbers);
400 | final startingNumberVal = int.parse(startingNumbers);
401 | if (endingNumberVal > 24 || startingNumberVal > 24) {
402 | return null;
403 | }
404 | }
405 |
406 | return result;
407 | }
408 |
409 | String? _cachedPrimaryPrefix;
410 | String? _cachedPrimarySuffix;
411 | RegExp? _cachedPrimaryTimePattern;
412 |
413 | RegExp getPrimaryTimePatternThroughCache() {
414 | final primaryPrefix = this.primaryPrefix();
415 | final primarySuffix = this.primarySuffix();
416 |
417 | if (_cachedPrimaryPrefix == primaryPrefix &&
418 | _cachedPrimarySuffix == primarySuffix) {
419 | return _cachedPrimaryTimePattern!;
420 | }
421 |
422 | _cachedPrimaryTimePattern = primaryTimePattern(
423 | primaryPatternLeftBoundary(),
424 | primaryPrefix,
425 | primarySuffix,
426 | patternFlags());
427 | _cachedPrimaryPrefix = primaryPrefix;
428 | _cachedPrimarySuffix = primarySuffix;
429 | return _cachedPrimaryTimePattern!;
430 | }
431 |
432 | String? _cachedFollowingPhase;
433 | String? _cachedFollowingSuffix;
434 | RegExp? _cachedFollowingTimePattern;
435 |
436 | RegExp getFollowingTimePatternThroughCache() {
437 | final followingPhase = this.followingPhase();
438 | final followingSuffix = this.followingSuffix();
439 |
440 | if (_cachedFollowingPhase == followingPhase &&
441 | _cachedFollowingSuffix == followingSuffix) {
442 | return _cachedFollowingTimePattern!;
443 | }
444 |
445 | _cachedFollowingTimePattern =
446 | followingTimePatten(followingPhase, followingSuffix);
447 | _cachedFollowingPhase = followingPhase;
448 | _cachedFollowingSuffix = followingSuffix;
449 | return _cachedFollowingTimePattern!;
450 | }
451 | }
452 |
--------------------------------------------------------------------------------
/lib/src/common/parsers/ISOFormatParser.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_interpolation_to_compose_strings, prefer_adjacent_string_concatenation, constant_identifier_names
2 | import '../../chrono.dart' show ParsingContext;
3 | import '../../types.dart' show Component, RegExpChronoMatch;
4 | import './AbstractParserWithWordBoundary.dart';
5 |
6 | // ISO 8601
7 | // http://www.w3.org/TR/NOTE-datetime
8 | // - YYYY-MM-DD
9 | // - YYYY-MM-DDThh:mmTZD
10 | // - YYYY-MM-DDThh:mm:ssTZD
11 | // - YYYY-MM-DDThh:mm:ss.sTZD
12 | // - TZD = (Z or +hh:mm or -hh:mm)
13 |
14 | // prettier-ignore
15 | final _pattern = RegExp(
16 | "([0-9]{4})\\-([0-9]{1,2})\\-([0-9]{1,2})" +
17 | "(?:T" + //..
18 | "([0-9]{1,2}):([0-9]{1,2})" + // hh:mm
19 | "(?:" +
20 | ":([0-9]{1,2})(?:\\.(\\d{1,4}))?" +
21 | ")?" + // :ss.s
22 | "(?:" +
23 | "Z|([+-]\\d{2}):?(\\d{2})?" +
24 | ")?" + // TZD (Z or ±hh:mm or ±hhmm or ±hh)
25 | ")?" +
26 | "(?=\\W|\$)",
27 | caseSensitive: false);
28 |
29 | const _YEAR_NUMBER_GROUP = 1;
30 | const _MONTH_NUMBER_GROUP = 2;
31 | const _DATE_NUMBER_GROUP = 3;
32 | const _HOUR_NUMBER_GROUP = 4;
33 | const _MINUTE_NUMBER_GROUP = 5;
34 | const _SECOND_NUMBER_GROUP = 6;
35 | const _MILLISECOND_NUMBER_GROUP = 7;
36 | const _TZD_HOUR_OFFSET_GROUP = 8;
37 | const _TZD_MINUTE_OFFSET_GROUP = 9;
38 |
39 | class ISOFormatParser extends AbstractParserWithWordBoundaryChecking {
40 | @override
41 | RegExp innerPattern(context) {
42 | return _pattern;
43 | }
44 |
45 | @override
46 | Map innerExtract(
47 | ParsingContext context, RegExpChronoMatch match) {
48 | final Map components = {};
49 | components[Component.year] = int.parse(match[_YEAR_NUMBER_GROUP]!);
50 | components[Component.month] = int.parse(match[_MONTH_NUMBER_GROUP]!);
51 | components[Component.day] = int.parse(match[_DATE_NUMBER_GROUP]!);
52 |
53 | if (match[_HOUR_NUMBER_GROUP] != null) {
54 | components[Component.hour] = int.parse(match[_HOUR_NUMBER_GROUP]!);
55 | components[Component.minute] = int.parse(match[_MINUTE_NUMBER_GROUP]!);
56 |
57 | if (match[_SECOND_NUMBER_GROUP] != null) {
58 | components[Component.second] = int.parse(match[_SECOND_NUMBER_GROUP]!);
59 | }
60 |
61 | if (match[_MILLISECOND_NUMBER_GROUP] != null) {
62 | components[Component.millisecond] =
63 | int.parse(match[_MILLISECOND_NUMBER_GROUP]!);
64 | }
65 |
66 | if (match[_TZD_HOUR_OFFSET_GROUP] == null) {
67 | components[Component.timezoneOffset] = 0;
68 | } else {
69 | final hourOffset = int.parse(match[_TZD_HOUR_OFFSET_GROUP]!);
70 |
71 | var minuteOffset = 0;
72 | if (match[_TZD_MINUTE_OFFSET_GROUP] != null) {
73 | minuteOffset = int.parse(match[_TZD_MINUTE_OFFSET_GROUP]!);
74 | }
75 |
76 | var offset = hourOffset * 60;
77 | if (offset < 0) {
78 | offset -= minuteOffset;
79 | } else {
80 | offset += minuteOffset;
81 | }
82 |
83 | components[Component.timezoneOffset] = offset;
84 | }
85 | }
86 |
87 | return components;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/lib/src/common/parsers/SlashDateFormatParser.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_interpolation_to_compose_strings, constant_identifier_names, prefer_adjacent_string_concatenation
2 | import '../../chrono.dart' show Parser, ParsingContext;
3 | import '../../results.dart' show ParsingResult;
4 | import '../../types.dart' show Component, RegExpChronoMatch;
5 | import '../../calculation/years.dart'
6 | show findMostLikelyADYear, findYearClosestToRef;
7 |
8 | /// Date format with slash "/" (or dot ".") between numbers.
9 | /// For examples:
10 | /// - 7/10
11 | /// - 7/12/2020
12 | /// - 7.12.2020
13 | // ignore: non_constant_identifier_names
14 | final _PATTERN = RegExp(
15 | "([^\\d]|^)" +
16 | "([0-3]{0,1}[0-9]{1})[\\/\\.\\-]([0-3]{0,1}[0-9]{1})" +
17 | "(?:[\\/\\.\\-]([0-9]{4}|[0-9]{2}))?" +
18 | "(\\W|\$)",
19 | caseSensitive: false,
20 | );
21 |
22 | const _OPENING_GROUP = 1;
23 | const _ENDING_GROUP = 5;
24 |
25 | const _FIRST_NUMBERS_GROUP = 2;
26 | const _SECOND_NUMBERS_GROUP = 3;
27 |
28 | const _YEAR_GROUP = 4;
29 |
30 | class SlashDateFormatParser implements Parser {
31 | int groupNumberMonth;
32 | int groupNumberDay;
33 |
34 | SlashDateFormatParser(bool littleEndian)
35 | : groupNumberMonth =
36 | littleEndian ? _SECOND_NUMBERS_GROUP : _FIRST_NUMBERS_GROUP,
37 | groupNumberDay =
38 | littleEndian ? _FIRST_NUMBERS_GROUP : _SECOND_NUMBERS_GROUP;
39 |
40 | @override
41 | RegExp pattern(context) {
42 | return _PATTERN;
43 | }
44 |
45 | @override
46 | ParsingResult? extract(ParsingContext context, RegExpChronoMatch match) {
47 | // Because of how pattern is executed on remaining text in `chrono.ts`, the character before the match could
48 | // still be a number (e.g. X[X/YY/ZZ] or XX[/YY/ZZ] or [XX/YY/]ZZ). We want to check and skip them.
49 | if (match[_OPENING_GROUP]!.isEmpty &&
50 | match.index > 0 &&
51 | match.index < context.text.length) {
52 | final previousChar = int.tryParse(context.text[match.index - 1]);
53 | if (previousChar != null) {
54 | return null;
55 | }
56 | }
57 |
58 | final index = match.index + match[_OPENING_GROUP]!.length;
59 | final text = match[0]!.substring(match[_OPENING_GROUP]!.length,
60 | match[0]!.length - (match[_ENDING_GROUP]?.length ?? 0));
61 |
62 | // '1.12', '1.12.12' is more like a version numbers
63 | if (RegExp(r'^\d\.\d$').hasMatch(text) ||
64 | RegExp(r'^\d\.\d{1,2}\.\d{1,2}\s*$').hasMatch(text)) {
65 | return null;
66 | }
67 |
68 | // MM/dd -> OK
69 | // MM.dd -> NG
70 | if (match[_YEAR_GROUP] == null && !match[0]!.contains("/")) {
71 | return null;
72 | }
73 |
74 | final result = context.createParsingResult(index, text);
75 | var month = int.parse(match[groupNumberMonth]!);
76 | var day = int.parse(match[groupNumberDay]!);
77 |
78 | if (month < 1 || month > 12) {
79 | if (month > 12) {
80 | if (day >= 1 && day <= 12 && month <= 31) {
81 | [day, month] = [month, day];
82 | } else {
83 | return null;
84 | }
85 | }
86 | }
87 |
88 | if (day < 1 || day > 31) {
89 | return null;
90 | }
91 |
92 | result.start.assign(Component.day, day);
93 | result.start.assign(Component.month, month);
94 |
95 | if (match[_YEAR_GROUP] != null) {
96 | final rawYearNumber = int.parse(match[_YEAR_GROUP]!);
97 | final year = findMostLikelyADYear(rawYearNumber);
98 | result.start.assign(Component.year, year);
99 | } else {
100 | final year = findYearClosestToRef(context.reference.instant, day, month);
101 | result.start.imply(Component.year, year);
102 | }
103 |
104 | return result;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/lib/src/common/refiners/AbstractMergeDateRangeRefiner.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math' show min;
2 | import '../../results.dart' show ParsingResult;
3 | import '../../types.dart' show Component;
4 | import '../abstract_refiners.dart' show MergingRefiner;
5 |
6 | abstract class AbstractMergeDateRangeRefiner extends MergingRefiner {
7 | RegExp patternBetween();
8 |
9 | @override
10 | bool shouldMergeResults(textBetween, currentResult, nextResult, context) {
11 | return currentResult.end == null &&
12 | nextResult.end == null &&
13 | patternBetween().hasMatch(textBetween);
14 | }
15 |
16 | @override
17 | // ignore: avoid_renaming_method_parameters
18 | ParsingResult mergeResults(textBetween, fromResult, toResult, context) {
19 | if (!fromResult.start.isOnlyWeekdayComponent() &&
20 | !toResult.start.isOnlyWeekdayComponent()) {
21 | toResult.start.getCertainComponents().forEach((key) {
22 | if (!fromResult.start.isCertain(key)) {
23 | fromResult.start.imply(key, toResult.start.get(key)!);
24 | }
25 | });
26 |
27 | fromResult.start.getCertainComponents().forEach((key) {
28 | if (!toResult.start.isCertain(key)) {
29 | toResult.start.imply(key, fromResult.start.get(key)!);
30 | }
31 | });
32 | }
33 |
34 | if (fromResult.start.date().millisecondsSinceEpoch >
35 | toResult.start.date().millisecondsSinceEpoch) {
36 | var fromMoment = fromResult.start.dayjs();
37 | var toMoment = toResult.start.dayjs();
38 | if (toResult.start.isOnlyWeekdayComponent() &&
39 | toMoment.add(7, "days")!.isAfter(fromMoment)) {
40 | toMoment = toMoment.add(7, "days")!;
41 | toResult.start.imply(Component.day, toMoment.date());
42 | toResult.start.imply(Component.month, toMoment.month());
43 | toResult.start.imply(Component.year, toMoment.year());
44 | } else if (fromResult.start.isOnlyWeekdayComponent() &&
45 | fromMoment.add(-7, "days")!.isBefore(toMoment)) {
46 | fromMoment = fromMoment.add(-7, "days")!;
47 | fromResult.start.imply(Component.day, fromMoment.date());
48 | fromResult.start.imply(Component.month, fromMoment.month());
49 | fromResult.start.imply(Component.year, fromMoment.year());
50 | } else if (toResult.start.isDateWithUnknownYear() &&
51 | toMoment.add(1, "years")!.isAfter(fromMoment)) {
52 | toMoment = toMoment.add(1, "years")!;
53 | toResult.start.imply(Component.year, toMoment.year());
54 | } else if (fromResult.start.isDateWithUnknownYear() &&
55 | fromMoment.add(-1, "years")!.isBefore(toMoment)) {
56 | fromMoment = fromMoment.add(-1, "years")!;
57 | fromResult.start.imply(Component.year, fromMoment.year());
58 | } else {
59 | [toResult, fromResult] = [fromResult, toResult];
60 | }
61 | }
62 |
63 | final result = fromResult.clone();
64 | result.start = fromResult.start;
65 | result.end = toResult.start;
66 | result.index = min(fromResult.index, toResult.index);
67 | if (fromResult.index < toResult.index) {
68 | result.text = fromResult.text + textBetween + toResult.text;
69 | } else {
70 | result.text = toResult.text + textBetween + fromResult.text;
71 | }
72 |
73 | return result;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/lib/src/common/refiners/AbstractMergeDateTimeRefiner.dart:
--------------------------------------------------------------------------------
1 | import '../../results.dart' show ParsingResult;
2 | import '../abstract_refiners.dart' show MergingRefiner;
3 | import '../../calculation/mergingCalculation.dart' show mergeDateTimeResult;
4 |
5 | abstract class AbstractMergeDateTimeRefiner extends MergingRefiner {
6 | RegExp patternBetween();
7 |
8 | @override
9 | bool shouldMergeResults(String textBetween, ParsingResult currentResult,
10 | ParsingResult nextResult, context) {
11 | return (((currentResult.start.isOnlyDate() &&
12 | nextResult.start.isOnlyTime()) ||
13 | (nextResult.start.isOnlyDate() &&
14 | currentResult.start.isOnlyTime())) &&
15 | patternBetween().hasMatch(textBetween));
16 | }
17 |
18 | @override
19 | ParsingResult mergeResults(String textBetween, ParsingResult currentResult,
20 | ParsingResult nextResult, context) {
21 | final result = currentResult.start.isOnlyDate()
22 | ? mergeDateTimeResult(currentResult, nextResult)
23 | : mergeDateTimeResult(nextResult, currentResult);
24 |
25 | result.index = currentResult.index;
26 | result.text = currentResult.text + textBetween + nextResult.text;
27 | return result;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/src/common/refiners/ExtractTimezoneAbbrRefiner.dart:
--------------------------------------------------------------------------------
1 | // Map ABBR -> Offset in minute
2 | import '../../chrono.dart' show ParsingContext, Refiner;
3 | import '../../types.dart' show TimezoneAbbrMap, Component;
4 | import '../../results.dart' show ParsingResult;
5 | import '../../timezone.dart' show toTimezoneOffset;
6 |
7 | // ignore: non_constant_identifier_names
8 | final TIMEZONE_NAME_PATTERN =
9 | RegExp(r'^\s*,?\s*\(?([A-Z]{2,4})\)?(?=\W|$)', caseSensitive: false);
10 |
11 | class ExtractTimezoneAbbrRefiner implements Refiner {
12 | final TimezoneAbbrMap? timezoneOverrides;
13 | ExtractTimezoneAbbrRefiner([this.timezoneOverrides]);
14 |
15 | @override
16 | List refine(
17 | ParsingContext context, List results) {
18 | final timezoneOverrides = context.option.timezones ?? {};
19 |
20 | for (final result in results) {
21 | final suffix = context.text.substring(result.index + result.text.length);
22 | final match = TIMEZONE_NAME_PATTERN.firstMatch(suffix);
23 | if (match == null) {
24 | continue;
25 | }
26 |
27 | final timezoneAbbr = match[1]!.toUpperCase();
28 | final refDate =
29 | result.start.date(); // ?? result.refDate ?? DateTime.now();
30 | final tzOverrides = {
31 | ...(this.timezoneOverrides ?? {}),
32 | ...timezoneOverrides
33 | };
34 | final extractedTimezoneOffset =
35 | toTimezoneOffset(timezoneAbbr, refDate, tzOverrides);
36 | if (extractedTimezoneOffset == null) {
37 | continue;
38 | }
39 | context.debug(() {
40 | print(
41 | "Extracting timezone: '$timezoneAbbr' into: $extractedTimezoneOffset for: ${result.start}");
42 | });
43 |
44 | final currentTimezoneOffset = result.start.get(Component.timezoneOffset);
45 | if (currentTimezoneOffset != null &&
46 | extractedTimezoneOffset != currentTimezoneOffset) {
47 | // We may already have extracted the timezone offset e.g. "11 am GMT+0900 (JST)"
48 | // - if they are equal, we also want to take the abbreviation text into result
49 | // - if they are not equal, we trust the offset more
50 | if (result.start.isCertain(Component.timezoneOffset)) {
51 | continue;
52 | }
53 |
54 | // This is often because it's relative time with inferred timezone (e.g. in 1 hour, tomorrow)
55 | // Then, we want to double-check the abbr case (e.g. "GET" not "get")
56 | if (timezoneAbbr != match[1]) {
57 | continue;
58 | }
59 | }
60 |
61 | if (result.start.isOnlyDate()) {
62 | // If the time is not explicitly mentioned,
63 | // Then, we also want to double-check the abbr case (e.g. "GET" not "get")
64 | if (timezoneAbbr != match[1]) {
65 | continue;
66 | }
67 | }
68 |
69 | result.text += match[0]!;
70 |
71 | if (!result.start.isCertain(Component.timezoneOffset)) {
72 | result.start.assign(Component.timezoneOffset, extractedTimezoneOffset);
73 | }
74 |
75 | if (result.end != null &&
76 | !result.end!.isCertain(Component.timezoneOffset)) {
77 | result.end!.assign(Component.timezoneOffset, extractedTimezoneOffset);
78 | }
79 | }
80 |
81 | return results;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/lib/src/common/refiners/ExtractTimezoneOffsetRefiner.dart:
--------------------------------------------------------------------------------
1 | import '../../chrono.dart' show ParsingContext, Refiner;
2 | import '../../types.dart' show Component;
3 | import '../../results.dart' show ParsingResult;
4 |
5 | final TIMEZONE_OFFSET_PATTERN = RegExp(
6 | r'^\s*(?:\(?(?:GMT|UTC)\s?)?([+-])(\d{1,2})(?::?(\d{2}))?\)?',
7 | caseSensitive: false);
8 | const _TIMEZONE_OFFSET_SIGN_GROUP = 1;
9 | const _TIMEZONE_OFFSET_HOUR_OFFSET_GROUP = 2;
10 | const _TIMEZONE_OFFSET_MINUTE_OFFSET_GROUP = 3;
11 |
12 | class ExtractTimezoneOffsetRefiner implements Refiner {
13 | @override
14 | List refine(
15 | ParsingContext context, List results) {
16 | for (final result in results) {
17 | if (result.start.isCertain(Component.timezoneOffset)) {
18 | continue;
19 | }
20 |
21 | final suffix = context.text.substring(result.index + result.text.length);
22 | final match = TIMEZONE_OFFSET_PATTERN.firstMatch(suffix);
23 | if (match == null) {
24 | continue;
25 | }
26 |
27 | context.debug(() {
28 | print("Extracting timezone: '${match[0]}' into : $result");
29 | });
30 |
31 | final hourOffset = int.parse(match[_TIMEZONE_OFFSET_HOUR_OFFSET_GROUP]!);
32 | final minuteOffset =
33 | int.parse(match[_TIMEZONE_OFFSET_MINUTE_OFFSET_GROUP] ?? "0");
34 | var timezoneOffset = hourOffset * 60 + minuteOffset;
35 | // No timezones have offsets greater than 14 hours, so disregard this match
36 | if (timezoneOffset > 14 * 60) {
37 | continue;
38 | }
39 | if (match[_TIMEZONE_OFFSET_SIGN_GROUP] == "-") {
40 | timezoneOffset = -timezoneOffset;
41 | }
42 |
43 | if (result.end != null) {
44 | result.end!.assign(Component.timezoneOffset, timezoneOffset);
45 | }
46 |
47 | result.start.assign(Component.timezoneOffset, timezoneOffset);
48 | result.text += match[0]!;
49 | }
50 |
51 | return results;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/lib/src/common/refiners/ForwardDateRefiner.dart:
--------------------------------------------------------------------------------
1 | /*
2 | Enforce 'forwardDate' option to on the results. When there are missing component,
3 | e.g. "March 12-13 (without year)" or "Thursday", the refiner will try to adjust the result
4 | into the future instead of the past.
5 | */
6 |
7 | import 'package:day/day.dart' as dayjs;
8 | import '../../chrono.dart' show ParsingContext, Refiner;
9 | import '../../types.dart' show Component;
10 | import '../../results.dart' show ParsingResult;
11 | import '../../utils/day.dart' show implySimilarDate;
12 |
13 | extension DayWeekdayWriter on dayjs.Day {
14 | /// Gets or sets the weekday
15 | dayjs.Day setWeekday(int day) {
16 | final d = clone();
17 | d.setValue('weekday', day);
18 | d.finished();
19 | return d;
20 | }
21 | }
22 |
23 | class ForwardDateRefiner implements Refiner {
24 | @override
25 | List refine(
26 | ParsingContext context, List results) {
27 | if (context.option.forwardDate == null) {
28 | return results;
29 | }
30 |
31 | for (final result in results) {
32 | var refMoment = dayjs.Day.fromDateTime(context.reference.instant);
33 |
34 | if (result.start.isOnlyTime() &&
35 | refMoment.isAfter(result.start.dayjs())) {
36 | refMoment = refMoment.add(1, 'd')!;
37 | implySimilarDate(result.start, refMoment);
38 | if (result.end != null && result.end!.isOnlyTime()) {
39 | implySimilarDate(result.end!, refMoment);
40 | if (result.start.dayjs().isAfter(result.end!.dayjs())) {
41 | refMoment = refMoment.add(1, 'd')!;
42 | implySimilarDate(result.end!, refMoment);
43 | }
44 | }
45 | }
46 |
47 | if (result.start.isOnlyWeekdayComponent() &&
48 | refMoment.isAfter(result.start.dayjs())) {
49 | if (refMoment.weekday() >= result.start.get(Component.weekday)!) {
50 | refMoment =
51 | refMoment.setWeekday(result.start.get(Component.weekday)! + 7);
52 | } else {
53 | refMoment =
54 | refMoment.setWeekday(result.start.get(Component.weekday)!);
55 | }
56 |
57 | result.start.imply(Component.day, refMoment.date());
58 | result.start.imply(Component.month, refMoment.month());
59 | result.start.imply(Component.year, refMoment.year());
60 | context.debug(() {
61 | print("Forward weekly adjusted for $result (${result.start})");
62 | });
63 |
64 | if (result.end != null && result.end!.isOnlyWeekdayComponent()) {
65 | // Adjust date to the coming week
66 | if (refMoment.weekday() > result.end!.get(Component.weekday)!) {
67 | refMoment =
68 | refMoment.setWeekday(result.end!.get(Component.weekday)! + 7);
69 | } else {
70 | refMoment =
71 | refMoment.setWeekday(result.end!.get(Component.weekday)!);
72 | }
73 |
74 | result.end!.imply(Component.day, refMoment.date());
75 | result.end!.imply(Component.month, refMoment.month());
76 | result.end!.imply(Component.year, refMoment.year());
77 | context.debug(() {
78 | print("Forward weekly adjusted for $result (${result.end})");
79 | });
80 | }
81 | }
82 |
83 | // In case where we know the month, but not which year (e.g. "in December", "25th December"),
84 | // try move to another year
85 | if (result.start.isDateWithUnknownYear() &&
86 | refMoment.isAfter(result.start.dayjs())) {
87 | for (int i = 0; i < 3 && refMoment.isAfter(result.start.dayjs()); i++) {
88 | result.start
89 | .imply(Component.year, result.start.get(Component.year)! + 1);
90 | context.debug(() {
91 | print("Forward yearly adjusted for $result (${result.start})");
92 | });
93 |
94 | if (result.end != null && !result.end!.isCertain(Component.year)) {
95 | result.end!
96 | .imply(Component.year, result.end!.get(Component.year)! + 1);
97 | context.debug(() {
98 | print("Forward yearly adjusted for $result (${result.end})");
99 | });
100 | }
101 | }
102 | }
103 | }
104 |
105 | return results;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/lib/src/common/refiners/MergeWeekdayComponentRefiner.dart:
--------------------------------------------------------------------------------
1 | import '../abstract_refiners.dart' show MergingRefiner;
2 | import '../../results.dart' show ParsingResult;
3 | import '../../types.dart' show Component;
4 |
5 | /// Merge weekday component into more completed data
6 | /// - [Sunday] [12/7/2014] => [Sunday 12/7/2014]
7 | /// - [Tuesday], [January 13, 2012] => [Sunday 12/7/2014]
8 | class MergeWeekdayComponentRefiner extends MergingRefiner {
9 | @override
10 | ParsingResult mergeResults(String textBetween, ParsingResult currentResult,
11 | ParsingResult nextResult, context) {
12 | final newResult = nextResult.clone();
13 | newResult.index = currentResult.index;
14 | newResult.text = currentResult.text + textBetween + newResult.text;
15 |
16 | newResult.start
17 | .assign(Component.weekday, currentResult.start.get(Component.weekday)!);
18 | if (newResult.end != null) {
19 | newResult.end!.assign(
20 | Component.weekday, currentResult.start.get(Component.weekday)!);
21 | }
22 |
23 | return newResult;
24 | }
25 |
26 | @override
27 | bool shouldMergeResults(String textBetween, ParsingResult currentResult,
28 | ParsingResult nextResult, context) {
29 | final weekdayThenNormalDate =
30 | currentResult.start.isOnlyWeekdayComponent() &&
31 | !currentResult.start.isCertain(Component.hour) &&
32 | nextResult.start.isCertain(Component.day);
33 | return weekdayThenNormalDate && RegExp(r'^,?\s*$').hasMatch(textBetween);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/lib/src/common/refiners/OverlapRemovalRefiner.dart:
--------------------------------------------------------------------------------
1 | import '../../chrono.dart' show ParsingContext, Refiner;
2 | import '../../results.dart' show ParsingResult;
3 |
4 | class OverlapRemovalRefiner implements Refiner {
5 | @override
6 | List refine(
7 | ParsingContext context, List results) {
8 | if (results.length < 2) {
9 | return results;
10 | }
11 |
12 | final List filteredResults = [];
13 |
14 | ParsingResult? prevResult = results[0];
15 | for (int i = 1; i < results.length; i++) {
16 | final result = results[i];
17 |
18 | // If overlap, compare the length and discard the shorter one
19 | if (result.index < prevResult!.index + prevResult.text.length) {
20 | if (result.text.length > prevResult.text.length) {
21 | prevResult = result;
22 | }
23 | } else {
24 | filteredResults.add(prevResult);
25 | prevResult = result;
26 | }
27 | }
28 |
29 | // The last one
30 | if (prevResult != null) {
31 | filteredResults.add(prevResult);
32 | }
33 |
34 | if (filteredResults.length != results.length) {
35 | context.debug(() {
36 | print("Refiner $runtimeType: filtered out ${results.length - filteredResults.length} results");
37 | });
38 | }
39 |
40 | return filteredResults;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/lib/src/common/refiners/UnlikelyFormatFilter.dart:
--------------------------------------------------------------------------------
1 | import '../abstract_refiners.dart' show Filter;
2 | import '../../results.dart' show ParsingResult;
3 | import '../../types.dart' show Component;
4 |
5 | class UnlikelyFormatFilter extends Filter {
6 | final bool _strictMode;
7 |
8 | UnlikelyFormatFilter([bool strictMode = false])
9 | : _strictMode = strictMode,
10 | super();
11 |
12 | @override
13 | bool isValid(context, ParsingResult result) {
14 | if (RegExp(r'^\d*(\.\d*)?$').hasMatch(result.text.replaceFirst(" ", ""))) {
15 | context.debug(() {
16 | print("Removing unlikely result '${result.text}'");
17 | });
18 |
19 | return false;
20 | }
21 |
22 | if (!result.start.isValidDate()) {
23 | context.debug(() {
24 | print("Removing invalid result: $result (${result.start})");
25 | });
26 |
27 | return false;
28 | }
29 |
30 | if (result.end != null && !result.end!.isValidDate()) {
31 | context.debug(() {
32 | print("Removing invalid result: $result (${result.end})");
33 | });
34 |
35 | return false;
36 | }
37 |
38 | if (_strictMode) {
39 | return isStrictModeValid(context, result);
40 | }
41 |
42 | return true;
43 | }
44 |
45 | bool isStrictModeValid(context, ParsingResult result) {
46 | if (result.start.isOnlyWeekdayComponent()) {
47 | context.debug(() {
48 | print(
49 | "(Strict) Removing weekday only component: $result (${result.end})");
50 | });
51 |
52 | return false;
53 | }
54 |
55 | if (result.start.isOnlyTime() &&
56 | (!result.start.isCertain(Component.hour) ||
57 | !result.start.isCertain(Component.minute))) {
58 | context.debug(() {
59 | print(
60 | "(Strict) Removing uncertain time component: $result (${result.end})");
61 | });
62 |
63 | return false;
64 | }
65 |
66 | return true;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/lib/src/configurations.dart:
--------------------------------------------------------------------------------
1 | import './chrono.dart' show Configuration;
2 | import './common/refiners/ExtractTimezoneAbbrRefiner.dart';
3 | import './common/refiners/ExtractTimezoneOffsetRefiner.dart';
4 | import './common/refiners/OverlapRemovalRefiner.dart';
5 | import './common/refiners/ForwardDateRefiner.dart';
6 | import './common/refiners/UnlikelyFormatFilter.dart';
7 | import './common/parsers/ISOFormatParser.dart';
8 | import './common/refiners/MergeWeekdayComponentRefiner.dart';
9 |
10 | Configuration includeCommonConfiguration(Configuration configuration, [ bool strictMode = false ]) {
11 | configuration.parsers.insert(0, ISOFormatParser());
12 |
13 | configuration.refiners.insert(0, MergeWeekdayComponentRefiner());
14 | configuration.refiners.insert(0, ExtractTimezoneOffsetRefiner());
15 | configuration.refiners.insert(0, OverlapRemovalRefiner());
16 |
17 | // Unlike ExtractTimezoneOffsetRefiner, this refiner relies on knowing both date and time in cases where the tz
18 | // is ambiguous (in terms of DST/non-DST). It therefore needs to be applied as late as possible in the parsing.
19 | configuration.refiners.add(ExtractTimezoneAbbrRefiner());
20 | configuration.refiners.add(OverlapRemovalRefiner());
21 | configuration.refiners.add(ForwardDateRefiner());
22 | configuration.refiners.add(UnlikelyFormatFilter(strictMode));
23 | return configuration;
24 | }
25 |
--------------------------------------------------------------------------------
/lib/src/debugging.dart:
--------------------------------------------------------------------------------
1 | typedef AsyncDebugBlock = dynamic Function();
2 | typedef DebugConsume = void Function(AsyncDebugBlock debugLog);
3 |
4 | abstract class DebugHandler {
5 | void debug(AsyncDebugBlock debugLog);
6 | }
7 |
8 | class BufferedDebugHandler implements DebugHandler {
9 |
10 | List _buffer;
11 |
12 | BufferedDebugHandler(): _buffer = [], super();
13 |
14 | constructor() {
15 | _buffer = [];
16 | }
17 |
18 | @override
19 | void debug(AsyncDebugBlock debugMsg) {
20 | _buffer.add(debugMsg);
21 | }
22 |
23 | List executeBufferedBlocks() {
24 | final logs = _buffer.map((block) => block());
25 | _buffer = [];
26 | return logs.toList();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/lib/src/locales/en/configuration.dart:
--------------------------------------------------------------------------------
1 | import '../../chrono.dart' show Configuration;
2 |
3 | import './parsers/ENTimeUnitWithinFormatParser.dart';
4 | import './parsers/ENMonthNameLittleEndianParser.dart';
5 | import './parsers/ENMonthNameMiddleEndianParser.dart';
6 | import './parsers/ENMonthNameParser.dart';
7 | import './parsers/ENCasualYearMonthDayParser.dart';
8 | import './parsers/ENSlashMonthFormatParser.dart';
9 | import './parsers/ENTimeExpressionParser.dart';
10 | import './parsers/ENTimeUnitAgoFormatParser.dart';
11 | import './parsers/ENTimeUnitLaterFormatParser.dart';
12 | import './refiners/ENMergeDateRangeRefiner.dart';
13 | import './refiners/ENMergeDateTimeRefiner.dart';
14 |
15 | import '../../configurations.dart' show includeCommonConfiguration;
16 | import './parsers/ENCasualDateParser.dart';
17 | import './parsers/ENCasualTimeParser.dart';
18 | import './parsers/ENWeekdayParser.dart';
19 | import './parsers/ENRelativeDateFormatParser.dart';
20 |
21 | import '../../common/parsers/SlashDateFormatParser.dart';
22 | import './parsers/ENTimeUnitCasualRelativeFormatParser.dart';
23 | import './refiners/ENMergeRelativeDateRefiner.dart';
24 |
25 | class ENDefaultConfiguration {
26 | const ENDefaultConfiguration();
27 |
28 | /// Create a default *casual* {@Link Configuration} for English chrono.
29 | /// It calls {@Link createConfiguration} and includes additional parsers.
30 | Configuration createCasualConfiguration([ bool littleEndian = false ]) {
31 | final option = createConfiguration(false, littleEndian);
32 | option.parsers.insert(0, ENCasualDateParser());
33 | option.parsers.insert(0, ENCasualTimeParser());
34 | option.parsers.insert(0, ENMonthNameParser());
35 | option.parsers.insert(0, ENRelativeDateFormatParser());
36 | option.parsers.insert(0, ENTimeUnitCasualRelativeFormatParser());
37 | return option;
38 | }
39 |
40 | /// Create a default {@Link Configuration} for English chrono
41 | ///
42 | /// @param strictMode If the timeunit mentioning should be strict, not casual
43 | /// @param littleEndian If format should be date-first/littleEndian (e.g. en_UK), not month-first/middleEndian (e.g. en_US)
44 | Configuration createConfiguration([ bool strictMode = true, bool littleEndian = false ]) {
45 | final options = includeCommonConfiguration(
46 | Configuration(
47 | parsers: [
48 | SlashDateFormatParser(littleEndian),
49 | ENTimeUnitWithinFormatParser(strictMode),
50 | ENMonthNameLittleEndianParser(),
51 | ENMonthNameMiddleEndianParser(),
52 | ENWeekdayParser(),
53 | ENCasualYearMonthDayParser(),
54 | ENSlashMonthFormatParser(),
55 | ENTimeExpressionParser(strictMode),
56 | ENTimeUnitAgoFormatParser(strictMode),
57 | ENTimeUnitLaterFormatParser(strictMode),
58 | ],
59 | refiners: [ENMergeRelativeDateRefiner(), ENMergeDateTimeRefiner()],
60 | ),
61 | strictMode
62 | );
63 | // Re-apply the date time refiner again after the timezone refinement and exclusion in common refiners.
64 | options.refiners.add(ENMergeDateTimeRefiner());
65 | // Keep the date range refiner at the end (after all other refinements).
66 | options.refiners.add(ENMergeDateRangeRefiner());
67 | return options;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/lib/src/locales/en/constants.dart:
--------------------------------------------------------------------------------
1 | import '../../utils/pattern.dart' show matchAnyPattern, repeatedTimeunitPattern;
2 | import '../../calculation/years.dart' show findMostLikelyADYear;
3 | import '../../utils/timeunits.dart' show TimeUnits;
4 |
5 | const Map WEEKDAY_DICTIONARY = {
6 | 'sunday': 0,
7 | 'sun': 0,
8 | 'sun.': 0,
9 | 'monday': 1,
10 | 'mon': 1,
11 | 'mon.': 1,
12 | 'tuesday': 2,
13 | 'tue': 2,
14 | 'tue.': 2,
15 | 'wednesday': 3,
16 | 'wed': 3,
17 | 'wed.': 3,
18 | 'thursday': 4,
19 | 'thurs': 4,
20 | 'thurs.': 4,
21 | 'thur': 4,
22 | 'thur.': 4,
23 | 'thu': 4,
24 | 'thu.': 4,
25 | 'friday': 5,
26 | 'fri': 5,
27 | 'fri.': 5,
28 | 'saturday': 6,
29 | 'sat': 6,
30 | 'sat.': 6,
31 | };
32 |
33 | const Map FULL_MONTH_NAME_DICTIONARY = {
34 | 'january': 1,
35 | 'february': 2,
36 | 'march': 3,
37 | 'april': 4,
38 | 'may': 5,
39 | 'june': 6,
40 | 'july': 7,
41 | 'august': 8,
42 | 'september': 9,
43 | 'october': 10,
44 | 'november': 11,
45 | 'december': 12,
46 | };
47 |
48 | const Map MONTH_DICTIONARY = {
49 | ...FULL_MONTH_NAME_DICTIONARY,
50 | 'jan': 1,
51 | 'jan.': 1,
52 | 'feb': 2,
53 | 'feb.': 2,
54 | 'mar': 3,
55 | 'mar.': 3,
56 | 'apr': 4,
57 | 'apr.': 4,
58 | 'jun': 6,
59 | 'jun.': 6,
60 | 'jul': 7,
61 | 'jul.': 7,
62 | 'aug': 8,
63 | 'aug.': 8,
64 | 'sep': 9,
65 | 'sep.': 9,
66 | 'sept': 9,
67 | 'sept.': 9,
68 | 'oct': 10,
69 | 'oct.': 10,
70 | 'nov': 11,
71 | 'nov.': 11,
72 | 'dec': 12,
73 | 'dec.': 12,
74 | };
75 |
76 | const Map INTEGER_WORD_DICTIONARY = {
77 | 'one': 1,
78 | 'two': 2,
79 | 'three': 3,
80 | 'four': 4,
81 | 'five': 5,
82 | 'six': 6,
83 | 'seven': 7,
84 | 'eight': 8,
85 | 'nine': 9,
86 | 'ten': 10,
87 | 'eleven': 11,
88 | 'twelve': 12,
89 | };
90 |
91 | const Map ORDINAL_WORD_DICTIONARY = {
92 | 'first': 1,
93 | 'second': 2,
94 | 'third': 3,
95 | 'fourth': 4,
96 | 'fifth': 5,
97 | 'sixth': 6,
98 | 'seventh': 7,
99 | 'eighth': 8,
100 | 'ninth': 9,
101 | 'tenth': 10,
102 | 'eleventh': 11,
103 | 'twelfth': 12,
104 | 'thirteenth': 13,
105 | 'fourteenth': 14,
106 | 'fifteenth': 15,
107 | 'sixteenth': 16,
108 | 'seventeenth': 17,
109 | 'eighteenth': 18,
110 | 'nineteenth': 19,
111 | 'twentieth': 20,
112 | 'twenty first': 21,
113 | 'twenty-first': 21,
114 | 'twenty second': 22,
115 | 'twenty-second': 22,
116 | 'twenty third': 23,
117 | 'twenty-third': 23,
118 | 'twenty fourth': 24,
119 | 'twenty-fourth': 24,
120 | 'twenty fifth': 25,
121 | 'twenty-fifth': 25,
122 | 'twenty sixth': 26,
123 | 'twenty-sixth': 26,
124 | 'twenty seventh': 27,
125 | 'twenty-seventh': 27,
126 | 'twenty eighth': 28,
127 | 'twenty-eighth': 28,
128 | 'twenty ninth': 29,
129 | 'twenty-ninth': 29,
130 | 'thirtieth': 30,
131 | 'thirty first': 31,
132 | 'thirty-first': 31,
133 | };
134 |
135 | const Map TIME_UNIT_DICTIONARY_NO_ABBR = {
136 | 'second': 'second',
137 | 'seconds': 'second',
138 | 'minute': 'minute',
139 | 'minutes': 'minute',
140 | 'hour': 'hour',
141 | 'hours': 'hour',
142 | 'day': 'd',
143 | 'days': 'd',
144 | 'week': 'week',
145 | 'weeks': 'week',
146 | 'month': 'month',
147 | 'months': 'month',
148 | 'quarter': 'quarter',
149 | 'quarters': 'quarter',
150 | 'year': 'year',
151 | 'years': 'year',
152 | };
153 |
154 | final Map TIME_UNIT_DICTIONARY = {
155 | 's': "second",
156 | 'sec': "second",
157 | 'second': "second",
158 | 'seconds': "second",
159 | 'm': "minute",
160 | 'min': "minute",
161 | 'mins': "minute",
162 | 'minute': "minute",
163 | 'minutes': "minute",
164 | 'h': "hour",
165 | 'hr': "hour",
166 | 'hrs': "hour",
167 | 'hour': "hour",
168 | 'hours': "hour",
169 | 'd': "d",
170 | 'day': "d",
171 | 'days': "d",
172 | 'w': "w",
173 | 'week': "week",
174 | 'weeks': "week",
175 | 'mo': "month",
176 | 'mon': "month",
177 | 'mos': "month",
178 | 'month': "month",
179 | 'months': "month",
180 | 'qtr': "quarter",
181 | 'quarter': "quarter",
182 | 'quarters': "quarter",
183 | 'y': "year",
184 | 'yr': "year",
185 | 'year': "year",
186 | 'years': "year",
187 | // Also, merge the entries from the full-name dictionary.
188 | // We leave the duplicated entries for readability.
189 | ...TIME_UNIT_DICTIONARY_NO_ABBR,
190 | };
191 |
192 | //-----------------------------
193 |
194 | final NUMBER_PATTERN =
195 | "(?:${matchAnyPattern(INTEGER_WORD_DICTIONARY)}|[0-9]+|[0-9]+\\.[0-9]+|half(?:\\s{0,2}an?)?|an?\\b(?:\\s{0,2}few)?|few|several|the|a?\\s{0,2}couple\\s{0,2}(?:of)?)";
196 |
197 | double? parseNumberPattern(String match) {
198 | final nmb = match.toLowerCase();
199 | if (INTEGER_WORD_DICTIONARY[nmb] != null) {
200 | return INTEGER_WORD_DICTIONARY[nmb]!.toDouble();
201 | } else if (nmb == "a" || nmb == "an" || nmb == "the") {
202 | return 1;
203 | } else if (RegExp(r'few').hasMatch(nmb)) {
204 | return 3;
205 | } else if (RegExp(r'half').hasMatch(nmb)) {
206 | return 0.5;
207 | } else if (RegExp(r'couple').hasMatch(nmb)) {
208 | return 2;
209 | } else if (RegExp(r'several').hasMatch(nmb)) {
210 | return 7;
211 | }
212 |
213 | return double.tryParse(nmb);
214 | }
215 |
216 | //-----------------------------
217 |
218 | final ORDINAL_NUMBER_PATTERN =
219 | "(?:${matchAnyPattern(ORDINAL_WORD_DICTIONARY)}|[0-9]{1,2}(?:st|nd|rd|th)?)";
220 | int? parseOrdinalNumberPattern(String match) {
221 | var nmb = match.toLowerCase();
222 | if (ORDINAL_WORD_DICTIONARY[nmb] != null) {
223 | return ORDINAL_WORD_DICTIONARY[nmb];
224 | }
225 |
226 | nmb = nmb.replaceFirst(RegExp(r'(?:st|nd|rd|th)$'), "");
227 | return int.tryParse(nmb);
228 | }
229 |
230 | //-----------------------------
231 |
232 | const YEAR_PATTERN =
233 | "(?:[1-9][0-9]{0,3}\\s{0,2}(?:BE|AD|BC|BCE|CE)|[1-2][0-9]{3}|[5-9][0-9])";
234 | int? parseYear(String match) {
235 | if (RegExp(r'BE', caseSensitive: true).hasMatch(match)) {
236 | // Buddhist Era
237 | match = match.replaceFirst(RegExp(r'BE', caseSensitive: true), "");
238 | return (int.tryParse(match) ?? 0) - 543;
239 | }
240 |
241 | if (RegExp(r'BCE?', caseSensitive: true).hasMatch(match)) {
242 | // Before Christ, Before Common Era
243 | match = match.replaceFirst(RegExp(r'BCE?', caseSensitive: true), "");
244 | return -(int.tryParse(match) ?? 0);
245 | }
246 |
247 | if (RegExp(r'(AD|CE)', caseSensitive: true).hasMatch(match)) {
248 | // Anno Domini, Common Era
249 | match = match.replaceFirst(RegExp(r'(AD|CE)', caseSensitive: true), "");
250 | return int.tryParse(match) ?? 0;
251 | }
252 |
253 | final rawYearNumber = int.tryParse(match)!;
254 | return findMostLikelyADYear(rawYearNumber);
255 | }
256 |
257 | //-----------------------------
258 |
259 | final SINGLE_TIME_UNIT_PATTERN =
260 | "($NUMBER_PATTERN)\\s{0,3}(${matchAnyPattern(TIME_UNIT_DICTIONARY)})";
261 | final SINGLE_TIME_UNIT_REGEX =
262 | RegExp(SINGLE_TIME_UNIT_PATTERN, caseSensitive: false);
263 |
264 | final SINGLE_TIME_UNIT_NO_ABBR_PATTERN =
265 | "($NUMBER_PATTERN)\\s{0,3}(${matchAnyPattern(TIME_UNIT_DICTIONARY_NO_ABBR)})";
266 |
267 | final TIME_UNITS_PATTERN = repeatedTimeunitPattern(
268 | "(?:(?:about|around)\\s{0,3})?", SINGLE_TIME_UNIT_PATTERN);
269 | final TIME_UNITS_NO_ABBR_PATTERN = repeatedTimeunitPattern(
270 | "(?:(?:about|around)\\s{0,3})?", SINGLE_TIME_UNIT_NO_ABBR_PATTERN);
271 |
272 | TimeUnits parseTimeUnits(String timeunitText) {
273 | final Map fragments = {};
274 | var remainingText = timeunitText;
275 | var match = SINGLE_TIME_UNIT_REGEX.firstMatch(remainingText);
276 | while (match != null) {
277 | collectDateTimeFragment(fragments, match);
278 | remainingText = remainingText.substring(match[0]!.length).trim();
279 | match = SINGLE_TIME_UNIT_REGEX.firstMatch(remainingText);
280 | }
281 | return fragments;
282 | }
283 |
284 | void collectDateTimeFragment(Map fragments, RegExpMatch match) {
285 | final num = parseNumberPattern(match[1]!);
286 | final unit = TIME_UNIT_DICTIONARY[match[2]!.toLowerCase()];
287 | fragments[unit!] = num!;
288 | }
289 |
--------------------------------------------------------------------------------
/lib/src/locales/en/en.dart:
--------------------------------------------------------------------------------
1 | /// Chrono components for English support (*parsers*, *refiners*, and *configuration*)
2 | ///
3 | /// @module
4 |
5 | import '../../../chrono_dart.dart' show ChronoInstance;
6 | import '../../types.dart' show ParsedResult, ParsingOption;
7 |
8 | import './configuration.dart';
9 |
10 | final enConfig = ENDefaultConfiguration();
11 |
12 | /// Chrono object configured for parsing *casual* English
13 | final casual = ChronoInstance(enConfig.createCasualConfiguration(false));
14 |
15 | /// ChronoInstance object configured for parsing *strict* English
16 | final strict = ChronoInstance(enConfig.createConfiguration(true, false));
17 |
18 | /// ChronoInstance object configured for parsing *UK-style* English
19 | final GB = ChronoInstance(enConfig.createConfiguration(false, true));
20 |
21 | /// A shortcut for en.casual.parse()
22 | List parse(String text, [DateTime? ref, ParsingOption? option]) {
23 | return casual.parse(text, ref, option);
24 | }
25 |
26 | /// A shortcut for en.casual.parseDate()
27 | DateTime? parseDate(String text, DateTime ref, ParsingOption option) {
28 | return casual.parseDate(text, ref, option);
29 | }
30 |
--------------------------------------------------------------------------------
/lib/src/locales/en/parsers/ENCasualDateParser.dart:
--------------------------------------------------------------------------------
1 | import 'package:day/day.dart' as dayjs;
2 | import '../../../chrono.dart' show ParsingContext;
3 | import '../../../types.dart' show RegExpChronoMatch, Component;
4 | import '../../../common/parsers/AbstractParserWithWordBoundary.dart';
5 | import '../../../utils/day.dart' show assignSimilarDate;
6 | import '../../../common/casual_references.dart' as references;
7 |
8 | final _pattern = RegExp(
9 | r'(now|today|tonight|tomorrow|tmr|tmrw|yesterday|last\s*night)(?=\W|$)',
10 | caseSensitive: false);
11 |
12 | class ENCasualDateParser extends AbstractParserWithWordBoundaryChecking {
13 | @override
14 | RegExp innerPattern(ParsingContext context) {
15 | return _pattern;
16 | }
17 |
18 | /// @returns ParsingComponents | ParsingResult
19 | @override
20 | innerExtract(ParsingContext context, RegExpChronoMatch match) {
21 | var targetDate = dayjs.Day.fromDateTime(context.reference.instant);
22 | final lowerText = match[0]!.toLowerCase();
23 | var component = context.createParsingComponents();
24 |
25 | switch (lowerText) {
26 | case "now":
27 | component = references.now(context.reference);
28 | break;
29 |
30 | case "today":
31 | component = references.today(context.reference);
32 | break;
33 |
34 | case "yesterday":
35 | component = references.yesterday(context.reference);
36 | break;
37 |
38 | case "tomorrow":
39 | case "tmr":
40 | case "tmrw":
41 | component = references.tomorrow(context.reference);
42 | break;
43 |
44 | case "tonight":
45 | component = references.tonight(context.reference);
46 | break;
47 |
48 | default:
49 | if (RegExp(r'last\s*night').hasMatch(lowerText)) {
50 | if (targetDate.hour() > 6) {
51 | targetDate = targetDate.add(-1, 'd')!;
52 | }
53 |
54 | assignSimilarDate(component, targetDate);
55 | component.imply(Component.hour, 0);
56 | }
57 | break;
58 | }
59 | component.addTag("parser/ENCasualDateParser");
60 | return component;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/lib/src/locales/en/parsers/ENCasualTimeParser.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: library_prefixes
2 | import '../../../results.dart' show ParsingComponents;
3 | import '../../../chrono.dart' show ParsingContext;
4 | import '../../../types.dart' show RegExpChronoMatch;
5 | import '../../../common/parsers/AbstractParserWithWordBoundary.dart';
6 | import '../../../common/casual_references.dart' as casualReferences;
7 |
8 | final _pattern = RegExp(
9 | r'(?:this)?\s{0,3}(morning|afternoon|evening|night|midnight|midday|noon)(?=\W|$)',
10 | caseSensitive: false);
11 |
12 | class ENCasualTimeParser extends AbstractParserWithWordBoundaryChecking {
13 | @override
14 | innerPattern(context) {
15 | return _pattern;
16 | }
17 |
18 | @override
19 | innerExtract(ParsingContext context, RegExpChronoMatch match) {
20 | ParsingComponents? component;
21 | switch (match[1]!.toLowerCase()) {
22 | case "afternoon":
23 | component = casualReferences.afternoon(context.reference);
24 | break;
25 | case "evening":
26 | case "night":
27 | component = casualReferences.evening(context.reference);
28 | break;
29 | case "midnight":
30 | component = casualReferences.midnight(context.reference);
31 | break;
32 | case "morning":
33 | component = casualReferences.morning(context.reference);
34 | break;
35 | case "noon":
36 | case "midday":
37 | component = casualReferences.noon(context.reference);
38 | break;
39 | }
40 | if (component != null) {
41 | component.addTag("parser/ENCasualTimeParser");
42 | }
43 | return component;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/lib/src/locales/en/parsers/ENCasualYearMonthDayParser.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_interpolation_to_compose_strings, constant_identifier_names
2 | import '../../../chrono.dart' show ParsingContext;
3 | import '../../../types.dart' show RegExpChronoMatch, Component;
4 | import '../constants.dart' show MONTH_DICTIONARY;
5 | import '../../../utils/pattern.dart' show matchAnyPattern;
6 | import '../../../common/parsers/AbstractParserWithWordBoundary.dart';
7 |
8 | /*
9 | Date format with slash "/" between numbers like ENSlashDateFormatParser,
10 | but this parser expect year before month and date.
11 | - YYYY/MM/DD
12 | - YYYY-MM-DD
13 | - YYYY.MM.DD
14 | */
15 | final _pattern = RegExp(
16 | // ignore: prefer_adjacent_string_concatenation
17 | "([0-9]{4})[\\.\\/\\s]" +
18 | "(?:(${matchAnyPattern(MONTH_DICTIONARY)})|([0-9]{1,2}))[\\.\\/\\s]" +
19 | "([0-9]{1,2})" +
20 | "(?=\\W|\$)",
21 | caseSensitive: false);
22 |
23 | const _YEAR_NUMBER_GROUP = 1;
24 | const _MONTH_NAME_GROUP = 2;
25 | const _MONTH_NUMBER_GROUP = 3;
26 | const _DATE_NUMBER_GROUP = 4;
27 |
28 | class ENCasualYearMonthDayParser
29 | extends AbstractParserWithWordBoundaryChecking {
30 | @override
31 | RegExp innerPattern(context) {
32 | return _pattern;
33 | }
34 |
35 | @override
36 | Map? innerExtract(
37 | ParsingContext context, RegExpChronoMatch match) {
38 | final month = match[_MONTH_NUMBER_GROUP] != null
39 | ? int.parse(match[_MONTH_NUMBER_GROUP]!)
40 | : MONTH_DICTIONARY[match[_MONTH_NAME_GROUP]!.toLowerCase()]!;
41 |
42 | if (month < 1 || month > 12) {
43 | return null;
44 | }
45 |
46 | final year = int.parse(match[_YEAR_NUMBER_GROUP]!);
47 | final day = int.parse(match[_DATE_NUMBER_GROUP]!);
48 |
49 | return {
50 | Component.day: day,
51 | Component.month: month,
52 | Component.year: year,
53 | };
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/lib/src/locales/en/parsers/ENMonthNameLittleEndianParser.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_interpolation_to_compose_strings, constant_identifier_names
2 | import '../../../results.dart' show ParsingResult;
3 | import '../../../chrono.dart' show ParsingContext;
4 | import '../../../types.dart' show RegExpChronoMatch, Component;
5 | import '../../../calculation/years.dart' show findYearClosestToRef;
6 | import '../constants.dart' show MONTH_DICTIONARY, YEAR_PATTERN, parseYear, ORDINAL_NUMBER_PATTERN, parseOrdinalNumberPattern;
7 | import '../../../utils/pattern.dart' show matchAnyPattern;
8 | import '../../../common/parsers/AbstractParserWithWordBoundary.dart';
9 |
10 | // prettier-ignore
11 | final _pattern = RegExp(
12 | // ignore: prefer_adjacent_string_concatenation
13 | "(?:on\\s{0,3})?" +
14 | "($ORDINAL_NUMBER_PATTERN)" +
15 | "(?:" +
16 | "\\s{0,3}(?:to|\\-|\\–|until|through|till)?\\s{0,3}" +
17 | "($ORDINAL_NUMBER_PATTERN)" +
18 | ")?" +
19 | "(?:-|/|\\s{0,3}(?:of)?\\s{0,3})" +
20 | "(${matchAnyPattern(MONTH_DICTIONARY)})" +
21 | "(?:" +
22 | "(?:-|/|,?\\s{0,3})" +
23 | "($YEAR_PATTERN(?![^\\s]\\d))" +
24 | ")?" +
25 | "(?=\\W|\$)",
26 | caseSensitive: false
27 | );
28 |
29 | const _DATE_GROUP = 1;
30 | const _DATE_TO_GROUP = 2;
31 | const _MONTH_NAME_GROUP = 3;
32 | const _YEAR_GROUP = 4;
33 |
34 | class ENMonthNameLittleEndianParser extends AbstractParserWithWordBoundaryChecking {
35 | @override
36 | RegExp innerPattern(context) {
37 | return _pattern;
38 | }
39 |
40 | @override
41 | ParsingResult? innerExtract(ParsingContext context, RegExpChronoMatch match) {
42 | final result = context.createParsingResult(match.index, match[0]);
43 |
44 | final month = MONTH_DICTIONARY[match[_MONTH_NAME_GROUP]!.toLowerCase()]!;
45 | final day = parseOrdinalNumberPattern(match[_DATE_GROUP]!)!;
46 | if (day > 31) {
47 | // e.g. "[96 Aug]" => "9[6 Aug]", we need to shift away from the next number
48 | match.index = match.index + match[_DATE_GROUP]!.length;
49 | return null;
50 | }
51 |
52 | result.start.assign(Component.month, month);
53 | result.start.assign(Component.day, day);
54 |
55 | if (match[_YEAR_GROUP] != null) {
56 | final yearNumber = parseYear(match[_YEAR_GROUP]!)!;
57 | result.start.assign(Component.year, yearNumber);
58 | } else {
59 | final year = findYearClosestToRef(context.reference.instant, day, month);
60 | result.start.imply(Component.year, year);
61 | }
62 |
63 | if (match[_DATE_TO_GROUP] != null) {
64 | final endDate = parseOrdinalNumberPattern(match[_DATE_TO_GROUP]!)!;
65 |
66 | result.end = result.start.clone();
67 | result.end!.assign(Component.day, endDate);
68 | }
69 |
70 | return result;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/lib/src/locales/en/parsers/ENMonthNameMiddleEndianParser.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_interpolation_to_compose_strings, constant_identifier_names, slash_for_doc_comments
2 | import '../../../chrono.dart' show ParsingContext;
3 | import '../../../types.dart' show RegExpChronoMatch, Component;
4 | import '../../../calculation/years.dart' show findYearClosestToRef;
5 | import '../constants.dart'
6 | show
7 | MONTH_DICTIONARY,
8 | YEAR_PATTERN,
9 | parseYear,
10 | ORDINAL_NUMBER_PATTERN,
11 | parseOrdinalNumberPattern;
12 | import '../../../utils/pattern.dart' show matchAnyPattern;
13 | import '../../../common/parsers/AbstractParserWithWordBoundary.dart';
14 |
15 | final _pattern = RegExp(
16 | "(${matchAnyPattern(MONTH_DICTIONARY)})" +
17 | "(?:-|/|\\s*,?\\s*)" +
18 | "($ORDINAL_NUMBER_PATTERN)(?!\\s*(?:am|pm))\\s*" +
19 | "(?:" +
20 | "(?:to|\\-)\\s*" +
21 | "($ORDINAL_NUMBER_PATTERN)\\s*" +
22 | ")?" +
23 | "(?:" +
24 | "(?:-|/|\\s*,?\\s*)" +
25 | "($YEAR_PATTERN)" +
26 | ")?" +
27 | "(?=\\W|\$)(?!\\:\\d)",
28 | caseSensitive: false);
29 |
30 | const _MONTH_NAME_GROUP = 1;
31 | const _DATE_GROUP = 2;
32 | const _DATE_TO_GROUP = 3;
33 | const _YEAR_GROUP = 4;
34 |
35 | /**
36 | * The parser for parsing US's date format that begin with month's name.
37 | * - January 13
38 | * - January 13, 2012
39 | * - January 13 - 15, 2012
40 | * Note: Watch out for:
41 | * - January 12:00
42 | * - January 12.44
43 | * - January 1222344
44 | */
45 | class ENMonthNameMiddleEndianParser
46 | extends AbstractParserWithWordBoundaryChecking {
47 | @override
48 | RegExp innerPattern(context) {
49 | return _pattern;
50 | }
51 |
52 | @override
53 | innerExtract(ParsingContext context, RegExpChronoMatch match) {
54 | final mStr = match[_MONTH_NAME_GROUP]!.toLowerCase();
55 | final month = MONTH_DICTIONARY[mStr]!;
56 | final day = parseOrdinalNumberPattern(match[_DATE_GROUP]!)!;
57 | if (day > 31) {
58 | return null;
59 | }
60 |
61 | final components = context.createParsingComponents({
62 | Component.day: day,
63 | Component.month: month,
64 | });
65 |
66 | if (match[_YEAR_GROUP] != null) {
67 | final year = parseYear(match[_YEAR_GROUP]!)!;
68 | components.assign(Component.year, year);
69 | } else {
70 | final year = findYearClosestToRef(context.reference.instant, day, month);
71 | components.imply(Component.year, year);
72 | }
73 |
74 | if (match[_DATE_TO_GROUP] == null) {
75 | return components;
76 | }
77 |
78 | // Text can be 'range' value. Such as 'January 12 - 13, 2012'
79 | final endDate = parseOrdinalNumberPattern(match[_DATE_TO_GROUP]!)!;
80 | final result = context.createParsingResult(match.index, match[0]);
81 | result.start = components;
82 | result.end = components.clone();
83 | result.end!.assign(Component.day, endDate);
84 |
85 | return result;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/lib/src/locales/en/parsers/ENMonthNameParser.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_interpolation_to_compose_strings, constant_identifier_names, slash_for_doc_comments
2 | import '../../../chrono.dart' show ParsingContext;
3 | import '../../../types.dart' show RegExpChronoMatch, Component;
4 | import '../../../calculation/years.dart' show findYearClosestToRef;
5 | import '../constants.dart'
6 | show MONTH_DICTIONARY, YEAR_PATTERN, parseYear, FULL_MONTH_NAME_DICTIONARY;
7 | import '../../../utils/pattern.dart' show matchAnyPattern;
8 | import '../../../common/parsers/AbstractParserWithWordBoundary.dart';
9 |
10 | final _pattern = RegExp(
11 | // ignore: prefer_adjacent_string_concatenation
12 | "((?:in)\\s*)?" +
13 | "(${matchAnyPattern(MONTH_DICTIONARY)})" +
14 | "\\s*" +
15 | "(?:" +
16 | "[,-]?\\s*($YEAR_PATTERN)?" +
17 | ")?" +
18 | "(?=[^\\s\\w]|\\s+[^0-9]|\\s+\$|\$)",
19 | caseSensitive: false);
20 |
21 | const _PREFIX_GROUP = 1;
22 | const _MONTH_NAME_GROUP = 2;
23 | const _YEAR_GROUP = 3;
24 |
25 | /**
26 | * The parser for parsing month name and year.
27 | * - January, 2012
28 | * - January 2012
29 | * - January
30 | * (in) Jan
31 | */
32 | class ENMonthNameParser extends AbstractParserWithWordBoundaryChecking {
33 | @override
34 | RegExp innerPattern(context) {
35 | return _pattern;
36 | }
37 |
38 | @override
39 | innerExtract(ParsingContext context, RegExpChronoMatch match) {
40 | final monthName = match[_MONTH_NAME_GROUP]!.toLowerCase();
41 |
42 | // skip some unlikely words "jan", "mar", ..
43 | if (match[0]!.length <= 3 &&
44 | FULL_MONTH_NAME_DICTIONARY[monthName] == null) {
45 | return null;
46 | }
47 |
48 | final result = context.createParsingResult(
49 | match.index + (match[_PREFIX_GROUP] ?? "").length,
50 | match.index + match[0]!.length);
51 | result.start.imply(Component.day, 1);
52 |
53 | final month = MONTH_DICTIONARY[monthName]!;
54 | result.start.assign(Component.month, month);
55 |
56 | if (match[_YEAR_GROUP] != null) {
57 | final year = parseYear(match[_YEAR_GROUP]!)!;
58 | result.start.assign(Component.year, year);
59 | } else {
60 | final year = findYearClosestToRef(context.reference.instant, 1, month);
61 | result.start.imply(Component.year, year);
62 | }
63 |
64 | return result;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/lib/src/locales/en/parsers/ENRelativeDateFormatParser.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: constant_identifier_names
2 | import 'package:day/day.dart' as dayjs;
3 | import '../../../chrono.dart' show ParsingContext;
4 | import '../../../types.dart' show RegExpChronoMatch, Component;
5 | import '../constants.dart'
6 | show TIME_UNIT_DICTIONARY;
7 | import '../../../results.dart' show ParsingComponents;
8 | import '../../../common/parsers/AbstractParserWithWordBoundary.dart';
9 | import '../../../utils/pattern.dart' show matchAnyPattern;
10 |
11 | final _pattern = RegExp(
12 | "(this|last|past|next|after\\s*this)\\s*(${matchAnyPattern(TIME_UNIT_DICTIONARY)})(?=\\s*)(?=\\W|\$)",
13 | caseSensitive: false,
14 | );
15 |
16 | const _MODIFIER_WORD_GROUP = 1;
17 | const _RELATIVE_WORD_GROUP = 2;
18 |
19 | class ENRelativeDateFormatParser extends AbstractParserWithWordBoundaryChecking {
20 | @override
21 | RegExp innerPattern(context) {
22 | return _pattern;
23 | }
24 |
25 | @override
26 | ParsingComponents innerExtract(ParsingContext context, RegExpChronoMatch match) {
27 | final modifier = match[_MODIFIER_WORD_GROUP]!.toLowerCase();
28 | final unitWord = match[_RELATIVE_WORD_GROUP]!.toLowerCase();
29 | final timeunit = TIME_UNIT_DICTIONARY[unitWord]!;
30 |
31 | if (modifier == "next" || modifier.startsWith("after")) {
32 | final Map timeUnits = {};
33 | timeUnits[timeunit] = 1;
34 | return ParsingComponents.createRelativeFromReference(context.reference, timeUnits);
35 | }
36 |
37 | if (modifier == "last" || modifier == "past") {
38 | final Map timeUnits = {};
39 | timeUnits[timeunit] = -1;
40 | return ParsingComponents.createRelativeFromReference(context.reference, timeUnits);
41 | }
42 |
43 | final components = context.createParsingComponents();
44 | var date = dayjs.Day.fromDateTime(context.reference.instant);
45 |
46 | // This week
47 | if (RegExp(r'week', caseSensitive: false).hasMatch(unitWord)) {
48 | date = date.add(-date.get("d")!, "d")!;
49 | components.imply(Component.day, date.date());
50 | components.imply(Component.month, date.month());
51 | components.imply(Component.year, date.year());
52 | }
53 |
54 | // This month
55 | else if (RegExp(r'month', caseSensitive: false).hasMatch(unitWord)) {
56 | date = date.add(-date.date() + 1, "d")!;
57 | components.imply(Component.day, date.date());
58 | components.assign(Component.year, date.year());
59 | components.assign(Component.month, date.month());
60 | }
61 |
62 | // This year
63 | else if (RegExp(r'year', caseSensitive: false).hasMatch(unitWord)) {
64 | date = date.add(-date.date() + 1, "d")!;
65 | date = date.add(-date.month(), "month")!;
66 |
67 | components.imply(Component.day, date.date());
68 | components.imply(Component.month, date.month());
69 | components.assign(Component.year, date.year());
70 | }
71 |
72 | return components;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/src/locales/en/parsers/ENSlashMonthFormatParser.dart:
--------------------------------------------------------------------------------
1 | import '../../../chrono.dart' show ParsingContext;
2 | import '../../../results.dart' show ParsingComponents;
3 | import '../../../types.dart' show Component, RegExpChronoMatch;
4 | import '../../../common/parsers/AbstractParserWithWordBoundary.dart';
5 |
6 | final _PATTERN = RegExp(r'([0-9]|0[1-9]|1[012])/([0-9]{4})');
7 |
8 | const _MONTH_GROUP = 1;
9 | const _YEAR_GROUP = 2;
10 |
11 | /// Month/Year date format with slash "/" (also "-" and ".") between numbers
12 | /// - 11/05
13 | /// - 06/2005
14 | class ENSlashMonthFormatParser extends AbstractParserWithWordBoundaryChecking {
15 | @override
16 | RegExp innerPattern(context) {
17 | return _PATTERN;
18 | }
19 |
20 | @override
21 | ParsingComponents innerExtract(ParsingContext context, RegExpChronoMatch match) {
22 | final year = int.tryParse(match[_YEAR_GROUP]!)!;
23 | final month = int.tryParse(match[_MONTH_GROUP]!)!;
24 |
25 | return context
26 | .createParsingComponents()
27 | .imply(Component.day, 1)
28 | .assign(Component.month, month)
29 | .assign(Component.year, year);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/src/locales/en/parsers/ENTimeExpressionParser.dart:
--------------------------------------------------------------------------------
1 | import '../../../chrono.dart' show ParsingContext;
2 | import '../../../results.dart' show ParsingComponents;
3 | import '../../../types.dart' show Meridiem, Component;
4 | import '../../../common/parsers/AbstractTimeExpressionParser.dart'
5 | show AbstractTimeExpressionParser;
6 |
7 | class ENTimeExpressionParser extends AbstractTimeExpressionParser {
8 | ENTimeExpressionParser(strictMode) : super(strictMode);
9 |
10 | @override
11 | String followingPhase() {
12 | return "\\s*(?:\\-|\\–|\\~|\\〜|to|until|through|till|\\?)\\s*";
13 | }
14 |
15 | @override
16 | String primaryPrefix() {
17 | return "(?:(?:at|from)\\s*)??";
18 | }
19 |
20 | @override
21 | String primarySuffix() {
22 | return "(?:\\s*(?:o\\W*clock|at\\s*night|in\\s*the\\s*(?:morning|afternoon)))?(?!/)(?=\\W|\$)";
23 | }
24 |
25 | @override
26 | ParsingComponents? extractPrimaryTimeComponents(
27 | ParsingContext context, RegExpMatch match,
28 | [bool strict = false]) {
29 | final components =
30 | super.extractPrimaryTimeComponents(context, match, strict);
31 | if (components == null) {
32 | return components;
33 | }
34 |
35 | if (match[0]!.endsWith("night")) {
36 | final hour = components.get(Component.hour)!;
37 | if (hour >= 6 && hour < 12) {
38 | components.assign(Component.hour, components.get(Component.hour)! + 12);
39 | components.assign(Component.meridiem, Meridiem.PM.id);
40 | } else if (hour < 6) {
41 | components.assign(Component.meridiem, Meridiem.AM.id);
42 | }
43 | }
44 |
45 | if (match[0]!.endsWith("afternoon")) {
46 | components.assign(Component.meridiem, Meridiem.PM.id);
47 | final hour = components.get(Component.hour)!;
48 | if (hour >= 0 && hour <= 6) {
49 | components.assign(Component.hour, components.get(Component.hour)! + 12);
50 | }
51 | }
52 |
53 | if (match[0]!.endsWith("morning")) {
54 | components.assign(Component.meridiem, Meridiem.AM.id);
55 | final hour = components.get(Component.hour)!;
56 | if (hour < 12) {
57 | components.assign(Component.hour, components.get(Component.hour)!);
58 | }
59 | }
60 |
61 | return components.addTag("parser/ENTimeExpressionParser");
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/lib/src/locales/en/parsers/ENTimeUnitAgoFormatParser.dart:
--------------------------------------------------------------------------------
1 | import '../../../chrono.dart' show ParsingContext;
2 | import '../../../types.dart' show RegExpChronoMatch;
3 | import '../constants.dart'
4 | show parseTimeUnits, TIME_UNITS_NO_ABBR_PATTERN, TIME_UNITS_PATTERN;
5 | import '../../../results.dart' show ParsingComponents;
6 | import '../../../common/parsers/AbstractParserWithWordBoundary.dart';
7 | import "../../../utils/timeunits.dart" show reverseTimeUnits;
8 |
9 | final _pattern = RegExp(
10 | "($TIME_UNITS_PATTERN)\\s{0,5}(?:ago|before|earlier)(?=\\W|\$)",
11 | caseSensitive: false);
12 | final _strictPattern = RegExp(
13 | "($TIME_UNITS_NO_ABBR_PATTERN)\\s{0,5}(?:ago|before|earlier)(?=\\W|\$)",
14 | caseSensitive: false);
15 |
16 | class ENTimeUnitAgoFormatParser extends AbstractParserWithWordBoundaryChecking {
17 | final bool _strictMode;
18 |
19 | ENTimeUnitAgoFormatParser(bool strictMode)
20 | : _strictMode = strictMode,
21 | super();
22 |
23 | @override
24 | RegExp innerPattern(context) {
25 | return _strictMode ? _strictPattern : _pattern;
26 | }
27 |
28 | @override
29 | innerExtract(ParsingContext context, RegExpChronoMatch match) {
30 | final timeUnits = parseTimeUnits(match[1]!);
31 | final outputTimeUnits = reverseTimeUnits(timeUnits);
32 |
33 | return ParsingComponents.createRelativeFromReference(
34 | context.reference, outputTimeUnits);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/src/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.dart:
--------------------------------------------------------------------------------
1 | import '../../../chrono.dart' show ParsingContext;
2 | import '../../../types.dart' show RegExpChronoMatch;
3 | import '../constants.dart'
4 | show TIME_UNITS_PATTERN, parseTimeUnits, TIME_UNITS_NO_ABBR_PATTERN;
5 | import '../../../results.dart' show ParsingComponents;
6 | import '../../../common/parsers/AbstractParserWithWordBoundary.dart';
7 | import "../../../utils/timeunits.dart" show reverseTimeUnits;
8 |
9 | final _pattern = RegExp(
10 | "(this|last|past|next|after|\\+|-)\\s*($TIME_UNITS_PATTERN)(?=\\W|\$)",
11 | caseSensitive: false);
12 | final _patternNoAbbr = RegExp(
13 | "(this|last|past|next|after|\\+|-)\\s*($TIME_UNITS_NO_ABBR_PATTERN)(?=\\W|\$)",
14 | caseSensitive: false,
15 | );
16 |
17 | class ENTimeUnitCasualRelativeFormatParser
18 | extends AbstractParserWithWordBoundaryChecking {
19 | final bool allowAbbreviations;
20 |
21 | ENTimeUnitCasualRelativeFormatParser([this.allowAbbreviations = true])
22 | : super();
23 |
24 | @override
25 | RegExp innerPattern(context) {
26 | return allowAbbreviations ? _pattern : _patternNoAbbr;
27 | }
28 |
29 | @override
30 | ParsingComponents innerExtract(
31 | ParsingContext context, RegExpChronoMatch match) {
32 | final prefix = match[1]!.toLowerCase();
33 | var timeUnits = parseTimeUnits(match[2]!);
34 | switch (prefix) {
35 | case "last":
36 | case "past":
37 | case "-":
38 | timeUnits = reverseTimeUnits(timeUnits);
39 | break;
40 | }
41 |
42 | return ParsingComponents.createRelativeFromReference(
43 | context.reference, timeUnits);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/lib/src/locales/en/parsers/ENTimeUnitLaterFormatParser.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: constant_identifier_names
2 | import '../../../chrono.dart' show ParsingContext;
3 | import '../../../types.dart' show RegExpChronoMatch;
4 | import '../constants.dart'
5 | show parseTimeUnits, TIME_UNITS_NO_ABBR_PATTERN, TIME_UNITS_PATTERN;
6 | import '../../../results.dart' show ParsingComponents;
7 | import '../../../common/parsers/AbstractParserWithWordBoundary.dart';
8 |
9 | final _pattern = RegExp(
10 | "($TIME_UNITS_PATTERN)\\s{0,5}(?:later|after|from now|henceforth|forward|out)(?=(?:\\W|\$))",
11 | caseSensitive: false,
12 | );
13 |
14 | final _strictPattern = RegExp(
15 | "($TIME_UNITS_NO_ABBR_PATTERN)(later|from now)(?=(?:\\W|\$))",
16 | caseSensitive: false,
17 | );
18 | const GROUP_NUM_TIMEUNITS = 1;
19 |
20 | class ENTimeUnitLaterFormatParser
21 | extends AbstractParserWithWordBoundaryChecking {
22 | final bool _strictMode;
23 |
24 | ENTimeUnitLaterFormatParser(bool strictMode)
25 | : _strictMode = strictMode,
26 | super();
27 |
28 | @override
29 | RegExp innerPattern(context) {
30 | return _strictMode ? _strictPattern : _pattern;
31 | }
32 |
33 | @override
34 | innerExtract(ParsingContext context, RegExpChronoMatch match) {
35 | final fragments = parseTimeUnits(match[GROUP_NUM_TIMEUNITS]!);
36 | return ParsingComponents.createRelativeFromReference(
37 | context.reference, fragments);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/src/locales/en/parsers/ENTimeUnitWithinFormatParser.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_adjacent_string_concatenation, non_constant_identifier_names
2 | import '../constants.dart'
3 | show TIME_UNITS_PATTERN, parseTimeUnits, TIME_UNITS_NO_ABBR_PATTERN;
4 | import '../../../chrono.dart' show ParsingContext;
5 | import '../../../results.dart' show ParsingComponents;
6 | import '../../../types.dart' show RegExpChronoMatch;
7 | import '../../../common/parsers/AbstractParserWithWordBoundary.dart';
8 |
9 | final PATTERN_WITH_OPTIONAL_PREFIX = RegExp(
10 | "(?:(?:within|in|for)\\s*)?" +
11 | "(?:(?:about|around|roughly|approximately|just)\\s*(?:~\\s*)?)?($TIME_UNITS_PATTERN)(?=\\W|\$)",
12 | caseSensitive: false);
13 |
14 | final PATTERN_WITH_PREFIX = RegExp(
15 | "(?:within|in|for)\\s*" +
16 | "(?:(?:about|around|roughly|approximately|just)\\s*(?:~\\s*)?)?($TIME_UNITS_PATTERN)(?=\\W|\$)",
17 | caseSensitive: false);
18 |
19 | final PATTERN_WITH_PREFIX_STRICT = RegExp(
20 | "(?:within|in|for)\\s*" +
21 | "(?:(?:about|around|roughly|approximately|just)\\s*(?:~\\s*)?)?($TIME_UNITS_NO_ABBR_PATTERN)(?=\\W|\$)",
22 | caseSensitive: false);
23 |
24 | class ENTimeUnitWithinFormatParser
25 | extends AbstractParserWithWordBoundaryChecking {
26 | final bool _strictMode;
27 |
28 | ENTimeUnitWithinFormatParser(bool strictMode)
29 | : _strictMode = strictMode,
30 | super();
31 |
32 | @override
33 | RegExp innerPattern(ParsingContext context) {
34 | if (_strictMode) {
35 | return PATTERN_WITH_PREFIX_STRICT;
36 | }
37 | return context.option.forwardDate != null
38 | ? PATTERN_WITH_OPTIONAL_PREFIX
39 | : PATTERN_WITH_PREFIX;
40 | }
41 |
42 | @override
43 | ParsingComponents innerExtract(
44 | ParsingContext context, RegExpChronoMatch match) {
45 | final timeUnits = parseTimeUnits(match[1]!);
46 | return ParsingComponents.createRelativeFromReference(
47 | context.reference, timeUnits);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/lib/src/locales/en/parsers/ENWeekdayParser.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: constant_identifier_names
2 | import '../../../types.dart';
3 | import '../../../chrono.dart' show ParsingContext;
4 | import '../../../results.dart' show ParsingComponents;
5 | import '../constants.dart' show WEEKDAY_DICTIONARY;
6 | import '../../../utils/pattern.dart' show matchAnyPattern;
7 | import '../../../common/parsers/AbstractParserWithWordBoundary.dart'
8 | show AbstractParserWithWordBoundaryChecking;
9 | import '../../../common/calculation/weekdays.dart'
10 | show createParsingComponentsAtWeekday;
11 |
12 | final _pattern = RegExp(
13 | // ignore: prefer_interpolation_to_compose_strings, prefer_adjacent_string_concatenation
14 | "(?:(?:\\,|\\(|\\()\\s*)?" +
15 | "(?:on\\s*?)?" +
16 | "(?:(this|last|past|next)\\s*)?" +
17 | "(${matchAnyPattern(WEEKDAY_DICTIONARY)})" +
18 | "(?:\\s*(?:\\,|\\)|\\)))?" +
19 | "(?:\\s*(this|last|past|next)\\s*week)?" +
20 | "(?=\\W|\$)",
21 | caseSensitive: false,
22 | );
23 |
24 | const _PREFIX_GROUP = 1;
25 | const _WEEKDAY_GROUP = 2;
26 | const _POSTFIX_GROUP = 3;
27 |
28 | class ENWeekdayParser extends AbstractParserWithWordBoundaryChecking {
29 | @override
30 | RegExp innerPattern(context) {
31 | return _pattern;
32 | }
33 |
34 | @override
35 | ParsingComponents innerExtract(
36 | ParsingContext context, RegExpChronoMatch match) {
37 | final dayOfWeek = match[_WEEKDAY_GROUP]!.toLowerCase();
38 |
39 | /// TODO: remove assumed weekday if null
40 | final weekday = Weekday.weekById(WEEKDAY_DICTIONARY[dayOfWeek] ?? 0);
41 | final prefix = match[_PREFIX_GROUP];
42 | final postfix = match[_POSTFIX_GROUP];
43 | var modifierWord = prefix ?? postfix;
44 | modifierWord = modifierWord ?? "";
45 | modifierWord = modifierWord.toLowerCase();
46 |
47 | String? modifier;
48 | if (modifierWord == "last" || modifierWord == "past") {
49 | modifier = "last";
50 | } else if (modifierWord == "next") {
51 | modifier = "next";
52 | } else if (modifierWord == "this") {
53 | modifier = "this";
54 | }
55 |
56 | return createParsingComponentsAtWeekday(
57 | context.reference, weekday, modifier);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/lib/src/locales/en/refiners/ENMergeDateRangeRefiner.dart:
--------------------------------------------------------------------------------
1 | import '../../../common/refiners/AbstractMergeDateRangeRefiner.dart';
2 |
3 | /// Merging before and after results (see. AbstractMergeDateRangeRefiner)
4 | /// This implementation should provide English connecting phases
5 | /// - 2020-02-13 [to] 2020-02-13
6 | /// - Wednesday [-] Friday
7 | class ENMergeDateRangeRefiner extends AbstractMergeDateRangeRefiner {
8 | @override
9 | RegExp patternBetween() {
10 | return RegExp(r'^\s*(to|-|–|until|through|till)\s*$', caseSensitive: false);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/lib/src/locales/en/refiners/ENMergeDateTimeRefiner.dart:
--------------------------------------------------------------------------------
1 | import '../../../common/refiners/AbstractMergeDateTimeRefiner.dart';
2 |
3 | /// Merging date-only result and time-only result (see. AbstractMergeDateTimeRefiner).
4 | /// This implementation should provide English connecting phases
5 | /// - 2020-02-13 [at] 6pm
6 | /// - Tomorrow [after] 7am
7 | class ENMergeDateTimeRefiner extends AbstractMergeDateTimeRefiner {
8 | @override
9 | RegExp patternBetween() {
10 | return RegExp(r'^\s*(T|at|after|before|on|of|,|-)?\s*$', caseSensitive: false);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/lib/src/locales/en/refiners/ENMergeRelativeDateRefiner.dart:
--------------------------------------------------------------------------------
1 | import "../../../types.dart" show Component;
2 | import '../../../common/abstract_refiners.dart' show MergingRefiner;
3 | import "../../../results.dart"
4 | show ParsingComponents, ParsingResult, ReferenceWithTimezone;
5 | import "../constants.dart" show parseTimeUnits;
6 | import "../../../utils/timeunits.dart" show reverseTimeUnits;
7 |
8 | bool hasImpliedEarlierReferenceDate(ParsingResult result) {
9 | return RegExp(r'\s+(before|from)$').hasMatch(result.text);
10 | }
11 |
12 | bool hasImpliedLaterReferenceDate(ParsingResult result) {
13 | return RegExp(r'\s+(after|since)$', caseSensitive: false)
14 | .hasMatch(result.text);
15 | }
16 |
17 | /// Merges an absolute date with a relative date.
18 | /// - 2 weeks before 2020-02-13
19 | /// - 2 days after next Friday
20 | class ENMergeRelativeDateRefiner extends MergingRefiner {
21 | RegExp patternBetween() {
22 | return RegExp(r'^\s*$', caseSensitive: false);
23 | }
24 |
25 | @override
26 | bool shouldMergeResults(String textBetween, ParsingResult currentResult,
27 | ParsingResult nextResult, context) {
28 | // Dates need to be next to each other to get merged
29 | if (!patternBetween().hasMatch(textBetween)) {
30 | return false;
31 | }
32 |
33 | // Check if any relative tokens were swallowed by the first date.
34 | // E.g. [ from] []
35 | if (!hasImpliedEarlierReferenceDate(currentResult) &&
36 | !hasImpliedLaterReferenceDate(currentResult)) {
37 | return false;
38 | }
39 |
40 | // make sure that implies an absolute date
41 | return nextResult.start.get(Component.day) != null &&
42 | nextResult.start.get(Component.month) != null &&
43 | nextResult.start.get(Component.year) != null;
44 | }
45 |
46 | @override
47 | ParsingResult mergeResults(String textBetween, ParsingResult currentResult,
48 | ParsingResult nextResult, context) {
49 | var timeUnits = parseTimeUnits(currentResult.text);
50 | if (hasImpliedEarlierReferenceDate(currentResult)) {
51 | timeUnits = reverseTimeUnits(timeUnits);
52 | }
53 |
54 | final components = ParsingComponents.createRelativeFromReference(
55 | ReferenceWithTimezone(nextResult.start.date()), timeUnits);
56 |
57 | return ParsingResult(nextResult.reference, currentResult.index,
58 | "${currentResult.text}$textBetween${nextResult.text}", components);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/lib/src/results.dart:
--------------------------------------------------------------------------------
1 | // import 'package:day/plugins/quarter_of_year.dart';
2 | import 'dart:convert';
3 | import 'package:day/day.dart' as day_js;
4 | import './types.dart'
5 | show Component, ParsedComponents, ParsedResult, ParsingReference;
6 | import './utils/day.dart'
7 | show assignSimilarDate, assignSimilarTime, implySimilarTime;
8 | import './timezone.dart' show toTimezoneOffset;
9 |
10 | class ReferenceWithTimezone {
11 | final DateTime instant;
12 | final int? timezoneOffset;
13 |
14 | ReferenceWithTimezone([dynamic input])
15 | : assert(input == null || input is ParsingReference || input is DateTime),
16 | instant = (input is DateTime
17 | ? input
18 | : ((input is ParsingReference ? input.instant : null) ??
19 | DateTime.now())).toLocal(),
20 | timezoneOffset = input is! ParsingReference
21 | ? null
22 | : toTimezoneOffset(input.timezone, input.instant);
23 |
24 | /// Returns a Dart date (system timezone) with the { year, month, day, hour, minute, second } equal to the reference.
25 | /// The output's instant is NOT the reference's instant when the reference's and system's timezone are different.
26 | getDateWithAdjustedTimezone() {
27 | return DateTime.fromMillisecondsSinceEpoch(instant.millisecondsSinceEpoch +
28 | getSystemTimezoneAdjustmentMinute(instant) * 60000);
29 | }
30 |
31 | /// Returns the number minutes difference between the Dart date's timezone and the reference timezone.
32 | int getSystemTimezoneAdjustmentMinute(DateTime? date,
33 | [int? overrideTimezoneOffset]) {
34 | if (date == null || date.millisecondsSinceEpoch < 0) {
35 | // TODO: WARNING: Possibly remove due to JS <-> Dart differences?
36 | // Javascript date timezone calculation got effect when the time epoch < 0
37 | // e.g. new DateTime('Tue Feb 02 1300 00:00:00 GMT+0900 (JST)') => Tue Feb 02 1300 00:18:59 GMT+0918 (JST)
38 | date = DateTime.now();
39 | }
40 |
41 | /// TODO: WARNING: removed negative sign - seems to work as intended, but needs testing.
42 | final currentTimezoneOffset = date.timeZoneOffset.inMinutes;
43 | final targetTimezoneOffset =
44 | overrideTimezoneOffset ?? timezoneOffset ?? currentTimezoneOffset;
45 | return currentTimezoneOffset - targetTimezoneOffset;
46 | }
47 |
48 | @override
49 | String toString() => jsonEncode({
50 | 'instant': instant.toIso8601String(),
51 | 'timezoneOffset': timezoneOffset,
52 | });
53 | }
54 |
55 | class ParsingComponents implements ParsedComponents {
56 | Map knownValues = {};
57 | Map impliedValues = {};
58 | ReferenceWithTimezone reference;
59 | // ignore: prefer_collection_literals
60 | final _tags = Set();
61 |
62 | ParsingComponents(this.reference, [Map? knownComponents]) {
63 | knownValues = {};
64 | impliedValues = {};
65 | if (knownComponents != null) {
66 | for (final key in knownComponents.keys) {
67 | if (knownComponents[key] != null) {
68 | knownValues[key] = knownComponents[key]!;
69 | }
70 | }
71 | }
72 |
73 | final refDayJs = day_js.Day.fromDateTime(reference.instant);
74 | imply(Component.day, refDayJs.date());
75 | imply(Component.month, refDayJs.month());
76 | imply(Component.year, refDayJs.year());
77 | imply(Component.hour, 12);
78 | imply(Component.minute, 0);
79 | imply(Component.second, 0);
80 | imply(Component.millisecond, 0);
81 | }
82 |
83 | @override
84 | int? get(Component component) {
85 | if (knownValues.containsKey(component)) {
86 | return knownValues[component];
87 | }
88 |
89 | if (impliedValues.containsKey(component)) {
90 | return impliedValues[component];
91 | }
92 |
93 | return null;
94 | }
95 |
96 | @override
97 | bool isCertain(Component component) {
98 | return knownValues.containsKey(component);
99 | }
100 |
101 | List getCertainComponents() {
102 | return knownValues.keys.toList();
103 | }
104 |
105 | ParsingComponents imply(Component component, int value) {
106 | if (knownValues.containsKey(component)) {
107 | return this;
108 | }
109 | impliedValues[component] = value;
110 | return this;
111 | }
112 |
113 | ParsingComponents assign(Component component, int value) {
114 | knownValues[component] = value;
115 | impliedValues.remove(component);
116 | return this;
117 | }
118 |
119 | void delete(Component component) {
120 | knownValues.remove(component);
121 | impliedValues.remove(component);
122 | }
123 |
124 | ParsingComponents clone() {
125 | final component = ParsingComponents(reference);
126 | component.knownValues = {};
127 | component.impliedValues = {};
128 |
129 | for (final key in knownValues.keys) {
130 | component.knownValues[key] = knownValues[key]!;
131 | }
132 |
133 | for (final key in impliedValues.keys) {
134 | component.impliedValues[key] = impliedValues[key]!;
135 | }
136 |
137 | return component;
138 | }
139 |
140 | bool isOnlyDate() {
141 | return !isCertain(Component.hour) &&
142 | !isCertain(Component.minute) &&
143 | !isCertain(Component.second);
144 | }
145 |
146 | bool isOnlyTime() {
147 | return !isCertain(Component.weekday) &&
148 | !isCertain(Component.day) &&
149 | !isCertain(Component.month);
150 | }
151 |
152 | bool isOnlyWeekdayComponent() {
153 | return isCertain(Component.weekday) &&
154 | !isCertain(Component.day) &&
155 | !isCertain(Component.month);
156 | }
157 |
158 | bool isDateWithUnknownYear() {
159 | return isCertain(Component.month) && !isCertain(Component.year);
160 | }
161 |
162 | bool isValidDate() {
163 | var date = _dateWithoutTimezoneAdjustment();
164 | if (reference.instant.isUtc) {
165 | date = date.toUtc();
166 | }
167 |
168 | if (date.year != get(Component.year)) return false;
169 | if (date.month != (get(Component.month) ?? 999)) return false;
170 | if (date.day != get(Component.day)) return false;
171 | if (get(Component.hour) != null && date.hour != get(Component.hour)) {
172 | return false;
173 | }
174 | if (get(Component.minute) != null && date.minute != get(Component.minute)) {
175 | return false;
176 | }
177 |
178 | return true;
179 | }
180 |
181 | @override
182 | toString() {
183 | return '''[ParsingComponents {
184 | tags: ${jsonEncode(_tags.toList())},
185 | knownValues: ${jsonEncode(knownValues.map((key, val) => MapEntry(key.name, val)))},
186 | impliedValues: ${jsonEncode(impliedValues.map((key, val) => MapEntry(key.name, val)))}},
187 | reference: ${reference.toString()}]''';
188 | }
189 |
190 | day_js.Day dayjs() {
191 | return day_js.Day.fromDateTime(date());
192 | }
193 |
194 | @override
195 | DateTime date() {
196 | final date = _dateWithoutTimezoneAdjustment();
197 | final timezoneAdjustment = reference.getSystemTimezoneAdjustmentMinute(
198 | date, get(Component.timezoneOffset));
199 | return DateTime.fromMillisecondsSinceEpoch(
200 | date.millisecondsSinceEpoch, isUtc: true).add(Duration(minutes: timezoneAdjustment));
201 | }
202 |
203 | ParsingComponents addTag(String tag) {
204 | _tags.add(tag);
205 | return this;
206 | }
207 |
208 | ParsingComponents addTags(Iterable tags) {
209 | for (final tag in tags) {
210 | _tags.add(tag);
211 | }
212 | return this;
213 | }
214 |
215 | @override
216 | Set tags() {
217 | return _tags.toSet();
218 | }
219 |
220 | DateTime _dateWithoutTimezoneAdjustment() {
221 | return DateTime(
222 | get(Component.year)!,
223 | get(Component.month) ?? 1,
224 | get(Component.day) ?? 1,
225 | get(Component.hour) ?? 0,
226 | get(Component.minute) ?? 0,
227 | get(Component.second) ?? 0,
228 | get(Component.millisecond) ?? 0,
229 | );
230 | }
231 |
232 | static ParsingComponents createRelativeFromReference(
233 | ReferenceWithTimezone reference,
234 | Map fragments,
235 | ) {
236 | var date = day_js.Day.fromDateTime(reference.instant);
237 | for (final key in fragments.keys) {
238 | /// TODO: WARNING: forceful double to int; needs research
239 | date = date.add(fragments[key]!.toInt(), key) ?? date;
240 | }
241 |
242 | final components = ParsingComponents(reference);
243 | if (fragments["hour"] != null ||
244 | fragments["minute"] != null ||
245 | fragments["second"] != null) {
246 | assignSimilarTime(components, date);
247 | assignSimilarDate(components, date);
248 | if (reference.timezoneOffset != null) {
249 | components.assign(Component.timezoneOffset,
250 | -reference.instant.toLocal().timeZoneOffset.inMinutes);
251 | }
252 | } else {
253 | implySimilarTime(components, date);
254 | if (reference.timezoneOffset != null) {
255 | components.imply(Component.timezoneOffset,
256 | -reference.instant.toLocal().timeZoneOffset.inMinutes);
257 | }
258 |
259 | if (fragments["d"] != null) {
260 | components.assign(Component.day, date.date());
261 | components.assign(Component.month, date.month());
262 | components.assign(Component.year, date.year());
263 | } else {
264 | if (fragments["week"] != null) {
265 | components.imply(Component.weekday, date.weekday());
266 | }
267 |
268 | components.imply(Component.day, date.date());
269 | if (fragments["month"] != null) {
270 | components.assign(Component.month, date.month());
271 | components.assign(Component.year, date.year());
272 | } else {
273 | components.imply(Component.month, date.month());
274 | if (fragments["year"] != null) {
275 | components.assign(Component.year, date.year());
276 | } else {
277 | components.imply(Component.year, date.year());
278 | }
279 | }
280 | }
281 | }
282 |
283 | return components;
284 | }
285 | }
286 |
287 | class ParsingResult implements ParsedResult {
288 | @override
289 | final DateTime refDate;
290 | @override
291 | int index;
292 | @override
293 | String text;
294 |
295 | final ReferenceWithTimezone reference;
296 |
297 | @override
298 | ParsingComponents start;
299 | @override
300 | ParsingComponents? end;
301 |
302 | ParsingResult(
303 | this.reference,
304 | this.index,
305 | this.text, [
306 | ParsingComponents? start,
307 | this.end,
308 | ]) : start = start ?? ParsingComponents(reference),
309 | refDate = reference.instant;
310 |
311 | ParsingResult clone() {
312 | final result = ParsingResult(reference, index, text);
313 | result.start = start.clone();
314 | result.end = end?.clone();
315 | return result;
316 | }
317 |
318 | @override
319 | DateTime date() {
320 | return start.date();
321 | }
322 |
323 | @override
324 | Set tags() {
325 | final combinedTags = start.tags().toSet();
326 | if (end != null) {
327 | for (final tag in end!.tags()) {
328 | combinedTags.add(tag);
329 | }
330 | }
331 | return combinedTags;
332 | }
333 |
334 | @override
335 | toString() {
336 | final tags = this.tags().toList();
337 | return '''ParsingResult {index: $index, text: '$text', tags: ${jsonEncode(tags)}, date: ${date()}}''';
338 | }
339 | }
340 |
--------------------------------------------------------------------------------
/lib/src/timezone.dart:
--------------------------------------------------------------------------------
1 | import 'package:day/day.dart' as dayjs;
2 | import './types.dart' show TimezoneAbbrMap, Weekday, Month;
3 |
4 | final TimezoneAbbrMap TIMEZONE_ABBR_MAP = {
5 | 'ACDT': 630,
6 | 'ACST': 570,
7 | 'ADT': -180,
8 | 'AEDT': 660,
9 | 'AEST': 600,
10 | 'AFT': 270,
11 | 'AKDT': -480,
12 | 'AKST': -540,
13 | 'ALMT': 360,
14 | 'AMST': -180,
15 | 'AMT': -240,
16 | 'ANAST': 720,
17 | 'ANAT': 720,
18 | 'AQTT': 300,
19 | 'ART': -180,
20 | 'AST': -240,
21 | 'AWDT': 540,
22 | 'AWST': 480,
23 | 'AZOST': 0,
24 | 'AZOT': -60,
25 | 'AZST': 300,
26 | 'AZT': 240,
27 | 'BNT': 480,
28 | 'BOT': -240,
29 | 'BRST': -120,
30 | 'BRT': -180,
31 | 'BST': 60,
32 | 'BTT': 360,
33 | 'CAST': 480,
34 | 'CAT': 120,
35 | 'CCT': 390,
36 | 'CDT': -300,
37 | 'CEST': 120,
38 | // Note: Many sources define CET as a constant UTC+1. In common usage, however,
39 | // CET usually refers to the time observed in most of Europe, be it standard time or daylight saving time.
40 | 'CET': {
41 | 'timezoneOffsetDuringDst': 2 * 60,
42 | 'timezoneOffsetNonDst': 60,
43 | 'dstStart': (int year) =>
44 | getLastWeekdayOfMonth(year, Month.MARCH, Weekday.SUNDAY, 2),
45 | 'dstEnd': (int year) =>
46 | getLastWeekdayOfMonth(year, Month.OCTOBER, Weekday.SUNDAY, 3),
47 | },
48 | 'CHADT': 825,
49 | 'CHAST': 765,
50 | 'CKT': -600,
51 | 'CLST': -180,
52 | 'CLT': -240,
53 | 'COT': -300,
54 | 'CST': -360,
55 | 'CT': {
56 | 'timezoneOffsetDuringDst': -5 * 60,
57 | 'timezoneOffsetNonDst': -6 * 60,
58 | 'dstStart': (int year) =>
59 | getNthWeekdayOfMonth(year, Month.MARCH, Weekday.SUNDAY, 2, 2),
60 | 'dstEnd': (int year) =>
61 | getNthWeekdayOfMonth(year, Month.NOVEMBER, Weekday.SUNDAY, 1, 2),
62 | },
63 | 'CVT': -60,
64 | 'CXT': 420,
65 | 'ChST': 600,
66 | 'DAVT': 420,
67 | 'EASST': -300,
68 | 'EAST': -360,
69 | 'EAT': 180,
70 | 'ECT': -300,
71 | 'EDT': -240,
72 | 'EEST': 180,
73 | 'EET': 120,
74 | 'EGST': 0,
75 | 'EGT': -60,
76 | 'EST': -300,
77 | 'ET': {
78 | 'timezoneOffsetDuringDst': -4 * 60,
79 | 'timezoneOffsetNonDst': -5 * 60,
80 | 'dstStart': (int year) =>
81 | getNthWeekdayOfMonth(year, Month.MARCH, Weekday.SUNDAY, 2, 2),
82 | 'dstEnd': (int year) =>
83 | getNthWeekdayOfMonth(year, Month.NOVEMBER, Weekday.SUNDAY, 1, 2),
84 | },
85 | 'FJST': 780,
86 | 'FJT': 720,
87 | 'FKST': -180,
88 | 'FKT': -240,
89 | 'FNT': -120,
90 | 'GALT': -360,
91 | 'GAMT': -540,
92 | 'GET': 240,
93 | 'GFT': -180,
94 | 'GILT': 720,
95 | 'GMT': 0,
96 | 'GST': 240,
97 | 'GYT': -240,
98 | 'HAA': -180,
99 | 'HAC': -300,
100 | 'HADT': -540,
101 | 'HAE': -240,
102 | 'HAP': -420,
103 | 'HAR': -360,
104 | 'HAST': -600,
105 | 'HAT': -90,
106 | 'HAY': -480,
107 | 'HKT': 480,
108 | 'HLV': -210,
109 | 'HNA': -240,
110 | 'HNC': -360,
111 | 'HNE': -300,
112 | 'HNP': -480,
113 | 'HNR': -420,
114 | 'HNT': -150,
115 | 'HNY': -540,
116 | 'HOVT': 420,
117 | 'ICT': 420,
118 | 'IDT': 180,
119 | 'IOT': 360,
120 | 'IRDT': 270,
121 | 'IRKST': 540,
122 | 'IRKT': 540,
123 | 'IRST': 210,
124 | 'IST': 330,
125 | 'JST': 540,
126 | 'KGT': 360,
127 | 'KRAST': 480,
128 | 'KRAT': 480,
129 | 'KST': 540,
130 | 'KUYT': 240,
131 | 'LHDT': 660,
132 | 'LHST': 630,
133 | 'LINT': 840,
134 | 'MAGST': 720,
135 | 'MAGT': 720,
136 | 'MART': -510,
137 | 'MAWT': 300,
138 | 'MDT': -360,
139 | 'MESZ': 120,
140 | 'MEZ': 60,
141 | 'MHT': 720,
142 | 'MMT': 390,
143 | 'MSD': 240,
144 | 'MSK': 180,
145 | 'MST': -420,
146 | 'MT': {
147 | 'timezoneOffsetDuringDst': -6 * 60,
148 | 'timezoneOffsetNonDst': -7 * 60,
149 | 'dstStart': (int year) =>
150 | getNthWeekdayOfMonth(year, Month.MARCH, Weekday.SUNDAY, 2, 2),
151 | 'dstEnd': (int year) =>
152 | getNthWeekdayOfMonth(year, Month.NOVEMBER, Weekday.SUNDAY, 1, 2),
153 | },
154 | 'MUT': 240,
155 | 'MVT': 300,
156 | 'MYT': 480,
157 | 'NCT': 660,
158 | 'NDT': -90,
159 | 'NFT': 690,
160 | 'NOVST': 420,
161 | 'NOVT': 360,
162 | 'NPT': 345,
163 | 'NST': -150,
164 | 'NUT': -660,
165 | 'NZDT': 780,
166 | 'NZST': 720,
167 | 'OMSST': 420,
168 | 'OMST': 420,
169 | 'PDT': -420,
170 | 'PET': -300,
171 | 'PETST': 720,
172 | 'PETT': 720,
173 | 'PGT': 600,
174 | 'PHOT': 780,
175 | 'PHT': 480,
176 | 'PKT': 300,
177 | 'PMDT': -120,
178 | 'PMST': -180,
179 | 'PONT': 660,
180 | 'PST': -480,
181 | 'PT': {
182 | 'timezoneOffsetDuringDst': -7 * 60,
183 | 'timezoneOffsetNonDst': -8 * 60,
184 | 'dstStart': (int year) =>
185 | getNthWeekdayOfMonth(year, Month.MARCH, Weekday.SUNDAY, 2, 2),
186 | 'dstEnd': (int year) =>
187 | getNthWeekdayOfMonth(year, Month.NOVEMBER, Weekday.SUNDAY, 1, 2),
188 | },
189 | 'PWT': 540,
190 | 'PYST': -180,
191 | 'PYT': -240,
192 | 'RET': 240,
193 | 'SAMT': 240,
194 | 'SAST': 120,
195 | 'SBT': 660,
196 | 'SCT': 240,
197 | 'SGT': 480,
198 | 'SRT': -180,
199 | 'SST': -660,
200 | 'TAHT': -600,
201 | 'TFT': 300,
202 | 'TJT': 300,
203 | 'TKT': 780,
204 | 'TLT': 540,
205 | 'TMT': 300,
206 | 'TVT': 720,
207 | 'ULAT': 480,
208 | 'UTC': 0,
209 | 'UYST': -120,
210 | 'UYT': -180,
211 | 'UZT': 300,
212 | 'VET': -210,
213 | 'VLAST': 660,
214 | 'VLAT': 660,
215 | 'VUT': 660,
216 | 'WAST': 120,
217 | 'WAT': 60,
218 | 'WEST': 60,
219 | 'WESZ': 60,
220 | 'WET': 0,
221 | 'WEZ': 0,
222 | 'WFT': 720,
223 | 'WGST': -120,
224 | 'WGT': -180,
225 | 'WIB': 420,
226 | 'WIT': 540,
227 | 'WITA': 480,
228 | 'WST': 780,
229 | 'WT': 0,
230 | 'YAKST': 600,
231 | 'YAKT': 600,
232 | 'YAPT': 600,
233 | 'YEKST': 360,
234 | 'YEKT': 360,
235 | };
236 |
237 | /// Get the date which is the nth occurence of a given weekday in a given month and year.
238 | ///
239 | /// @param year The year for which to find the date
240 | /// @param month The month in which the date occurs
241 | /// @param weekday The weekday on which the date occurs
242 | /// @param n The nth occurence of the given weekday on the month to return
243 | /// @param hour The hour of day which should be set on the returned date
244 | /// @return The date which is the nth occurence of a given weekday in a given
245 | /// month and year, at the given hour of day
246 | DateTime getNthWeekdayOfMonth(int year, Month month, Weekday weekday, int n,
247 | [int hour = 0]) {
248 | assert(n == 1 || n == 2 || n == 3 || n == 4);
249 |
250 | int dayOfMonth = 0;
251 | int i = 0;
252 | while (i <= n) {
253 | dayOfMonth++;
254 | final date = DateTime(year, month.id - 1, dayOfMonth);
255 | if (date.weekday == weekday.id) {
256 | i++;
257 | }
258 | }
259 | return DateTime(year, month.id, dayOfMonth, hour);
260 | }
261 |
262 | /// Get the date which is the last occurence of a given weekday in a given month and year.
263 | ///
264 | /// @param year The year for which to find the date
265 | /// @param month The month in which the date occurs
266 | /// @param weekday The weekday on which the date occurs
267 | /// @param hour The hour of day which should be set on the returned date
268 | /// @return The date which is the last occurence of a given weekday in a given
269 | /// month and year, at the given hour of day
270 | DateTime getLastWeekdayOfMonth(int year, Month month, Weekday weekday,
271 | [int hour = 0]) {
272 | // Procedure: Find the first weekday of the next month, compare with the given weekday,
273 | // and use the difference to determine how many days to subtract from the first of the next month.
274 | final oneIndexedWeekday = weekday.id == 0 ? 7 : weekday.id;
275 | final date = DateTime(year, month.id + 1, 1, 12);
276 | final firstWeekdayNextMonth = date.weekday;
277 | int dayDiff;
278 | if (firstWeekdayNextMonth == oneIndexedWeekday) {
279 | dayDiff = 7;
280 | } else if (firstWeekdayNextMonth < oneIndexedWeekday) {
281 | dayDiff = 7 + firstWeekdayNextMonth - oneIndexedWeekday;
282 | } else {
283 | dayDiff = firstWeekdayNextMonth - oneIndexedWeekday;
284 | }
285 | date.add(Duration(days: -dayDiff));
286 | return DateTime(year, month.id, date.day, hour);
287 | }
288 |
289 | /// Finds and returns timezone offset. If timezoneInput is numeric, it is returned. Otherwise, look for timezone offsets
290 | /// in the following order: timezoneOverrides -> {@link TIMEZONE_ABBR_MAP}.
291 | ///
292 | /// @param timezoneInput Uppercase timezone abbreviation or numeric offset in minutes
293 | /// @param date The date to use to determine whether to return DST offsets for ambiguous timezones
294 | /// @param timezoneOverrides Overrides for timezones
295 | /// @return timezone offset in minutes
296 | int? toTimezoneOffset(dynamic timezoneInput,
297 | [DateTime? date, TimezoneAbbrMap timezoneOverrides = const {}]) {
298 | assert(timezoneInput == null || timezoneInput is int || timezoneInput is String);
299 |
300 | if (timezoneInput == null) {
301 | return null;
302 | }
303 |
304 | if (timezoneInput is int) {
305 | return timezoneInput;
306 | }
307 |
308 | final matchedTimezone =
309 | timezoneOverrides[timezoneInput] ?? TIMEZONE_ABBR_MAP[timezoneInput];
310 | if (matchedTimezone == null) {
311 | return null;
312 | }
313 | // This means that we have matched an unambiguous timezone
314 | if (matchedTimezone is int) {
315 | return matchedTimezone;
316 | }
317 |
318 | // The matched timezone is an ambiguous timezone, where the offset depends on whether the context (refDate)
319 | // is during daylight savings or not.
320 |
321 | // Without refDate as context, there's no way to know if DST or non-DST offset should be used. Return null instead.
322 | if (date == null) {
323 | return null;
324 | }
325 |
326 | // Return DST offset if the refDate is during daylight savings
327 | if (dayjs.Day.fromDateTime(date)
328 | .isAfter(matchedTimezone.dstStart(date.year)) &&
329 | !dayjs.Day.fromDateTime(date)
330 | .isAfter(matchedTimezone.dstEnd(date.year))) {
331 | return matchedTimezone.timezoneOffsetDuringDst;
332 | }
333 |
334 | // refDate is not during DST => return non-DST offset
335 | return matchedTimezone.timezoneOffsetNonDst;
336 | }
337 |
--------------------------------------------------------------------------------
/lib/src/types.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: constant_identifier_names
2 |
3 | class ParsingOption {
4 | /// To parse only forward dates (the results should be after the reference date).
5 | /// This effects date/time implication (e.g. weekday or time mentioning)
6 | final bool? forwardDate;
7 |
8 | /// Additional timezone keywords for the parsers to recognize.
9 | /// Any value provided will override the default handling of that value.
10 | final TimezoneAbbrMap? timezones;
11 |
12 | ParsingOption({this.forwardDate, this.timezones});
13 |
14 | /// Internal debug event handler.
15 | /// @internal
16 | dynamic debug;
17 | }
18 |
19 | /// Some timezone abbreviations are ambiguous in that they refer to different offsets
20 | /// depending on the time of year — daylight savings time (DST), or non-DST. This interface
21 | /// allows defining such timezones
22 | abstract class AmbiguousTimezoneMap {
23 | num get timezoneOffsetDuringDst;
24 | num get timezoneOffsetNonDst;
25 |
26 | /// Return the start date of DST for the given year.
27 | /// timezone.ts contains helper methods for common such rules.
28 | DateTime dstStart(num year);
29 |
30 | /// Return the end date of DST for the given year.
31 | /// timezone.ts contains helper methods for common such rules.
32 | DateTime dstEnd(num year);
33 | }
34 |
35 | /// A map describing how timezone abbreviations should map to time offsets.
36 | /// Supports both unambigous mappings abbreviation => offset,
37 | /// and ambiguous mappings, where the offset will depend on whether the
38 | /// time in question is during daylight savings time or not.
39 | typedef TimezoneAbbrMap = Map;
40 |
41 | class ParsingReference {
42 | /// Reference date. The instant (JavaScript Date object) when the input is written or mention.
43 | /// This effect date/time implication (e.g. weekday or time mentioning).
44 | /// (default = now)
45 | DateTime? instant;
46 |
47 | /// Reference timezone. The timezone where the input is written or mention.
48 | /// Date/time implication will account the difference between input timezone and the current system timezone.
49 | /// (default = current timezone)
50 | /// string | number
51 | dynamic timezone;
52 |
53 | ParsingReference({this.instant, this.timezone});
54 | }
55 |
56 | /// Parsed result or final output.
57 | /// Each result object represents a date/time (or date/time-range) mentioning in the input.
58 | abstract class ParsedResult {
59 | DateTime get refDate;
60 | num get index;
61 | String get text;
62 |
63 | ParsedComponents get start;
64 | ParsedComponents? get end;
65 |
66 | /// @return a javascript date object created from the `result.start`.
67 | DateTime date();
68 |
69 | /// @return debugging tags combined of the `result.start` and `result.end`.
70 | Set tags();
71 | }
72 |
73 | /// A collection of parsed date/time components (e.g. day, hour, minute, ..., etc).
74 | ///
75 | /// Each parsed component has three different levels of certainty.
76 | /// - *Certain* (or *Known*): The component is directly mentioned and parsed.
77 | /// - *Implied*: The component is not directly mentioned, but implied by other parsed information.
78 | /// - *Unknown*: Completely no mention of the component.
79 | abstract class ParsedComponents {
80 | /// Check the component certainly if the component is *Certain* (or *Known*)
81 | bool isCertain(Component component);
82 |
83 | /// Get the component value for either *Certain* or *Implied* value.
84 | num? get(Component component);
85 |
86 | /// @return a javascript date object.
87 | DateTime date();
88 |
89 | /// @return debugging tags of the parsed component.
90 | Set tags();
91 | }
92 |
93 | enum Component {
94 | year,
95 | month,
96 | day,
97 | weekday,
98 | hour,
99 | minute,
100 | second,
101 | millisecond,
102 | meridiem,
103 | timezoneOffset,
104 | }
105 |
106 | mixin EnumId {
107 | /// Overridable enum element ID.
108 | int get id;
109 | }
110 |
111 | enum Meridiem implements EnumId {
112 | AM,
113 | PM;
114 |
115 | @override
116 | int get id => index;
117 | }
118 |
119 | enum Weekday implements EnumId {
120 | SUNDAY,
121 | MONDAY,
122 | TUESDAY,
123 | WEDNESDAY,
124 | THURSDAY,
125 | FRIDAY,
126 | SATURDAY;
127 |
128 | @override
129 | int get id => index;
130 |
131 | static Weekday weekById(int id) =>
132 | id == 0 || id == 7 ? SUNDAY : Weekday.values.asMap()[id]!;
133 | }
134 |
135 | enum Month implements EnumId {
136 | JANUARY,
137 | FEBRUARY,
138 | MARCH,
139 | APRIL,
140 | MAY,
141 | JUNE,
142 | JULY,
143 | AUGUST,
144 | SEPTEMBER,
145 | OCTOBER,
146 | NOVEMBER,
147 | DECEMBER;
148 |
149 | @override
150 | int get id => index + 1;
151 | }
152 |
153 | class RegExpChronoMatch {
154 | final RegExpMatch original;
155 |
156 | final Map _groups = {};
157 |
158 | RegExpChronoMatch(this.original) : index = original.start;
159 |
160 | static RegExpChronoMatch? matchOrNull(RegExpMatch? regex) {
161 | if (regex == null) {
162 | return null;
163 | }
164 | return RegExpChronoMatch(regex);
165 | }
166 |
167 | int get groupCount => original.groupCount;
168 |
169 | String? operator [](int group) => group > groupCount
170 | ? null
171 | : (_groups.containsKey(group) ? _groups[group] : original[group]);
172 |
173 | void operator []=(int index, String? value) {
174 | _groups[index] = value;
175 | }
176 |
177 | int index;
178 |
179 | @override
180 | String toString() =>
181 | '''RegExpChronoMatch { index: $index, _groups: $_groups }''';
182 | }
183 |
--------------------------------------------------------------------------------
/lib/src/utils/day.dart:
--------------------------------------------------------------------------------
1 | import 'package:day/day.dart' as dayjs;
2 | import '../results.dart' show ParsingComponents;
3 | import '../types.dart' show Meridiem, Component;
4 |
5 | void assignTheNextDay(ParsingComponents component, dayjs.Day targetDayJs) {
6 | targetDayJs = targetDayJs.add(1, 'd')!;
7 | assignSimilarDate(component, targetDayJs);
8 | implySimilarTime(component, targetDayJs);
9 | }
10 |
11 | void implyTheNextDay(ParsingComponents component, dayjs.Day targetDayJs) {
12 | targetDayJs = targetDayJs.add(1, 'd')!;
13 | implySimilarDate(component, targetDayJs);
14 | implySimilarTime(component, targetDayJs);
15 | }
16 |
17 | void assignSimilarDate(ParsingComponents component, dayjs.Day targetDayJs) {
18 | component.assign(Component.day, targetDayJs.date());
19 | component.assign(Component.month, targetDayJs.month());
20 | component.assign(Component.year, targetDayJs.year());
21 | }
22 |
23 | void assignSimilarTime(ParsingComponents component, dayjs.Day targetDayJs) {
24 | component.assign(Component.hour, targetDayJs.hour());
25 | component.assign(Component.minute, targetDayJs.minute());
26 | component.assign(Component.second, targetDayJs.second());
27 | component.assign(Component.millisecond, targetDayJs.millisecond());
28 | if ((component.get(Component.day) ?? 12) < 12) {
29 | component.assign(Component.meridiem, Meridiem.AM.id);
30 | } else {
31 | component.assign(Component.meridiem, Meridiem.PM.id);
32 | }
33 | }
34 |
35 | void implySimilarDate(ParsingComponents component, dayjs.Day targetDayJs) {
36 | component.imply(Component.day, targetDayJs.date());
37 | component.imply(Component.month, targetDayJs.month());
38 | component.imply(Component.year, targetDayJs.year());
39 | }
40 |
41 | void implySimilarTime(ParsingComponents component, dayjs.Day targetDayJs) {
42 | component.imply(Component.hour, targetDayJs.hour());
43 | component.imply(Component.minute, targetDayJs.minute());
44 | component.imply(Component.second, targetDayJs.second());
45 | component.imply(Component.millisecond, targetDayJs.millisecond());
46 | }
47 |
--------------------------------------------------------------------------------
/lib/src/utils/pattern.dart:
--------------------------------------------------------------------------------
1 | String repeatedTimeunitPattern(String prefix, String singleTimeunitPattern) {
2 | final singleTimeunitPatternNoCapture =
3 | singleTimeunitPattern.replaceAll(RegExp(r'\((?!\?)'), "(?:");
4 | return "$prefix$singleTimeunitPatternNoCapture\\s{0,5}(?:,?\\s{0,5}$singleTimeunitPatternNoCapture){0,10}";
5 | }
6 |
7 | List extractTerms(dynamic dictionary) {
8 | assert(dictionary is Iterable || dictionary is Map);
9 |
10 | List keys = [];
11 | if (dictionary is Iterable) {
12 | keys = [...dictionary];
13 | } else {
14 | keys = Map.from(dictionary).keys.map((a) => a.toString()).toList();
15 | }
16 |
17 | return keys;
18 | }
19 |
20 | String matchAnyPattern(dynamic dictionary) {
21 | // TODO: More efficient regex pattern by considering duplicated prefix
22 |
23 | final terms = extractTerms(dictionary);
24 | terms.sort((a, b) => b.length - a.length);
25 | final joinedTerms = terms.join("|").replaceAll(RegExp(r'\.'), "\\.");
26 |
27 | return "(?:$joinedTerms)";
28 | }
29 |
--------------------------------------------------------------------------------
/lib/src/utils/timeunits.dart:
--------------------------------------------------------------------------------
1 | import '../results.dart' show ParsingComponents;
2 | import '../types.dart' show Component;
3 |
4 | typedef TimeUnits = Map;
5 |
6 | TimeUnits reverseTimeUnits(TimeUnits timeUnits) {
7 | final reversed = {};
8 | for (final key in timeUnits.keys) {
9 | reversed[key] = -timeUnits[key]!;
10 | }
11 |
12 | return reversed;
13 | }
14 |
15 | ParsingComponents addImpliedTimeUnits(
16 | ParsingComponents components, TimeUnits timeUnits) {
17 | final output = components.clone();
18 |
19 | var date = components.dayjs();
20 | for (final key in timeUnits.keys) {
21 | /// TODO: WARNING: forces doubles to be int. Needs research
22 | date = date.add(timeUnits[key]!.toInt(), key)!;
23 | }
24 |
25 | if (timeUnits.containsKey("day") ||
26 | timeUnits.containsKey("d") ||
27 | timeUnits.containsKey("week") ||
28 | timeUnits.containsKey("month") ||
29 | timeUnits.containsKey("year")) {
30 | output.imply(Component.day, date.date());
31 | output.imply(Component.month, date.month());
32 | output.imply(Component.year, date.year());
33 | }
34 |
35 | if (timeUnits.containsKey("second") ||
36 | timeUnits.containsKey("minute") ||
37 | timeUnits.containsKey("hour")) {
38 | output.imply(Component.second, date.second());
39 | output.imply(Component.minute, date.minute());
40 | output.imply(Component.hour, date.hour());
41 | }
42 |
43 | return output;
44 | }
45 |
--------------------------------------------------------------------------------
/pubspec.lock:
--------------------------------------------------------------------------------
1 | # Generated by pub
2 | # See https://dart.dev/tools/pub/glossary#lockfile
3 | packages:
4 | _fe_analyzer_shared:
5 | dependency: transitive
6 | description:
7 | name: _fe_analyzer_shared
8 | sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051
9 | url: "https://pub.dev"
10 | source: hosted
11 | version: "64.0.0"
12 | analyzer:
13 | dependency: transitive
14 | description:
15 | name: analyzer
16 | sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893"
17 | url: "https://pub.dev"
18 | source: hosted
19 | version: "6.2.0"
20 | args:
21 | dependency: transitive
22 | description:
23 | name: args
24 | sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
25 | url: "https://pub.dev"
26 | source: hosted
27 | version: "2.4.2"
28 | async:
29 | dependency: transitive
30 | description:
31 | name: async
32 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
33 | url: "https://pub.dev"
34 | source: hosted
35 | version: "2.11.0"
36 | boolean_selector:
37 | dependency: transitive
38 | description:
39 | name: boolean_selector
40 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
41 | url: "https://pub.dev"
42 | source: hosted
43 | version: "2.1.1"
44 | collection:
45 | dependency: transitive
46 | description:
47 | name: collection
48 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
49 | url: "https://pub.dev"
50 | source: hosted
51 | version: "1.18.0"
52 | convert:
53 | dependency: transitive
54 | description:
55 | name: convert
56 | sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
57 | url: "https://pub.dev"
58 | source: hosted
59 | version: "3.1.1"
60 | coverage:
61 | dependency: transitive
62 | description:
63 | name: coverage
64 | sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097"
65 | url: "https://pub.dev"
66 | source: hosted
67 | version: "1.6.3"
68 | crypto:
69 | dependency: transitive
70 | description:
71 | name: crypto
72 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
73 | url: "https://pub.dev"
74 | source: hosted
75 | version: "3.0.3"
76 | day:
77 | dependency: "direct main"
78 | description:
79 | name: day
80 | sha256: "1e7068deb2f825a8b705d01d1116cc485ddc32531b43dc8c4bf58c5a1b87cd48"
81 | url: "https://pub.dev"
82 | source: hosted
83 | version: "0.8.0"
84 | file:
85 | dependency: transitive
86 | description:
87 | name: file
88 | sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
89 | url: "https://pub.dev"
90 | source: hosted
91 | version: "7.0.0"
92 | frontend_server_client:
93 | dependency: transitive
94 | description:
95 | name: frontend_server_client
96 | sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
97 | url: "https://pub.dev"
98 | source: hosted
99 | version: "3.2.0"
100 | glob:
101 | dependency: transitive
102 | description:
103 | name: glob
104 | sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
105 | url: "https://pub.dev"
106 | source: hosted
107 | version: "2.1.2"
108 | http_multi_server:
109 | dependency: transitive
110 | description:
111 | name: http_multi_server
112 | sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
113 | url: "https://pub.dev"
114 | source: hosted
115 | version: "3.2.1"
116 | http_parser:
117 | dependency: transitive
118 | description:
119 | name: http_parser
120 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
121 | url: "https://pub.dev"
122 | source: hosted
123 | version: "4.0.2"
124 | io:
125 | dependency: transitive
126 | description:
127 | name: io
128 | sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
129 | url: "https://pub.dev"
130 | source: hosted
131 | version: "1.0.4"
132 | js:
133 | dependency: transitive
134 | description:
135 | name: js
136 | sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
137 | url: "https://pub.dev"
138 | source: hosted
139 | version: "0.6.7"
140 | lints:
141 | dependency: "direct dev"
142 | description:
143 | name: lints
144 | sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
145 | url: "https://pub.dev"
146 | source: hosted
147 | version: "2.1.1"
148 | logging:
149 | dependency: transitive
150 | description:
151 | name: logging
152 | sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
153 | url: "https://pub.dev"
154 | source: hosted
155 | version: "1.2.0"
156 | matcher:
157 | dependency: transitive
158 | description:
159 | name: matcher
160 | sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
161 | url: "https://pub.dev"
162 | source: hosted
163 | version: "0.12.16"
164 | meta:
165 | dependency: transitive
166 | description:
167 | name: meta
168 | sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
169 | url: "https://pub.dev"
170 | source: hosted
171 | version: "1.10.0"
172 | mime:
173 | dependency: transitive
174 | description:
175 | name: mime
176 | sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
177 | url: "https://pub.dev"
178 | source: hosted
179 | version: "1.0.4"
180 | node_preamble:
181 | dependency: transitive
182 | description:
183 | name: node_preamble
184 | sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
185 | url: "https://pub.dev"
186 | source: hosted
187 | version: "2.0.2"
188 | package_config:
189 | dependency: transitive
190 | description:
191 | name: package_config
192 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
193 | url: "https://pub.dev"
194 | source: hosted
195 | version: "2.1.0"
196 | path:
197 | dependency: transitive
198 | description:
199 | name: path
200 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
201 | url: "https://pub.dev"
202 | source: hosted
203 | version: "1.8.3"
204 | pool:
205 | dependency: transitive
206 | description:
207 | name: pool
208 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
209 | url: "https://pub.dev"
210 | source: hosted
211 | version: "1.5.1"
212 | pub_semver:
213 | dependency: transitive
214 | description:
215 | name: pub_semver
216 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
217 | url: "https://pub.dev"
218 | source: hosted
219 | version: "2.1.4"
220 | shelf:
221 | dependency: transitive
222 | description:
223 | name: shelf
224 | sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
225 | url: "https://pub.dev"
226 | source: hosted
227 | version: "1.4.1"
228 | shelf_packages_handler:
229 | dependency: transitive
230 | description:
231 | name: shelf_packages_handler
232 | sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
233 | url: "https://pub.dev"
234 | source: hosted
235 | version: "3.0.2"
236 | shelf_static:
237 | dependency: transitive
238 | description:
239 | name: shelf_static
240 | sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e
241 | url: "https://pub.dev"
242 | source: hosted
243 | version: "1.1.2"
244 | shelf_web_socket:
245 | dependency: transitive
246 | description:
247 | name: shelf_web_socket
248 | sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
249 | url: "https://pub.dev"
250 | source: hosted
251 | version: "1.0.4"
252 | source_map_stack_trace:
253 | dependency: transitive
254 | description:
255 | name: source_map_stack_trace
256 | sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae"
257 | url: "https://pub.dev"
258 | source: hosted
259 | version: "2.1.1"
260 | source_maps:
261 | dependency: transitive
262 | description:
263 | name: source_maps
264 | sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
265 | url: "https://pub.dev"
266 | source: hosted
267 | version: "0.10.12"
268 | source_span:
269 | dependency: transitive
270 | description:
271 | name: source_span
272 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
273 | url: "https://pub.dev"
274 | source: hosted
275 | version: "1.10.0"
276 | stack_trace:
277 | dependency: transitive
278 | description:
279 | name: stack_trace
280 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
281 | url: "https://pub.dev"
282 | source: hosted
283 | version: "1.11.1"
284 | stream_channel:
285 | dependency: transitive
286 | description:
287 | name: stream_channel
288 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
289 | url: "https://pub.dev"
290 | source: hosted
291 | version: "2.1.2"
292 | string_scanner:
293 | dependency: transitive
294 | description:
295 | name: string_scanner
296 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
297 | url: "https://pub.dev"
298 | source: hosted
299 | version: "1.2.0"
300 | term_glyph:
301 | dependency: transitive
302 | description:
303 | name: term_glyph
304 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
305 | url: "https://pub.dev"
306 | source: hosted
307 | version: "1.2.1"
308 | test:
309 | dependency: "direct dev"
310 | description:
311 | name: test
312 | sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9"
313 | url: "https://pub.dev"
314 | source: hosted
315 | version: "1.24.6"
316 | test_api:
317 | dependency: transitive
318 | description:
319 | name: test_api
320 | sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
321 | url: "https://pub.dev"
322 | source: hosted
323 | version: "0.6.1"
324 | test_core:
325 | dependency: transitive
326 | description:
327 | name: test_core
328 | sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265"
329 | url: "https://pub.dev"
330 | source: hosted
331 | version: "0.5.6"
332 | typed_data:
333 | dependency: transitive
334 | description:
335 | name: typed_data
336 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
337 | url: "https://pub.dev"
338 | source: hosted
339 | version: "1.3.2"
340 | vm_service:
341 | dependency: transitive
342 | description:
343 | name: vm_service
344 | sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583
345 | url: "https://pub.dev"
346 | source: hosted
347 | version: "11.10.0"
348 | watcher:
349 | dependency: transitive
350 | description:
351 | name: watcher
352 | sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
353 | url: "https://pub.dev"
354 | source: hosted
355 | version: "1.1.0"
356 | web_socket_channel:
357 | dependency: transitive
358 | description:
359 | name: web_socket_channel
360 | sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
361 | url: "https://pub.dev"
362 | source: hosted
363 | version: "2.4.0"
364 | webkit_inspection_protocol:
365 | dependency: transitive
366 | description:
367 | name: webkit_inspection_protocol
368 | sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
369 | url: "https://pub.dev"
370 | source: hosted
371 | version: "1.2.1"
372 | yaml:
373 | dependency: transitive
374 | description:
375 | name: yaml
376 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
377 | url: "https://pub.dev"
378 | source: hosted
379 | version: "3.1.2"
380 | sdks:
381 | dart: ">=3.1.0 <4.0.0"
382 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: chrono_dart
2 | description: A natural language date parser in Dart. Finds date references in user-generated text and returns objects containing DateTime and position in text.
3 | version: 2.0.2
4 | repository: https://github.com/g-30/chrono_dart
5 |
6 | environment:
7 | sdk: ^3.1.0
8 |
9 | dependencies:
10 | day: ^0.8.0
11 |
12 | dev_dependencies:
13 | lints: ^2.0.0
14 | test: ^1.24.6
15 |
--------------------------------------------------------------------------------
/test/calculation_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:chrono_dart/chrono_dart.dart' show Weekday, ReferenceWithTimezone;
2 | import 'package:test/test.dart';
3 | import 'package:chrono_dart/src/common/calculation/weekdays.dart'
4 | show createParsingComponentsAtWeekday, getDaysToWeekday;
5 |
6 | void main() {
7 | test("Test - This Weekday Calculation", () {
8 | (() {
9 | final reference =
10 | ReferenceWithTimezone(DateTime.parse("2022-08-20 12:00:00"));
11 | final output =
12 | createParsingComponentsAtWeekday(reference, Weekday.MONDAY, "this");
13 | expect(output.date().millisecondsSinceEpoch,
14 | DateTime.parse("2022-08-22 12:00:00").millisecondsSinceEpoch);
15 | })();
16 | (() {
17 | final reference =
18 | ReferenceWithTimezone(DateTime.parse("2022-08-21 14:00:00+02:00"));
19 | final output =
20 | createParsingComponentsAtWeekday(reference, Weekday.FRIDAY, "this");
21 | expect(output.date().millisecondsSinceEpoch,
22 | DateTime.parse("2022-08-26 12:00:00").millisecondsSinceEpoch);
23 | })();
24 | (() {
25 | final reference =
26 | ReferenceWithTimezone(DateTime.parse("2022-08-02 12:00:00"));
27 | final output =
28 | createParsingComponentsAtWeekday(reference, Weekday.SUNDAY, "this");
29 | expect(output.date().millisecondsSinceEpoch,
30 | DateTime.parse("2022-08-07 12:00:00").millisecondsSinceEpoch);
31 | })();
32 | });
33 |
34 | test("Test - Last Weekday Calculation", () {
35 | (() {
36 | final reference =
37 | ReferenceWithTimezone(DateTime.parse("2022-08-20 12:00:00"));
38 | final output =
39 | createParsingComponentsAtWeekday(reference, Weekday.FRIDAY, "last");
40 | expect(output.date().millisecondsSinceEpoch,
41 | DateTime.parse("2022-08-19 12:00:00").millisecondsSinceEpoch);
42 | })();
43 | (() {
44 | final reference =
45 | ReferenceWithTimezone(DateTime.parse("2022-08-20 12:00:00"));
46 | final output =
47 | createParsingComponentsAtWeekday(reference, Weekday.MONDAY, "last");
48 | expect(output.date().millisecondsSinceEpoch,
49 | DateTime.parse("2022-08-15 12:00:00").millisecondsSinceEpoch);
50 | })();
51 | (() {
52 | final reference =
53 | ReferenceWithTimezone(DateTime.parse("2022-08-20 12:00:00"));
54 | final output =
55 | createParsingComponentsAtWeekday(reference, Weekday.SUNDAY, "last");
56 | expect(output.date().millisecondsSinceEpoch,
57 | DateTime.parse("2022-08-14 12:00:00").millisecondsSinceEpoch);
58 | })();
59 | (() {
60 | final reference =
61 | ReferenceWithTimezone(DateTime.parse("2022-08-20 12:00:00"));
62 | final output =
63 | createParsingComponentsAtWeekday(reference, Weekday.SATURDAY, "last");
64 | expect(output.date().millisecondsSinceEpoch,
65 | DateTime.parse("2022-08-13 12:00:00").millisecondsSinceEpoch);
66 | })();
67 | });
68 |
69 | test("Test - Next Weekday Calculation", () {
70 | (() {
71 | final reference =
72 | ReferenceWithTimezone(DateTime.parse("2022-08-21 12:00:00"));
73 | final output =
74 | createParsingComponentsAtWeekday(reference, Weekday.MONDAY, "next");
75 | expect(output.date().millisecondsSinceEpoch,
76 | DateTime.parse("2022-08-22 12:00:00").millisecondsSinceEpoch);
77 | })();
78 | (() {
79 | final reference =
80 | ReferenceWithTimezone(DateTime.parse("2022-08-21 12:00:00"));
81 | final output =
82 | createParsingComponentsAtWeekday(reference, Weekday.SATURDAY, "next");
83 | expect(output.date().millisecondsSinceEpoch,
84 | DateTime.parse("2022-08-27 12:00:00").millisecondsSinceEpoch);
85 | })();
86 | (() {
87 | final reference =
88 | ReferenceWithTimezone(DateTime.parse("2022-08-21 12:00:00"));
89 | final output =
90 | createParsingComponentsAtWeekday(reference, Weekday.SUNDAY, "next");
91 | expect(output.date().millisecondsSinceEpoch,
92 | DateTime.parse("2022-08-28 12:00:00").millisecondsSinceEpoch);
93 | })();
94 | (() {
95 | final reference =
96 | ReferenceWithTimezone(DateTime.parse("2022-08-20 12:00:00"));
97 | final output =
98 | createParsingComponentsAtWeekday(reference, Weekday.FRIDAY, "next");
99 | expect(output.date().millisecondsSinceEpoch,
100 | DateTime.parse("2022-08-26 12:00:00").millisecondsSinceEpoch);
101 | })();
102 | (() {
103 | final reference =
104 | ReferenceWithTimezone(DateTime.parse("2022-08-20 12:00:00"));
105 | final output =
106 | createParsingComponentsAtWeekday(reference, Weekday.SATURDAY, "next");
107 | expect(output.date().millisecondsSinceEpoch,
108 | DateTime.parse("2022-08-27 12:00:00").millisecondsSinceEpoch);
109 | })();
110 | (() {
111 | final reference =
112 | ReferenceWithTimezone(DateTime.parse("2022-08-20 12:00:00"));
113 | final output =
114 | createParsingComponentsAtWeekday(reference, Weekday.SUNDAY, "next");
115 | expect(output.date().millisecondsSinceEpoch,
116 | DateTime.parse("2022-08-28 12:00:00").millisecondsSinceEpoch);
117 | })();
118 | (() {
119 | final reference =
120 | ReferenceWithTimezone(DateTime.parse("2022-08-02 12:00:00"));
121 | final output =
122 | createParsingComponentsAtWeekday(reference, Weekday.MONDAY, "next");
123 | expect(output.date().millisecondsSinceEpoch,
124 | DateTime.parse("2022-08-08 12:00:00").millisecondsSinceEpoch);
125 | })();
126 | (() {
127 | final reference =
128 | ReferenceWithTimezone(DateTime.parse("2022-08-02 12:00:00"));
129 | final output =
130 | createParsingComponentsAtWeekday(reference, Weekday.FRIDAY, "next");
131 | expect(output.date().millisecondsSinceEpoch,
132 | DateTime.parse("2022-08-12 12:00:00").millisecondsSinceEpoch);
133 | })();
134 |
135 | (() {
136 | final reference =
137 | ReferenceWithTimezone(DateTime.parse("2022-08-02 12:00:00"));
138 | final output =
139 | createParsingComponentsAtWeekday(reference, Weekday.SUNDAY, "next");
140 | expect(output.date().millisecondsSinceEpoch,
141 | DateTime.parse("2022-08-14 12:00:00").millisecondsSinceEpoch);
142 | })();
143 | });
144 |
145 | test("Test - Closest Weekday Calculation", () {
146 | (() {
147 | final refDate = DateTime.parse("2022-08-20");
148 | expect(getDaysToWeekday(refDate, Weekday.MONDAY), 2);
149 | })();
150 | (() {
151 | final refDate = DateTime.parse("2022-08-20");
152 | expect(getDaysToWeekday(refDate, Weekday.TUESDAY), 3);
153 | })();
154 | (() {
155 | final refDate = DateTime.parse("2022-08-20");
156 | expect(getDaysToWeekday(refDate, Weekday.FRIDAY), -1);
157 | })();
158 | (() {
159 | final refDate = DateTime.parse("2022-08-20");
160 | expect(getDaysToWeekday(refDate, Weekday.THURSDAY), -2);
161 | })();
162 | (() {
163 | final refDate = DateTime.parse("2022-08-20");
164 | expect(getDaysToWeekday(refDate, Weekday.WEDNESDAY), -3);
165 | })();
166 | });
167 | }
168 |
--------------------------------------------------------------------------------
/test/debugging_test.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_function_declarations_over_variables
2 | import 'package:test/test.dart';
3 | import 'package:chrono_dart/src/debugging.dart' show BufferedDebugHandler;
4 |
5 | void main() {
6 | test("Test - BufferedDebugHandler", () {
7 | final debugHandler = BufferedDebugHandler();
8 |
9 | int a = 1;
10 | final debugBlockA = () => a = 2;
11 | debugHandler.debug(() => debugBlockA());
12 | expect(a, 1);
13 |
14 | int b = 2;
15 | final debugBlockB = () => b = 3;
16 | debugHandler.debug(() => debugBlockB());
17 | expect(b, 2);
18 |
19 | debugHandler.executeBufferedBlocks();
20 | expect(a, 2);
21 | expect(b, 3);
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/test/result_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:test/test.dart';
2 | import 'package:chrono_dart/src/types.dart' show Component;
3 | import 'package:chrono_dart/src/results.dart'
4 | show ParsingComponents, ParsingResult, ReferenceWithTimezone;
5 |
6 | void main() {
7 | test("Test - Create & manipulate parsing components", () {
8 | final reference = ReferenceWithTimezone(DateTime.now());
9 | final components = ParsingComponents(reference,
10 | {Component.year: 2014, Component.month: 11, Component.day: 24});
11 |
12 | expect(components.get(Component.year), 2014);
13 | expect(components.get(Component.month), 11);
14 | expect(components.get(Component.day), 24);
15 | expect(components.date(), isNot(null));
16 | expect(components.tags().length, 0);
17 |
18 | // null
19 | expect(components.get(Component.weekday), isNull);
20 | expect(components.isCertain(Component.weekday), false);
21 |
22 | // "imply"
23 | components.imply(Component.weekday, 1);
24 | expect(components.get(Component.weekday), 1);
25 | expect(components.isCertain(Component.weekday), false);
26 |
27 | // "assign" overrides "imply"
28 | components.assign(Component.weekday, 2);
29 | expect(components.get(Component.weekday), 2);
30 | expect(components.isCertain(Component.weekday), true);
31 |
32 | // "imply" doesn't override "assign"
33 | components.imply(Component.year, 2013);
34 | expect(components.get(Component.year), 2014);
35 |
36 | // "assign" overrides "assign"
37 | components.assign(Component.year, 2013);
38 | expect(components.get(Component.year), 2013);
39 |
40 | components.addTag("custom/testing_component_tag");
41 | expect(components.tags().length, 1);
42 | expect(components.tags(), contains("custom/testing_component_tag"));
43 | expect(components.toString(), contains("custom/testing_component_tag"));
44 | });
45 |
46 | test("Test - Create & manipulate parsing results", () {
47 | final reference = ReferenceWithTimezone(DateTime.now());
48 | final text = "1 - 2 hour later";
49 |
50 | final startComponents =
51 | ParsingComponents.createRelativeFromReference(reference, {"hour": 1})
52 | .addTag("custom/testing_start_component_tag");
53 |
54 | final endComponents =
55 | ParsingComponents.createRelativeFromReference(reference, {"hour": 2})
56 | .addTag("custom/testing_end_component_tag");
57 |
58 | final result =
59 | ParsingResult(reference, 0, text, startComponents, endComponents);
60 |
61 | // The result's date() should be the same as the start components' date()
62 | expect(result.date().millisecondsSinceEpoch,
63 | startComponents.date().millisecondsSinceEpoch);
64 |
65 | // The result's tags should include both the start and end components' tags
66 | expect(result.tags(), contains("custom/testing_start_component_tag"));
67 | expect(result.tags(), contains("custom/testing_end_component_tag"));
68 |
69 | // The result's toString() should include the text and tags
70 | expect(result.toString(), contains(text));
71 | expect(result.toString(), contains("custom/testing_start_component_tag"));
72 | expect(result.toString(), contains("custom/testing_end_component_tag"));
73 | });
74 |
75 | test("Test - Calendar checking with implied components", () {
76 | final reference = ReferenceWithTimezone(DateTime.now());
77 |
78 | final components = ParsingComponents(reference, {
79 | Component.day: 13,
80 | Component.month: 12,
81 | Component.year: 2021,
82 | Component.hour: 14,
83 | Component.minute: 22,
84 | Component.second: 14,
85 | Component.millisecond: 0,
86 | });
87 | components.imply(Component.timezoneOffset, -300);
88 |
89 | expect(components.isValidDate(), true);
90 | });
91 |
92 | group("Test - Calendar Checking", () {
93 | final reference = ReferenceWithTimezone(DateTime.now());
94 |
95 | test('validity - 1', () {
96 | final components = ParsingComponents(reference,
97 | {Component.year: 2014, Component.month: 11, Component.day: 24});
98 | expect(components.isValidDate(), true);
99 | });
100 |
101 | test('validity - 2', () {
102 | final components = ParsingComponents(reference, {
103 | Component.year: 2014,
104 | Component.month: 11,
105 | Component.day: 24,
106 | Component.hour: 12
107 | });
108 | expect(components.isValidDate(), true);
109 | });
110 |
111 | test('validity - 3', () {
112 | final components = ParsingComponents(reference, {
113 | Component.year: 2014,
114 | Component.month: 11,
115 | Component.day: 24,
116 | Component.hour: 12,
117 | Component.minute: 30
118 | });
119 | expect(components.isValidDate(), true);
120 | });
121 |
122 | test('validity - 4', () {
123 | final components = ParsingComponents(reference, {
124 | Component.year: 2014,
125 | Component.month: 11,
126 | Component.day: 24,
127 | Component.hour: 12,
128 | Component.minute: 30,
129 | Component.second: 30,
130 | });
131 | expect(components.isValidDate(), true);
132 | });
133 |
134 | test('validity - 5', () {
135 | final components = ParsingComponents(reference,
136 | {Component.year: 2014, Component.month: 13, Component.day: 24});
137 | expect(components.isValidDate(), false);
138 | });
139 |
140 | test('validity - 6', () {
141 | final components = ParsingComponents(reference,
142 | {Component.year: 2014, Component.month: 11, Component.day: 32});
143 | expect(components.isValidDate(), false);
144 | });
145 |
146 | test('validity - 7', () {
147 | final components = ParsingComponents(reference, {
148 | Component.year: 2014,
149 | Component.month: 11,
150 | Component.day: 24,
151 | Component.hour: 24
152 | });
153 | expect(components.isValidDate(), false);
154 | });
155 |
156 | test('validity - 8', () {
157 | final components = ParsingComponents(reference, {
158 | Component.year: 2014,
159 | Component.month: 11,
160 | Component.day: 24,
161 | Component.hour: 12,
162 | Component.minute: 60
163 | });
164 | expect(components.isValidDate(), false);
165 | });
166 |
167 | test('validity - 9', () {
168 | final components = ParsingComponents(reference, {
169 | Component.year: 2014,
170 | Component.month: 11,
171 | Component.day: 24,
172 | Component.hour: 12,
173 | Component.minute: 30,
174 | Component.second: 60,
175 | });
176 | expect(components.isValidDate(), false);
177 | });
178 | });
179 | }
180 |
--------------------------------------------------------------------------------
/test/system_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:chrono_dart/src/common/parsers/ISOFormatParser.dart';
2 | import 'package:test/test.dart';
3 | import 'package:chrono_dart/chrono_dart.dart';
4 | import 'package:chrono_dart/src/common/refiners/UnlikelyFormatFilter.dart';
5 | import 'package:chrono_dart/src/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.dart';
6 | // import 'package:chrono_dart/src/locales/en/parsers/ENWeekdayParser.dart';
7 | import './test_util.dart' show testSingleCase, testUnexpectedResult, toBeDate;
8 |
9 | void main() {
10 | test("Test - Load modules", () {
11 | expect(Chrono, isNotNull);
12 |
13 | expect(Chrono.parse, isNotNull);
14 |
15 | expect(Chrono.parseDate, isNotNull);
16 |
17 | expect(Chrono.casual, isNotNull);
18 |
19 | expect(Chrono.strict, isNotNull);
20 | });
21 |
22 | test("Test - Basic parse date functions", () {
23 | expect(Chrono.parseDate("7:00PM July 5th, 2020")?.millisecondsSinceEpoch,
24 | DateTime(2020, 7, 5, 19).millisecondsSinceEpoch);
25 |
26 | expect(
27 | Chrono.strict
28 | .parseDate("7:00PM July 5th, 2020")
29 | ?.millisecondsSinceEpoch,
30 | DateTime(2020, 7, 5, 19).millisecondsSinceEpoch);
31 |
32 | expect(
33 | Chrono.casual
34 | .parseDate("7:00PM July 5th, 2020")
35 | ?.millisecondsSinceEpoch,
36 | DateTime(2020, 7, 5, 19).millisecondsSinceEpoch);
37 | });
38 |
39 | /*test("Test - Add custom parser", () {
40 | final Chrono.Parser customParser = {
41 | pattern: () {
42 | return /(\d{1,2})(st|nd|rd|th)/i;
43 | },
44 | extract: (context, match) {
45 | expect(match[0], "25th");
46 | expect(context.refDate).toBeTruthy();
47 |
48 | return {
49 | day: parseInt(match[1]),
50 | };
51 | },
52 | };
53 |
54 | final custom = Chrono.Chrono();
55 | custom.parsers.add(customParser);
56 | testSingleCase(custom, "meeting on 25th", DateTime(2017, 11, 19), (result) {
57 | expect(result.text, "25th");
58 | expect(result.start.get(Component.month), 11);
59 | expect(result.start.get(Component.day), 25);
60 | });
61 | });
62 |
63 | test("Test - Add custom parser example", () {
64 | final custom = Chrono.casual.clone();
65 | custom.parsers.add({
66 | pattern: () {
67 | return /\bChristmas\b/i;
68 | },
69 | extract: () {
70 | return {
71 | day: 25,
72 | month: 12,
73 | };
74 | },
75 | });
76 |
77 | testSingleCase(custom, "I'll arrive at 2.30AM on Christmas", (result) {
78 | expect(result.text, "at 2.30AM on Christmas");
79 | expect(result.start.get(Component.month), 12);
80 | expect(result.start.get(Component.day), 25);
81 | expect(result.start.get(Component.hour), 2);
82 | expect(result.start.get(Component.minute), 30);
83 | });
84 |
85 | testSingleCase(custom, "I'll arrive at Christmas night", (result) {
86 | expect(result.text, "Christmas night");
87 | expect(result.start.get(Component.month), 12);
88 | expect(result.start.get(Component.day), 25);
89 | expect(result.start.get(Component.meridiem), Meridiem.PM);
90 | expect(result.start.get(Component.meridiem), 1);
91 | });
92 |
93 | testSingleCase(custom, "Doing something tomorrow", (result) {
94 | expect(result.text, "tomorrow");
95 | });
96 | });
97 |
98 | test("Test - Add custom refiner example", () {
99 | final custom = Chrono.casual.clone();
100 | custom.refiners.add({
101 | refine: (context, results) {
102 | // If there is no AM/PM (meridiem) specified,
103 | // let all time between 1:00 - 4:00 be PM (13.00 - 16.00)
104 | results.forEach((result) {
105 | if (
106 | !result.start.isCertain("meridiem") &&
107 | result.start.get(Component.hour) >= 1 &&
108 | result.start.get(Component.hour) < 4
109 | ) {
110 | result.start.assign(Component.meridiem, Meridiem.PM);
111 | result.start.assign(Component.hour, result.start.get(Component.hour) + 12);
112 | }
113 | });
114 | return results;
115 | },
116 | });
117 |
118 | testSingleCase(custom, "This is at 2.30", (result) {
119 | expect(result.text, "at 2.30");
120 | expect(result.start.get(Component.hour), 14);
121 | expect(result.start.get(Component.minute), 30);
122 | });
123 |
124 | testSingleCase(custom, "This is at 2.30 AM", (result) {
125 | expect(result.text, "at 2.30 AM");
126 | expect(result.start.get(Component.hour), 2);
127 | expect(result.start.get(Component.minute), 30);
128 | });
129 | });
130 |
131 | test("Test - Add custom parser with tags example", () {
132 | final custom = Chrono.casual.clone();
133 | custom.parsers.add({
134 | pattern: () {
135 | return /\bChristmas\b/i;
136 | },
137 | extract: (context) {
138 | return context
139 | .createParsingComponents({
140 | Component.day: 25,
141 | Component.month: 12,
142 | })
143 | .addTag("parser/ChristmasDayParser");
144 | },
145 | });
146 |
147 | testSingleCase(custom, "Doing something tomorrow", (result) {
148 | expect(result.text, "tomorrow");
149 | expect(result.tags(), contains("parser/ENCasualDateParser"));
150 | });
151 |
152 | testSingleCase(custom, "I'll arrive at 2.30AM on Christmas", (result) {
153 | expect(result.text, "at 2.30AM on Christmas");
154 | expect(result.tags(), contains("parser/ChristmasDayParser"));
155 | expect(result.tags(), contains("parser/ENTimeExpressionParser"));
156 | });
157 |
158 | testSingleCase(custom, "I'll arrive at Christmas night", (result) {
159 | expect(result.text, "Christmas night");
160 | expect(result.tags(), contains("parser/ChristmasDayParser"));
161 | expect(result.tags(), contains("parser/ENCasualTimeParser"));
162 | });
163 |
164 | // TODO: Check if the merge date range combine tags correctly
165 | });*/
166 |
167 | test("Test - Remove parsers example", () {
168 | final custom = Chrono.strict.clone();
169 | custom.parsers =
170 | custom.parsers.whereType().toList();
171 | // custom.parsers.add(ISOFormatParser());
172 |
173 | testSingleCase(custom, "2018-10-06", (result) {
174 | expect(result.text, "2018-10-06");
175 | expect(result.start.get(Component.year), 2018);
176 | expect(result.start.get(Component.month), 10);
177 | expect(result.start.get(Component.day), 6);
178 | });
179 | });
180 |
181 | test("Test - Remove a refiner example", () {
182 | final custom = Chrono.casual.clone();
183 | custom.refiners =
184 | custom.refiners.where((r) => r is! UnlikelyFormatFilter).toList();
185 |
186 | testSingleCase(custom, "This is at 2.30", (result) {
187 | expect(result.text, "at 2.30");
188 | expect(result.start.get(Component.hour), 2);
189 | expect(result.start.get(Component.minute), 30);
190 | });
191 | });
192 |
193 | test("Test - Replace a parser example", () {
194 | final custom = Chrono.casual.clone();
195 | testSingleCase(custom, "next 5m", DateTime(2016, 10, 1, 14, 52),
196 | (result, text) {
197 | expect(result.start.get(Component.hour), 14);
198 | expect(result.start.get(Component.minute), 57);
199 | });
200 | testSingleCase(custom, "next 5 minutes", DateTime(2016, 10, 1, 14, 52),
201 | (result, text) {
202 | expect(result.start.get(Component.hour), 14);
203 | expect(result.start.get(Component.minute), 57);
204 | });
205 |
206 | final index = custom.parsers
207 | .indexWhere((r) => r is ENTimeUnitCasualRelativeFormatParser);
208 | custom.parsers[index] = ENTimeUnitCasualRelativeFormatParser(false);
209 | testUnexpectedResult(custom, "next 5m");
210 | testSingleCase(custom, "next 5 minutes", DateTime(2016, 10, 1, 14, 52),
211 | (result, text) {
212 | expect(result.start.get(Component.hour), 14);
213 | expect(result.start.get(Component.minute), 57);
214 | });
215 | });
216 |
217 | test("Test - Simple date parse", () {
218 | final chronoInst = Chrono.casual;
219 | final date = chronoInst.parseDate("I'll see you next Monday at 3pm", DateTime(2023, 10, 05));
220 | expect(date, isNotNull);
221 | expect(date, toBeDate(DateTime(2023, 10, 09, 15)));
222 | });
223 |
224 | test("Test - Tomorrow", () {
225 | final chronoInst = Chrono.casual;
226 | final res = chronoInst.parse("I'll see you tomorrow at 7pm!", DateTime(2023, 10, 05));
227 | final date = res.firstOrNull?.date();
228 | expect(date, isNotNull);
229 | expect(date, toBeDate(DateTime(2023, 10, 06, 19)));
230 | });
231 |
232 | test("Test - Day after tomorrow", () {
233 | final chronoInst = Chrono.casual;
234 | final res = chronoInst.parse("I'll see you the day after tomorrow at 6pm!", DateTime(2023, 10, 05));
235 | final date = res.firstOrNull?.date();
236 | expect(date, isNotNull);
237 | expect(date, toBeDate(DateTime(2023, 10, 07, 18)));
238 | });
239 |
240 | test("Test - yesterday", () {
241 | final chronoInst = Chrono.casual;
242 | final res = chronoInst.parse("We had a meeting yesterday at 8am!", DateTime(2023, 10, 05));
243 | final date = res.firstOrNull?.date();
244 | expect(date, isNotNull);
245 | expect(date, toBeDate(DateTime(2023, 10, 04, 8)));
246 | });
247 |
248 | test("Test - Compare with native dart", () {
249 | final chronoInst = Chrono.instance;
250 |
251 | void testByCompareWithNative(text) {
252 | final expectedDate = DateTime.parse(text);
253 | testSingleCase(chronoInst, text, (result) {
254 | expect(result.text, text);
255 | expect(result, toBeDate(expectedDate));
256 | });
257 | }
258 |
259 | testByCompareWithNative("1994-11-05T13:15:30Z");
260 |
261 | testByCompareWithNative("1994-02-28T08:15:30-05:30");
262 |
263 | testByCompareWithNative("1994-11-05T08:15:30-05:30");
264 |
265 | testByCompareWithNative("1994-11-05T08:15:30+11:30");
266 |
267 | testByCompareWithNative("2014-11-30T08:15:30-05:30");
268 |
269 | testByCompareWithNative("1900-01-01T00:00:00-01:00");
270 |
271 | testByCompareWithNative("1900-01-01T00:00:00-00:00");
272 |
273 | testByCompareWithNative("9999-12-31T23:59:00-00:00");
274 |
275 | testByCompareWithNative("20170925 22:31:50.522");
276 |
277 | testByCompareWithNative("2014-12-14T18:22:14.759Z");
278 | });
279 | }
280 |
--------------------------------------------------------------------------------
/test/system_timezone_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:test/test.dart';
2 | import 'package:chrono_dart/chrono_dart.dart';
3 | import './test_util.dart' show testSingleCase, toBeDate;
4 |
5 | void main() {
6 | final chrono = Chrono.instance;
7 | test("Test - Timezone difference on reference example", () {
8 | testSingleCase(
9 | chrono,
10 | "Friday at 4pm",
11 | ParsingReference(
12 | instant: DateTime.parse("2021-06-09T07:00:00-05:00"),
13 | timezone: "CDT",
14 | ), (result) {
15 | expect(result, toBeDate(DateTime.parse("2021-06-11T16:00:00-05:00")));
16 | expect(result, toBeDate(DateTime.parse("2021-06-12T06:00:00+09:00")));
17 | });
18 | });
19 |
20 | test("Test - Timezone difference on default timezone", () {
21 | final INPUT = "Friday at 4pm";
22 | final REF_INSTANT = DateTime(2021, 6, 9, 7, 0, 0);
23 | final EXPECTED_INSTANT = DateTime(2021, 6, 11, 16, 0, 0);
24 |
25 | testSingleCase(chrono, INPUT, REF_INSTANT, (result) {
26 | expect(result, toBeDate(EXPECTED_INSTANT));
27 | });
28 |
29 | testSingleCase(chrono, INPUT,
30 | ParsingReference(instant: REF_INSTANT),
31 | (result) {
32 | expect(result, toBeDate(EXPECTED_INSTANT));
33 | });
34 |
35 | testSingleCase(
36 | chrono,
37 | INPUT,
38 |
39 | ParsingReference(instant: REF_INSTANT, timezone: null), (result) {
40 | expect(result, toBeDate(EXPECTED_INSTANT));
41 | });
42 |
43 | testSingleCase(
44 | chrono,
45 | INPUT,
46 |
47 | ParsingReference(instant: REF_INSTANT, timezone: ""), (result) {
48 | expect(result, toBeDate(EXPECTED_INSTANT));
49 | });
50 | });
51 |
52 | test("Test - Timezone difference on reference date", () {
53 | // 2021-06-06T19:00:00+09:00
54 | // 2021-06-06T11:00:00+01:00
55 | final refInstant = DateTime.parse("2021-06-06T19:00:00+09:00");
56 |
57 | testSingleCase(
58 | chrono,
59 | "At 4pm tomorrow",
60 |
61 | ParsingReference(instant: refInstant, timezone: "BST"), (result) {
62 | final expectedInstant = DateTime.parse("2021-06-07T16:00:00+01:00");
63 | expect(result, toBeDate(expectedInstant));
64 | });
65 |
66 | testSingleCase(
67 | chrono,
68 | "At 4pm tomorrow",
69 |
70 | ParsingReference(instant: refInstant, timezone: "JST"), (result) {
71 | final expectedInstant = DateTime.parse("2021-06-07T16:00:00+09:00");
72 | expect(result, toBeDate(expectedInstant));
73 | });
74 | });
75 |
76 | test("Test - Timezone difference on written date", () {
77 | // 2021-06-06T19:00:00+09:00
78 | // 2021-06-06T11:00:00+01:00
79 | final refInstant = DateTime.parse("2021-06-06T19:00:00+09:00");
80 |
81 | testSingleCase(chrono, "2021-06-06T19:00:00",
82 | ParsingReference(timezone: "JST"), (result) {
83 | expect(result, toBeDate(refInstant));
84 | });
85 |
86 | testSingleCase(chrono, "2021-06-06T11:00:00",
87 | ParsingReference(timezone: "BST"), (result) {
88 | expect(result, toBeDate(refInstant));
89 | });
90 |
91 | testSingleCase(chrono, "2021-06-06T11:00:00",
92 | ParsingReference(timezone: 60), (result) {
93 | expect(result, toBeDate(refInstant));
94 | });
95 | });
96 |
97 | test("Test - Precise [now] mentioned", () {
98 | final refDate = DateTime.parse("2021-13-03T14:22:14+09:00");
99 |
100 | testSingleCase(chrono, "now", refDate, (result) {
101 | expect(result, toBeDate(refDate));
102 | });
103 |
104 | testSingleCase(chrono, "now",
105 | ParsingReference(instant: refDate), (result) {
106 | expect(result, toBeDate(refDate));
107 | });
108 |
109 | testSingleCase(
110 | chrono,
111 | "now",
112 |
113 | ParsingReference(instant: refDate, timezone: 540), (result) {
114 | expect(result, toBeDate(refDate));
115 | });
116 |
117 | testSingleCase(
118 | chrono,
119 | "now",
120 |
121 | ParsingReference(instant: refDate, timezone: "JST"), (result) {
122 | expect(result, toBeDate(refDate));
123 | });
124 |
125 | testSingleCase(
126 | chrono,
127 | "now",
128 |
129 | ParsingReference(instant: refDate, timezone: -300), (result) {
130 | expect(result, toBeDate(refDate));
131 | });
132 | });
133 |
134 | test("Test - Precise date/time mentioned", () {
135 | final text = "Sat Mar 13 2021 14:22:14+09:00";
136 | final dartDate = DateTime.parse('2021-03-13T14:22:14+09:00');
137 | final refDate = DateTime.now();
138 |
139 | testSingleCase(chrono, text, refDate, (result, text) {
140 | expect(result, toBeDate(dartDate));
141 | });
142 |
143 | testSingleCase(
144 | chrono, text, ParsingReference(instant: refDate),
145 | (result) {
146 | expect(result, toBeDate(dartDate));
147 | });
148 |
149 | testSingleCase(
150 | chrono,
151 | text,
152 |
153 | ParsingReference(instant: refDate, timezone: 540), (result) {
154 | expect(result, toBeDate(dartDate));
155 | });
156 |
157 | testSingleCase(
158 | chrono,
159 | text,
160 |
161 | ParsingReference(instant: refDate, timezone: "JST"), (result) {
162 | expect(result, toBeDate(dartDate));
163 | });
164 |
165 | testSingleCase(
166 | chrono,
167 | text,
168 |
169 | ParsingReference(instant: refDate, timezone: -300), (result) {
170 | expect(result, toBeDate(dartDate));
171 | });
172 | });
173 | }
174 |
--------------------------------------------------------------------------------
/test/test_util.dart:
--------------------------------------------------------------------------------
1 | import 'package:test/test.dart';
2 | import 'package:chrono_dart/src/debugging.dart' show BufferedDebugHandler;
3 | import 'package:chrono_dart/chrono_dart.dart'
4 | show ChronoInstance, ParsedResult, ParsingOption, ParsingReference;
5 |
6 | typedef ChronoLike = ChronoInstance;
7 | typedef CheckResult = void Function(ParsedResult p, String text);
8 |
9 | void testSingleCase(
10 | ChronoLike chrono,
11 | String text, [
12 | /// [ ParsingReference | Date | CheckResult ]
13 | refDateOrCheckResult,
14 |
15 | /// ParsingOption | CheckResult,
16 | optionOrCheckResult,
17 | CheckResult? checkResult,
18 | ]) {
19 | var _refDateOrCheckResult = refDateOrCheckResult;
20 | var _optionOrCheckResult = optionOrCheckResult;
21 | var _checkResult = checkResult;
22 | if (_checkResult == null && _optionOrCheckResult is CheckResult) {
23 | _checkResult = _optionOrCheckResult;
24 | _optionOrCheckResult = null;
25 | }
26 |
27 | if (_optionOrCheckResult == null && _refDateOrCheckResult is CheckResult) {
28 | _checkResult = _refDateOrCheckResult;
29 | _refDateOrCheckResult = null;
30 | }
31 |
32 | final debugHandler = BufferedDebugHandler();
33 | _optionOrCheckResult = _optionOrCheckResult ?? ParsingOption();
34 | if (_optionOrCheckResult is ParsingOption) {
35 | _optionOrCheckResult.debug = debugHandler;
36 | }
37 |
38 | try {
39 | final results = chrono.parse(
40 | text,
41 | _refDateOrCheckResult is DateTime ||
42 | _refDateOrCheckResult is ParsingReference
43 | ? _refDateOrCheckResult
44 | : null,
45 | _optionOrCheckResult is ParsingOption ? _optionOrCheckResult : null);
46 | expect(results, toBeSingleOnText(text));
47 | if (_checkResult != null) {
48 | _checkResult(results[0], text);
49 | }
50 | } catch (e) {
51 | debugHandler.executeBufferedBlocks();
52 | rethrow;
53 | }
54 | }
55 |
56 | void testWithExpectedDate(
57 | ChronoLike chrono, String text, DateTime expectedDate) {
58 | testSingleCase(chrono, text, (result) {
59 | expect(result.start, toBeDate(expectedDate));
60 | });
61 | }
62 |
63 | void testUnexpectedResult(ChronoLike chrono, String text,
64 | [DateTime? refDate, ParsingOption? options]) {
65 | final debugHandler = BufferedDebugHandler();
66 | options ??= ParsingOption();
67 | options.debug = debugHandler;
68 |
69 | try {
70 | final results = chrono.parse(text, refDate, options);
71 | expect(results, hasLength(0));
72 | } catch (e) {
73 | debugHandler.executeBufferedBlocks();
74 | rethrow;
75 | }
76 | }
77 |
78 | int measureMilliSec(Function block) {
79 | final startTime = DateTime.now().millisecondsSinceEpoch;
80 | block();
81 | final endTime = DateTime.now().millisecondsSinceEpoch;
82 | return endTime - startTime;
83 | }
84 |
85 | Matcher toBeDate(DateTime matcher) => wrapMatcher((Object? item) {
86 | if (item is DateTime) {
87 | if (item.millisecondsSinceEpoch == matcher.millisecondsSinceEpoch) {
88 | return true;
89 | }
90 | throw 'Actual: $item, expected: $matcher';
91 | }
92 | if ((item as dynamic).date is Function) {
93 | final date = (item as dynamic).date() as DateTime;
94 | if (date.millisecondsSinceEpoch == matcher.millisecondsSinceEpoch) {
95 | return true;
96 | }
97 | throw 'Actual: $date, expected: $matcher';
98 | }
99 | throw 'Actual is not a date object';
100 | });
101 |
102 | Matcher toBeSingleOnText(Object? matcher) =>
103 | _SingleOnText(wrapMatcher(matcher));
104 |
105 | class _SingleOnText extends Matcher {
106 | final Matcher _matcher;
107 | _SingleOnText(this._matcher);
108 |
109 | @override
110 | bool matches(Object? item, Map matchState) {
111 | try {
112 | final length = (item as dynamic).length;
113 | return length == 1;
114 | } catch (e) {
115 | return false;
116 | }
117 | }
118 |
119 | @override
120 | Description describe(Description description) =>
121 | description.add("Got single result ").addDescriptionOf(_matcher);
122 |
123 | @override
124 | Description describeMismatch(Object? item, Description mismatchDescription,
125 | Map matchState, bool verbose) {
126 | try {
127 | final length = (item as dynamic).length;
128 | return mismatchDescription
129 | .add('Got ')
130 | .addDescriptionOf(length)
131 | .add(' results from ')
132 | .addDescriptionOf(item);
133 | } catch (e) {
134 | return mismatchDescription.add('has no length property');
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------