├── .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 | [![pub.dev/packages/chrono_dart](https://img.shields.io/pub/v/chrono_dart.svg "chrono_dart on pub.dev")](https://pub.dev/packages/chrono_dart) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 | chrono_dart 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 | --------------------------------------------------------------------------------