chrono-node 2.6.1 → 2.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +32 -2
  2. package/dist/cjs/common/refiners/AbstractMergeDateRangeRefiner.js +15 -7
  3. package/dist/cjs/common/refiners/AbstractMergeDateRangeRefiner.js.map +1 -1
  4. package/dist/cjs/common/refiners/ExtractTimezoneAbbrRefiner.d.ts +3 -4
  5. package/dist/cjs/common/refiners/ExtractTimezoneAbbrRefiner.js +6 -196
  6. package/dist/cjs/common/refiners/ExtractTimezoneAbbrRefiner.js.map +1 -1
  7. package/dist/cjs/common/refiners/ForwardDateRefiner.js +14 -14
  8. package/dist/cjs/common/refiners/ForwardDateRefiner.js.map +1 -1
  9. package/dist/cjs/configurations.js +1 -1
  10. package/dist/cjs/configurations.js.map +1 -1
  11. package/dist/cjs/index.d.ts +26 -4
  12. package/dist/cjs/index.js +16 -1
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/locales/en/constants.d.ts +4 -0
  15. package/dist/cjs/locales/en/constants.js +5 -18
  16. package/dist/cjs/locales/en/constants.js.map +1 -1
  17. package/dist/cjs/locales/en/index.js +1 -1
  18. package/dist/cjs/locales/en/index.js.map +1 -1
  19. package/dist/cjs/locales/en/parsers/ENMonthNameMiddleEndianParser.d.ts +1 -1
  20. package/dist/cjs/locales/en/parsers/ENMonthNameParser.d.ts +1 -1
  21. package/dist/cjs/locales/en/parsers/ENTimeUnitAgoFormatParser.js +2 -2
  22. package/dist/cjs/locales/en/parsers/ENTimeUnitAgoFormatParser.js.map +1 -1
  23. package/dist/cjs/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.d.ts +2 -0
  24. package/dist/cjs/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.js +6 -1
  25. package/dist/cjs/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.js.map +1 -1
  26. package/dist/cjs/locales/en/parsers/ENTimeUnitLaterFormatParser.js +1 -1
  27. package/dist/cjs/locales/en/parsers/ENTimeUnitLaterFormatParser.js.map +1 -1
  28. package/dist/cjs/locales/en/parsers/ENTimeUnitWithinFormatParser.d.ts +2 -0
  29. package/dist/cjs/locales/en/parsers/ENTimeUnitWithinFormatParser.js +10 -1
  30. package/dist/cjs/locales/en/parsers/ENTimeUnitWithinFormatParser.js.map +1 -1
  31. package/dist/cjs/locales/nl/parsers/NLMonthNameMiddleEndianParser.d.ts +1 -1
  32. package/dist/cjs/locales/ru/constants.js +4 -1
  33. package/dist/cjs/locales/ru/constants.js.map +1 -1
  34. package/dist/cjs/locales/ru/parsers/RUMonthNameParser.d.ts +1 -1
  35. package/dist/cjs/locales/zh/hans/parsers/ZHHansDateParser.d.ts +1 -1
  36. package/dist/cjs/locales/zh/hans/parsers/ZHHansDeadlineFormatParser.d.ts +1 -1
  37. package/dist/cjs/locales/zh/hans/parsers/ZHHansTimeExpressionParser.d.ts +1 -1
  38. package/dist/cjs/locales/zh/hant/parsers/ZHHantDateParser.d.ts +1 -1
  39. package/dist/cjs/locales/zh/hant/parsers/ZHHantDeadlineFormatParser.d.ts +1 -1
  40. package/dist/cjs/locales/zh/hant/parsers/ZHHantTimeExpressionParser.d.ts +1 -1
  41. package/dist/cjs/package.json +1 -1
  42. package/dist/cjs/results.d.ts +1 -1
  43. package/dist/cjs/results.js +3 -3
  44. package/dist/cjs/results.js.map +1 -1
  45. package/dist/cjs/timezone.d.ts +5 -194
  46. package/dist/cjs/timezone.js +80 -7
  47. package/dist/cjs/timezone.js.map +1 -1
  48. package/dist/esm/common/refiners/AbstractMergeDateRangeRefiner.js +15 -7
  49. package/dist/esm/common/refiners/AbstractMergeDateRangeRefiner.js.map +1 -1
  50. package/dist/esm/common/refiners/ExtractTimezoneAbbrRefiner.d.ts +3 -4
  51. package/dist/esm/common/refiners/ExtractTimezoneAbbrRefiner.js +6 -196
  52. package/dist/esm/common/refiners/ExtractTimezoneAbbrRefiner.js.map +1 -1
  53. package/dist/esm/common/refiners/ForwardDateRefiner.js +14 -14
  54. package/dist/esm/common/refiners/ForwardDateRefiner.js.map +1 -1
  55. package/dist/esm/configurations.js +1 -1
  56. package/dist/esm/configurations.js.map +1 -1
  57. package/dist/esm/index.d.ts +26 -4
  58. package/dist/esm/index.js +15 -0
  59. package/dist/esm/index.js.map +1 -1
  60. package/dist/esm/locales/en/constants.d.ts +4 -0
  61. package/dist/esm/locales/en/constants.js +21 -0
  62. package/dist/esm/locales/en/constants.js.map +1 -1
  63. package/dist/esm/locales/en/index.js +1 -1
  64. package/dist/esm/locales/en/index.js.map +1 -1
  65. package/dist/esm/locales/en/parsers/ENMonthNameMiddleEndianParser.d.ts +1 -1
  66. package/dist/esm/locales/en/parsers/ENMonthNameParser.d.ts +1 -1
  67. package/dist/esm/locales/en/parsers/ENTimeUnitAgoFormatParser.js +3 -3
  68. package/dist/esm/locales/en/parsers/ENTimeUnitAgoFormatParser.js.map +1 -1
  69. package/dist/esm/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.d.ts +2 -0
  70. package/dist/esm/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.js +7 -2
  71. package/dist/esm/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.js.map +1 -1
  72. package/dist/esm/locales/en/parsers/ENTimeUnitLaterFormatParser.js +2 -2
  73. package/dist/esm/locales/en/parsers/ENTimeUnitLaterFormatParser.js.map +1 -1
  74. package/dist/esm/locales/en/parsers/ENTimeUnitWithinFormatParser.d.ts +2 -0
  75. package/dist/esm/locales/en/parsers/ENTimeUnitWithinFormatParser.js +11 -2
  76. package/dist/esm/locales/en/parsers/ENTimeUnitWithinFormatParser.js.map +1 -1
  77. package/dist/esm/locales/nl/parsers/NLMonthNameMiddleEndianParser.d.ts +1 -1
  78. package/dist/esm/locales/ru/constants.js +4 -1
  79. package/dist/esm/locales/ru/constants.js.map +1 -1
  80. package/dist/esm/locales/ru/parsers/RUMonthNameParser.d.ts +1 -1
  81. package/dist/esm/locales/zh/hans/parsers/ZHHansDateParser.d.ts +1 -1
  82. package/dist/esm/locales/zh/hans/parsers/ZHHansDeadlineFormatParser.d.ts +1 -1
  83. package/dist/esm/locales/zh/hans/parsers/ZHHansTimeExpressionParser.d.ts +1 -1
  84. package/dist/esm/locales/zh/hant/parsers/ZHHantDateParser.d.ts +1 -1
  85. package/dist/esm/locales/zh/hant/parsers/ZHHantDeadlineFormatParser.d.ts +1 -1
  86. package/dist/esm/locales/zh/hant/parsers/ZHHantTimeExpressionParser.d.ts +1 -1
  87. package/dist/esm/package.json +1 -1
  88. package/dist/esm/results.d.ts +1 -1
  89. package/dist/esm/results.js +3 -3
  90. package/dist/esm/results.js.map +1 -1
  91. package/dist/esm/timezone.d.ts +5 -194
  92. package/dist/esm/timezone.js +74 -6
  93. package/dist/esm/timezone.js.map +1 -1
  94. package/package.json +6 -7
  95. package/src/common/refiners/AbstractMergeDateRangeRefiner.ts +13 -8
  96. package/src/common/refiners/ExtractTimezoneAbbrRefiner.ts +7 -201
  97. package/src/common/refiners/ForwardDateRefiner.ts +18 -16
  98. package/src/configurations.ts +3 -1
  99. package/src/index.ts +48 -3
  100. package/src/locales/en/constants.ts +30 -0
  101. package/src/locales/en/index.ts +1 -1
  102. package/src/locales/en/parsers/ENTimeUnitAgoFormatParser.ts +3 -3
  103. package/src/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.ts +10 -2
  104. package/src/locales/en/parsers/ENTimeUnitLaterFormatParser.ts +5 -2
  105. package/src/locales/en/parsers/ENTimeUnitWithinFormatParser.ts +16 -3
  106. package/src/locales/ru/constants.ts +4 -1
  107. package/src/results.ts +3 -3
  108. package/src/timezone.ts +124 -7
  109. package/test/de/de_time_exp.test.ts +7 -7
  110. package/test/en/en_month.test.ts +70 -0
  111. package/test/en/en_time_units_ago.test.ts +12 -0
  112. package/test/en/en_time_units_casual_relative.test.ts +18 -0
  113. package/test/en/en_time_units_later.test.ts +13 -10
  114. package/test/en/en_time_units_within.test.ts +12 -1
  115. package/test/en/en_timezone_exp.test.ts +142 -0
  116. package/test/en/en_weekday.test.ts +26 -0
  117. package/test/en/negative_cases.test.ts +2 -0
  118. package/test/fr/fr_time_exp.test.ts +2 -2
  119. package/test/system.test.ts +23 -1
@@ -30,22 +30,6 @@ export default class ForwardDateRefiner implements Refiner {
30
30
  }
31
31
  }
32
32
 
33
- if (result.start.isOnlyDayMonthComponent() && refMoment.isAfter(result.start.dayjs())) {
34
- for (let i = 0; i < 3 && refMoment.isAfter(result.start.dayjs()); i++) {
35
- result.start.imply("year", result.start.get("year") + 1);
36
- context.debug(() => {
37
- console.log(`Forward yearly adjusted for ${result} (${result.start})`);
38
- });
39
-
40
- if (result.end && !result.end.isCertain("year")) {
41
- result.end.imply("year", result.end.get("year") + 1);
42
- context.debug(() => {
43
- console.log(`Forward yearly adjusted for ${result} (${result.end})`);
44
- });
45
- }
46
- }
47
- }
48
-
49
33
  if (result.start.isOnlyWeekdayComponent() && refMoment.isAfter(result.start.dayjs())) {
50
34
  if (refMoment.day() >= result.start.get("weekday")) {
51
35
  refMoment = refMoment.day(result.start.get("weekday") + 7);
@@ -76,6 +60,24 @@ export default class ForwardDateRefiner implements Refiner {
76
60
  });
77
61
  }
78
62
  }
63
+
64
+ // In case where we know the month, but not which year (e.g. "in December", "25th December"),
65
+ // try move to another year
66
+ if (result.start.isDateWithUnknownYear() && refMoment.isAfter(result.start.dayjs())) {
67
+ for (let i = 0; i < 3 && refMoment.isAfter(result.start.dayjs()); i++) {
68
+ result.start.imply("year", result.start.get("year") + 1);
69
+ context.debug(() => {
70
+ console.log(`Forward yearly adjusted for ${result} (${result.start})`);
71
+ });
72
+
73
+ if (result.end && !result.end.isCertain("year")) {
74
+ result.end.imply("year", result.end.get("year") + 1);
75
+ context.debug(() => {
76
+ console.log(`Forward yearly adjusted for ${result} (${result.end})`);
77
+ });
78
+ }
79
+ }
80
+ }
79
81
  });
80
82
 
81
83
  return results;
@@ -12,10 +12,12 @@ export function includeCommonConfiguration(configuration: Configuration, strictM
12
12
  configuration.parsers.unshift(new ISOFormatParser());
13
13
 
14
14
  configuration.refiners.unshift(new MergeWeekdayComponentRefiner());
15
- configuration.refiners.unshift(new ExtractTimezoneAbbrRefiner());
16
15
  configuration.refiners.unshift(new ExtractTimezoneOffsetRefiner());
17
16
  configuration.refiners.unshift(new OverlapRemovalRefiner());
18
17
 
18
+ // Unlike ExtractTimezoneOffsetRefiner, this refiner relies on knowing both date and time in cases where the tz
19
+ // is ambiguous (in terms of DST/non-DST). It therefore needs to be applied as late as possible in the parsing.
20
+ configuration.refiners.push(new ExtractTimezoneAbbrRefiner());
19
21
  configuration.refiners.push(new OverlapRemovalRefiner());
20
22
  configuration.refiners.push(new ForwardDateRefiner());
21
23
  configuration.refiners.push(new UnlikelyFormatFilter(strictMode));
package/src/index.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { DebugHandler, DebugConsume } from "./debugging";
2
2
  import * as en from "./locales/en";
3
3
  import { Chrono, Parser, Refiner } from "./chrono";
4
+ import type { ParsingResult } from "./results";
4
5
 
5
- export { en, Chrono, Parser, Refiner };
6
+ export { en, Chrono, Parser, Refiner, ParsingResult };
6
7
 
7
8
  export interface ParsingOption {
8
9
  /**
@@ -12,9 +13,10 @@ export interface ParsingOption {
12
13
  forwardDate?: boolean;
13
14
 
14
15
  /**
15
- * Additional timezone keywords for the parsers to recognize
16
+ * Additional timezone keywords for the parsers to recognize.
17
+ * Any value provided will override the default handling of that value.
16
18
  */
17
- timezones?: { [tzKeyword: string]: number };
19
+ timezones?: TimezoneAbbrMap;
18
20
 
19
21
  /**
20
22
  * Internal debug event handler.
@@ -23,6 +25,34 @@ export interface ParsingOption {
23
25
  debug?: DebugHandler | DebugConsume;
24
26
  }
25
27
 
28
+ /**
29
+ * Some timezone abbreviations are ambiguous in that they refer to different offsets
30
+ * depending on the time of year — daylight savings time (DST), or non-DST. This interface
31
+ * allows defining such timezones
32
+ */
33
+ export interface AmbiguousTimezoneMap {
34
+ timezoneOffsetDuringDst: number;
35
+ timezoneOffsetNonDst: number;
36
+ /**
37
+ * Return the start date of DST for the given year.
38
+ * timezone.ts contains helper methods for common such rules.
39
+ */
40
+ dstStart: (year: number) => Date;
41
+ /**
42
+ * Return the end date of DST for the given year.
43
+ * timezone.ts contains helper methods for common such rules.
44
+ */
45
+ dstEnd: (year: number) => Date;
46
+ }
47
+
48
+ /**
49
+ * A map describing how timezone abbreviations should map to time offsets.
50
+ * Supports both unambigous mappings abbreviation => offset,
51
+ * and ambiguous mappings, where the offset will depend on whether the
52
+ * time in question is during daylight savings time or not.
53
+ */
54
+ export type TimezoneAbbrMap = { [key: string]: number | AmbiguousTimezoneMap };
55
+
26
56
  export interface ParsingReference {
27
57
  /**
28
58
  * Reference date. The instant (JavaScript Date object) when the input is written or mention.
@@ -109,6 +139,21 @@ export enum Weekday {
109
139
  SATURDAY = 6,
110
140
  }
111
141
 
142
+ export enum Month {
143
+ JANUARY = 1,
144
+ FEBRUARY = 2,
145
+ MARCH = 3,
146
+ APRIL = 4,
147
+ MAY = 5,
148
+ JUNE = 6,
149
+ JULY = 7,
150
+ AUGUST = 8,
151
+ SEPTEMBER = 9,
152
+ OCTOBER = 10,
153
+ NOVEMBER = 11,
154
+ DECEMBER = 12,
155
+ }
156
+
112
157
  // Export all locales
113
158
  import * as de from "./locales/de";
114
159
  import * as fr from "./locales/fr";
@@ -134,6 +134,25 @@ export const ORDINAL_WORD_DICTIONARY: { [word: string]: number } = {
134
134
  "thirty-first": 31,
135
135
  };
136
136
 
137
+ export const TIME_UNIT_DICTIONARY_NO_ABBR: { [word: string]: OpUnitType | QUnitType } = {
138
+ second: "second",
139
+ seconds: "second",
140
+ minute: "minute",
141
+ minutes: "minute",
142
+ hour: "hour",
143
+ hours: "hour",
144
+ day: "d",
145
+ days: "d",
146
+ week: "week",
147
+ weeks: "week",
148
+ month: "month",
149
+ months: "month",
150
+ quarter: "quarter",
151
+ quarters: "quarter",
152
+ year: "year",
153
+ years: "year",
154
+ };
155
+
137
156
  export const TIME_UNIT_DICTIONARY: { [word: string]: OpUnitType | QUnitType } = {
138
157
  s: "second",
139
158
  sec: "second",
@@ -167,6 +186,9 @@ export const TIME_UNIT_DICTIONARY: { [word: string]: OpUnitType | QUnitType } =
167
186
  yr: "year",
168
187
  year: "year",
169
188
  years: "year",
189
+ // Also, merge the entries from the full-name dictionary.
190
+ // We leave the duplicated entries for readability.
191
+ ...TIME_UNIT_DICTIONARY_NO_ABBR,
170
192
  };
171
193
 
172
194
  //-----------------------------
@@ -238,7 +260,15 @@ export function parseYear(match: string): number {
238
260
  const SINGLE_TIME_UNIT_PATTERN = `(${NUMBER_PATTERN})\\s{0,3}(${matchAnyPattern(TIME_UNIT_DICTIONARY)})`;
239
261
  const SINGLE_TIME_UNIT_REGEX = new RegExp(SINGLE_TIME_UNIT_PATTERN, "i");
240
262
 
263
+ const SINGLE_TIME_UNIT_NO_ABBR_PATTERN = `(${NUMBER_PATTERN})\\s{0,3}(${matchAnyPattern(
264
+ TIME_UNIT_DICTIONARY_NO_ABBR
265
+ )})`;
266
+
241
267
  export const TIME_UNITS_PATTERN = repeatedTimeunitPattern(`(?:(?:about|around)\\s{0,3})?`, SINGLE_TIME_UNIT_PATTERN);
268
+ export const TIME_UNITS_NO_ABBR_PATTERN = repeatedTimeunitPattern(
269
+ `(?:(?:about|around)\\s{0,3})?`,
270
+ SINGLE_TIME_UNIT_NO_ABBR_PATTERN
271
+ );
242
272
 
243
273
  export function parseTimeUnits(timeunitText): TimeUnits {
244
274
  const fragments = {};
@@ -82,7 +82,7 @@ export function createConfiguration(strictMode = true, littleEndian = false): Co
82
82
  {
83
83
  parsers: [
84
84
  new SlashDateFormatParser(littleEndian),
85
- new ENTimeUnitWithinFormatParser(),
85
+ new ENTimeUnitWithinFormatParser(strictMode),
86
86
  new ENMonthNameLittleEndianParser(),
87
87
  new ENMonthNameMiddleEndianParser(),
88
88
  new ENWeekdayParser(),
@@ -1,11 +1,11 @@
1
1
  import { ParsingContext } from "../../../chrono";
2
- import { parseTimeUnits, TIME_UNITS_PATTERN } from "../constants";
2
+ import { parseTimeUnits, TIME_UNITS_NO_ABBR_PATTERN, TIME_UNITS_PATTERN } from "../constants";
3
3
  import { ParsingComponents } from "../../../results";
4
4
  import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary";
5
5
  import { reverseTimeUnits } from "../../../utils/timeunits";
6
6
 
7
- const PATTERN = new RegExp(`(${TIME_UNITS_PATTERN})\\s{0,5}(?:ago|before|earlier)(?=(?:\\W|$))`, "i");
8
- const STRICT_PATTERN = new RegExp(`(${TIME_UNITS_PATTERN})\\s{0,5}ago(?=(?:\\W|$))`, "i");
7
+ const PATTERN = new RegExp(`(${TIME_UNITS_PATTERN})\\s{0,5}(?:ago|before|earlier)(?=\\W|$)`, "i");
8
+ const STRICT_PATTERN = new RegExp(`(${TIME_UNITS_NO_ABBR_PATTERN})\\s{0,5}(?:ago|before|earlier)(?=\\W|$)`, "i");
9
9
 
10
10
  export default class ENTimeUnitAgoFormatParser extends AbstractParserWithWordBoundaryChecking {
11
11
  constructor(private strictMode: boolean) {
@@ -1,14 +1,22 @@
1
- import { TIME_UNITS_PATTERN, parseTimeUnits } from "../constants";
1
+ import { TIME_UNITS_PATTERN, parseTimeUnits, TIME_UNITS_NO_ABBR_PATTERN } from "../constants";
2
2
  import { ParsingContext } from "../../../chrono";
3
3
  import { ParsingComponents } from "../../../results";
4
4
  import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary";
5
5
  import { reverseTimeUnits } from "../../../utils/timeunits";
6
6
 
7
7
  const PATTERN = new RegExp(`(this|last|past|next|after|\\+|-)\\s*(${TIME_UNITS_PATTERN})(?=\\W|$)`, "i");
8
+ const PATTERN_NO_ABBR = new RegExp(
9
+ `(this|last|past|next|after|\\+|-)\\s*(${TIME_UNITS_NO_ABBR_PATTERN})(?=\\W|$)`,
10
+ "i"
11
+ );
8
12
 
9
13
  export default class ENTimeUnitCasualRelativeFormatParser extends AbstractParserWithWordBoundaryChecking {
14
+ constructor(private allowAbbreviations: boolean = true) {
15
+ super();
16
+ }
17
+
10
18
  innerPattern(): RegExp {
11
- return PATTERN;
19
+ return this.allowAbbreviations ? PATTERN : PATTERN_NO_ABBR;
12
20
  }
13
21
 
14
22
  innerExtract(context: ParsingContext, match: RegExpMatchArray): ParsingComponents {
@@ -1,5 +1,5 @@
1
1
  import { ParsingContext } from "../../../chrono";
2
- import { parseTimeUnits, TIME_UNITS_PATTERN } from "../constants";
2
+ import { parseTimeUnits, TIME_UNITS_NO_ABBR_PATTERN, TIME_UNITS_PATTERN } from "../constants";
3
3
  import { ParsingComponents } from "../../../results";
4
4
  import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary";
5
5
 
@@ -8,7 +8,10 @@ const PATTERN = new RegExp(
8
8
  "i"
9
9
  );
10
10
 
11
- const STRICT_PATTERN = new RegExp("" + "(" + TIME_UNITS_PATTERN + ")" + "(later|from now)" + "(?=(?:\\W|$))", "i");
11
+ const STRICT_PATTERN = new RegExp(
12
+ "" + "(" + TIME_UNITS_NO_ABBR_PATTERN + ")" + "(later|from now)" + "(?=(?:\\W|$))",
13
+ "i"
14
+ );
12
15
  const GROUP_NUM_TIMEUNITS = 1;
13
16
 
14
17
  export default class ENTimeUnitLaterFormatParser extends AbstractParserWithWordBoundaryChecking {
@@ -1,21 +1,34 @@
1
- import { TIME_UNITS_PATTERN, parseTimeUnits } from "../constants";
1
+ import { TIME_UNITS_PATTERN, parseTimeUnits, TIME_UNITS_NO_ABBR_PATTERN } from "../constants";
2
2
  import { ParsingContext } from "../../../chrono";
3
3
  import { ParsingComponents } from "../../../results";
4
4
  import { AbstractParserWithWordBoundaryChecking } from "../../../common/parsers/AbstractParserWithWordBoundary";
5
5
 
6
+ const PATTERN_WITHOUT_PREFIX = new RegExp(
7
+ `(?:(?:about|around|roughly|approximately|just)\\s*(?:~\\s*)?)?(${TIME_UNITS_PATTERN})(?=\\W|$)`,
8
+ "i"
9
+ );
10
+
6
11
  const PATTERN_WITH_PREFIX = new RegExp(
7
12
  `(?:within|in|for)\\s*` +
8
13
  `(?:(?:about|around|roughly|approximately|just)\\s*(?:~\\s*)?)?(${TIME_UNITS_PATTERN})(?=\\W|$)`,
9
14
  "i"
10
15
  );
11
16
 
12
- const PATTERN_WITHOUT_PREFIX = new RegExp(
13
- `(?:(?:about|around|roughly|approximately|just)\\s*(?:~\\s*)?)?(${TIME_UNITS_PATTERN})(?=\\W|$)`,
17
+ const PATTERN_WITH_PREFIX_STRICT = new RegExp(
18
+ `(?:within|in|for)\\s*` +
19
+ `(?:(?:about|around|roughly|approximately|just)\\s*(?:~\\s*)?)?(${TIME_UNITS_NO_ABBR_PATTERN})(?=\\W|$)`,
14
20
  "i"
15
21
  );
16
22
 
17
23
  export default class ENTimeUnitWithinFormatParser extends AbstractParserWithWordBoundaryChecking {
24
+ constructor(private strictMode: boolean) {
25
+ super();
26
+ }
27
+
18
28
  innerPattern(context: ParsingContext): RegExp {
29
+ if (this.strictMode) {
30
+ return PATTERN_WITH_PREFIX_STRICT;
31
+ }
19
32
  return context.option.forwardDate ? PATTERN_WITHOUT_PREFIX : PATTERN_WITH_PREFIX;
20
33
  }
21
34
 
@@ -219,6 +219,9 @@ export const TIME_UNIT_DICTIONARY: { [word: string]: OpUnitType | QUnitType } =
219
219
  минуток: "minute",
220
220
  минутки: "minute",
221
221
  минутку: "minute",
222
+ минуточек: "minute",
223
+ минуточки: "minute",
224
+ минуточку: "minute",
222
225
  час: "hour",
223
226
  часов: "hour",
224
227
  часа: "hour",
@@ -284,7 +287,7 @@ export function parseNumberPattern(match: string): number {
284
287
 
285
288
  export const ORDINAL_NUMBER_PATTERN = `(?:${matchAnyPattern(ORDINAL_WORD_DICTIONARY)}|[0-9]{1,2}(?:го|ого|е|ое)?)`;
286
289
  export function parseOrdinalNumberPattern(match: string): number {
287
- let num = match.toLowerCase();
290
+ const num = match.toLowerCase();
288
291
  if (ORDINAL_WORD_DICTIONARY[num] !== undefined) {
289
292
  return ORDINAL_WORD_DICTIONARY[num];
290
293
  }
package/src/results.ts CHANGED
@@ -16,7 +16,7 @@ export class ReferenceWithTimezone {
16
16
  this.instant = input;
17
17
  } else {
18
18
  this.instant = input.instant ?? new Date();
19
- this.timezoneOffset = toTimezoneOffset(input.timezone);
19
+ this.timezoneOffset = toTimezoneOffset(input.timezone, this.instant);
20
20
  }
21
21
  }
22
22
 
@@ -138,8 +138,8 @@ export class ParsingComponents implements ParsedComponents {
138
138
  return this.isCertain("weekday") && !this.isCertain("day") && !this.isCertain("month");
139
139
  }
140
140
 
141
- isOnlyDayMonthComponent(): boolean {
142
- return this.isCertain("day") && this.isCertain("month") && !this.isCertain("year");
141
+ isDateWithUnknownYear(): boolean {
142
+ return this.isCertain("month") && !this.isCertain("year");
143
143
  }
144
144
 
145
145
  isValidDate(): boolean {
package/src/timezone.ts CHANGED
@@ -1,4 +1,7 @@
1
- export const TIMEZONE_ABBR_MAP = {
1
+ import dayjs from "dayjs";
2
+ import { TimezoneAbbrMap, Weekday, Month } from "./index";
3
+
4
+ export const TIMEZONE_ABBR_MAP: TimezoneAbbrMap = {
2
5
  ACDT: 630,
3
6
  ACST: 570,
4
7
  ADT: -180,
@@ -32,7 +35,14 @@ export const TIMEZONE_ABBR_MAP = {
32
35
  CCT: 390,
33
36
  CDT: -300,
34
37
  CEST: 120,
35
- CET: 60,
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: (year: number) => getLastWeekdayOfMonth(year, Month.MARCH, Weekday.SUNDAY, 2),
44
+ dstEnd: (year: number) => getLastWeekdayOfMonth(year, Month.OCTOBER, Weekday.SUNDAY, 3),
45
+ },
36
46
  CHADT: 825,
37
47
  CHAST: 765,
38
48
  CKT: -600,
@@ -40,6 +50,12 @@ export const TIMEZONE_ABBR_MAP = {
40
50
  CLT: -240,
41
51
  COT: -300,
42
52
  CST: -360,
53
+ CT: {
54
+ timezoneOffsetDuringDst: -5 * 60,
55
+ timezoneOffsetNonDst: -6 * 60,
56
+ dstStart: (year: number) => getNthWeekdayOfMonth(year, Month.MARCH, Weekday.SUNDAY, 2, 2),
57
+ dstEnd: (year: number) => getNthWeekdayOfMonth(year, Month.NOVEMBER, Weekday.SUNDAY, 1, 2),
58
+ },
43
59
  CVT: -60,
44
60
  CXT: 420,
45
61
  ChST: 600,
@@ -54,7 +70,12 @@ export const TIMEZONE_ABBR_MAP = {
54
70
  EGST: 0,
55
71
  EGT: -60,
56
72
  EST: -300,
57
- ET: -300,
73
+ ET: {
74
+ timezoneOffsetDuringDst: -4 * 60,
75
+ timezoneOffsetNonDst: -5 * 60,
76
+ dstStart: (year: number) => getNthWeekdayOfMonth(year, Month.MARCH, Weekday.SUNDAY, 2, 2),
77
+ dstEnd: (year: number) => getNthWeekdayOfMonth(year, Month.NOVEMBER, Weekday.SUNDAY, 1, 2),
78
+ },
58
79
  FJST: 780,
59
80
  FJT: 720,
60
81
  FKST: -180,
@@ -116,6 +137,12 @@ export const TIMEZONE_ABBR_MAP = {
116
137
  MSD: 240,
117
138
  MSK: 180,
118
139
  MST: -420,
140
+ MT: {
141
+ timezoneOffsetDuringDst: -6 * 60,
142
+ timezoneOffsetNonDst: -7 * 60,
143
+ dstStart: (year: number) => getNthWeekdayOfMonth(year, Month.MARCH, Weekday.SUNDAY, 2, 2),
144
+ dstEnd: (year: number) => getNthWeekdayOfMonth(year, Month.NOVEMBER, Weekday.SUNDAY, 1, 2),
145
+ },
119
146
  MUT: 240,
120
147
  MVT: 300,
121
148
  MYT: 480,
@@ -143,7 +170,12 @@ export const TIMEZONE_ABBR_MAP = {
143
170
  PMST: -180,
144
171
  PONT: 660,
145
172
  PST: -480,
146
- PT: -480,
173
+ PT: {
174
+ timezoneOffsetDuringDst: -7 * 60,
175
+ timezoneOffsetNonDst: -8 * 60,
176
+ dstStart: (year: number) => getNthWeekdayOfMonth(year, Month.MARCH, Weekday.SUNDAY, 2, 2),
177
+ dstEnd: (year: number) => getNthWeekdayOfMonth(year, Month.NOVEMBER, Weekday.SUNDAY, 1, 2),
178
+ },
147
179
  PWT: 540,
148
180
  PYST: -180,
149
181
  PYT: -240,
@@ -192,8 +224,67 @@ export const TIMEZONE_ABBR_MAP = {
192
224
  YEKT: 360,
193
225
  };
194
226
 
195
- export function toTimezoneOffset(timezoneInput?: string | number): number | null {
196
- if (timezoneInput === null || timezoneInput === undefined) {
227
+ /**
228
+ * Get the date which is the nth occurence of a given weekday in a given month and year.
229
+ *
230
+ * @param year The year for which to find the date
231
+ * @param month The month in which the date occurs
232
+ * @param weekday The weekday on which the date occurs
233
+ * @param n The nth occurence of the given weekday on the month to return
234
+ * @param hour The hour of day which should be set on the returned date
235
+ * @return The date which is the nth occurence of a given weekday in a given
236
+ * month and year, at the given hour of day
237
+ */
238
+ export function getNthWeekdayOfMonth(year: number, month: Month, weekday: Weekday, n: 1 | 2 | 3 | 4, hour = 0): Date {
239
+ let dayOfMonth = 0;
240
+ let i = 0;
241
+ while (i < n) {
242
+ dayOfMonth++;
243
+ const date = new Date(year, month - 1, dayOfMonth);
244
+ if (date.getDay() === weekday) i++;
245
+ }
246
+ return new Date(year, month - 1, dayOfMonth, hour);
247
+ }
248
+
249
+ /**
250
+ * Get the date which is the last occurence of a given weekday in a given month and year.
251
+ *
252
+ * @param year The year for which to find the date
253
+ * @param month The month in which the date occurs
254
+ * @param weekday The weekday on which the date occurs
255
+ * @param hour The hour of day which should be set on the returned date
256
+ * @return The date which is the last occurence of a given weekday in a given
257
+ * month and year, at the given hour of day
258
+ */
259
+ export function getLastWeekdayOfMonth(year: number, month: Month, weekday: Weekday, hour = 0): Date {
260
+ // Procedure: Find the first weekday of the next month, compare with the given weekday,
261
+ // and use the difference to determine how many days to subtract from the first of the next month.
262
+ const oneIndexedWeekday = weekday === 0 ? 7 : weekday;
263
+ const date = new Date(year, month - 1 + 1, 1, 12);
264
+ const firstWeekdayNextMonth = date.getDay() === 0 ? 7 : date.getDay();
265
+ let dayDiff;
266
+ if (firstWeekdayNextMonth === oneIndexedWeekday) dayDiff = 7;
267
+ else if (firstWeekdayNextMonth < oneIndexedWeekday) dayDiff = 7 + firstWeekdayNextMonth - oneIndexedWeekday;
268
+ else dayDiff = firstWeekdayNextMonth - oneIndexedWeekday;
269
+ date.setDate(date.getDate() - dayDiff);
270
+ return new Date(year, month - 1, date.getDate(), hour);
271
+ }
272
+
273
+ /**
274
+ * Finds and returns timezone offset. If timezoneInput is numeric, it is returned. Otherwise, look for timezone offsets
275
+ * in the following order: timezoneOverrides -> {@link TIMEZONE_ABBR_MAP}.
276
+ *
277
+ * @param timezoneInput Uppercase timezone abbreviation or numeric offset in minutes
278
+ * @param date The date to use to determine whether to return DST offsets for ambiguous timezones
279
+ * @param timezoneOverrides Overrides for timezones
280
+ * @return timezone offset in minutes
281
+ */
282
+ export function toTimezoneOffset(
283
+ timezoneInput?: string | number,
284
+ date?: Date,
285
+ timezoneOverrides: TimezoneAbbrMap = {}
286
+ ): number | null {
287
+ if (timezoneInput == null) {
197
288
  return null;
198
289
  }
199
290
 
@@ -201,5 +292,31 @@ export function toTimezoneOffset(timezoneInput?: string | number): number | null
201
292
  return timezoneInput;
202
293
  }
203
294
 
204
- return TIMEZONE_ABBR_MAP[timezoneInput] ?? null;
295
+ const matchedTimezone = timezoneOverrides[timezoneInput] ?? TIMEZONE_ABBR_MAP[timezoneInput];
296
+ if (matchedTimezone == null) {
297
+ return null;
298
+ }
299
+ // This means that we have matched an unambiguous timezone
300
+ if (typeof matchedTimezone == "number") {
301
+ return matchedTimezone;
302
+ }
303
+
304
+ // The matched timezone is an ambiguous timezone, where the offset depends on whether the context (refDate)
305
+ // is during daylight savings or not.
306
+
307
+ // Without refDate as context, there's no way to know if DST or non-DST offset should be used. Return null instead.
308
+ if (date == null) {
309
+ return null;
310
+ }
311
+
312
+ // Return DST offset if the refDate is during daylight savings
313
+ if (
314
+ dayjs(date).isAfter(matchedTimezone.dstStart(date.getFullYear())) &&
315
+ !dayjs(date).isAfter(matchedTimezone.dstEnd(date.getFullYear()))
316
+ ) {
317
+ return matchedTimezone.timezoneOffsetDuringDst;
318
+ }
319
+
320
+ // refDate is not during DST => return non-DST offset
321
+ return matchedTimezone.timezoneOffsetNonDst;
205
322
  }
@@ -180,22 +180,22 @@ test("Test - Range Expression", function () {
180
180
  });
181
181
 
182
182
  test("Test - Timezone extraction", function () {
183
- testSingleCase(chrono.de, "um 14 Uhr", new Date(2016, 3, 28), (result, text) => {
183
+ testSingleCase(chrono.de, "um 14 Uhr", new Date(2016, 1, 28), (result, text) => {
184
184
  expect(result.text).toBe(text);
185
185
  expect(result.start.isCertain("timezoneOffset")).toBe(false);
186
186
  expect(!result.start.get("timezoneOffset")).not.toBeNull();
187
187
  });
188
188
 
189
- testSingleCase(chrono.de, "um 14 Uhr CET", new Date(2016, 3, 28), (result, text) => {
189
+ testSingleCase(chrono.de, "um 14 Uhr CET", new Date(2016, 1, 28), (result, text) => {
190
190
  expect(result.text).toBe(text);
191
191
  expect(result.start.isCertain("timezoneOffset")).toBe(true);
192
192
  expect(result.start.get("timezoneOffset")).toBe(60);
193
193
  });
194
194
 
195
- testSingleCase(chrono.de, "14 Uhr cet", new Date(2016, 3, 28), (result, text) => {
195
+ testSingleCase(chrono.de, "14 Uhr cet", new Date(2016, 4, 28), (result, text) => {
196
196
  expect(result.text).toBe(text);
197
197
  expect(result.start.isCertain("timezoneOffset")).toBe(true);
198
- expect(result.start.get("timezoneOffset")).toBe(60);
198
+ expect(result.start.get("timezoneOffset")).toBe(120);
199
199
  });
200
200
  });
201
201
 
@@ -209,15 +209,15 @@ test("Test - Random date + time expression", function () {
209
209
  expect(result.start.get("hour")).toBe(12);
210
210
  });
211
211
 
212
- testSingleCase(chrono.de, "am Freitag um 14 Uhr cetteln wir etwas an", new Date(2016, 3, 28), (result, text) => {
212
+ testSingleCase(chrono.de, "am Freitag um 14 Uhr cetteln wir etwas an", new Date(2016, 1, 28), (result, text) => {
213
213
  expect(result.text).toBe("am Freitag um 14 Uhr");
214
214
  expect(result.start.isCertain("timezoneOffset")).toBe(false);
215
215
  expect(!result.start.get("timezoneOffset")).not.toBeNull();
216
216
  });
217
217
 
218
- testSingleCase(chrono.de, "Freitag um 14 Uhr CET", new Date(2016, 3, 28), (result, text) => {
218
+ testSingleCase(chrono.de, "Freitag um 14 Uhr CET", new Date(2016, 4, 28), (result, text) => {
219
219
  expect(result.text).toBe(text);
220
220
  expect(result.start.isCertain("timezoneOffset")).toBe(true);
221
- expect(result.start.get("timezoneOffset")).toBe(60);
221
+ expect(result.start.get("timezoneOffset")).toBe(120);
222
222
  });
223
223
  });
@@ -117,6 +117,76 @@ test("Test - Month-Only expression", function () {
117
117
  });
118
118
  });
119
119
 
120
+ test("Test - Month-Only Range expression", () => {
121
+ testSingleCase(chrono, "From May to December", new Date(2023, 4 - 1, 9), (result) => {
122
+ expect(result.start.get("year")).toBe(2023);
123
+ expect(result.start.get("month")).toBe(5);
124
+ expect(result.end.get("year")).toBe(2023);
125
+ expect(result.end.get("month")).toBe(12);
126
+ });
127
+
128
+ testSingleCase(chrono, "From December to May", new Date(2023, 4 - 1, 9), (result) => {
129
+ expect(result.start.get("year")).toBe(2022);
130
+ expect(result.start.get("month")).toBe(12);
131
+ expect(result.end.get("year")).toBe(2023);
132
+ expect(result.end.get("month")).toBe(5);
133
+ });
134
+
135
+ testSingleCase(chrono, "From May to December, 2022", new Date(2023, 4 - 1, 9), (result) => {
136
+ expect(result.start.get("year")).toBe(2022);
137
+ expect(result.start.get("month")).toBe(5);
138
+ expect(result.end.get("year")).toBe(2022);
139
+ expect(result.end.get("month")).toBe(12);
140
+ });
141
+
142
+ testSingleCase(chrono, "From December to May 2022", new Date(2023, 4 - 1, 9), (result) => {
143
+ expect(result.start.get("year")).toBe(2021);
144
+ expect(result.start.get("month")).toBe(12);
145
+ expect(result.end.get("year")).toBe(2022);
146
+ expect(result.end.get("month")).toBe(5);
147
+ });
148
+
149
+ testSingleCase(chrono, "From December to May 2020", new Date(2023, 4 - 1, 9), (result) => {
150
+ expect(result.start.get("year")).toBe(2019);
151
+ expect(result.start.get("month")).toBe(12);
152
+ expect(result.end.get("year")).toBe(2020);
153
+ expect(result.end.get("month")).toBe(5);
154
+ });
155
+
156
+ testSingleCase(chrono, "From December to May 2025", new Date(2023, 4 - 1, 9), (result) => {
157
+ expect(result.start.get("year")).toBe(2024);
158
+ expect(result.start.get("month")).toBe(12);
159
+ expect(result.end.get("year")).toBe(2025);
160
+ expect(result.end.get("month")).toBe(5);
161
+ });
162
+ });
163
+
164
+ test("Test - Month with farward date option", () => {
165
+ testSingleCase(chrono, "in December", new Date(2023, 4 - 1, 9), { forwardDate: true }, (result) => {
166
+ expect(result.start.get("year")).toBe(2023);
167
+ expect(result.start.get("month")).toBe(12);
168
+ });
169
+
170
+ testSingleCase(chrono, "in May", new Date(2023, 4 - 1, 9), { forwardDate: true }, (result) => {
171
+ expect(result.start.get("year")).toBe(2023);
172
+ expect(result.start.get("month")).toBe(5);
173
+ });
174
+
175
+ testSingleCase(chrono, "From May to December", new Date(2023, 4 - 1, 9), { forwardDate: true }, (result) => {
176
+ expect(result.start.get("year")).toBe(2023);
177
+ expect(result.start.get("month")).toBe(5);
178
+ expect(result.end.get("year")).toBe(2023);
179
+ expect(result.end.get("month")).toBe(12);
180
+ });
181
+
182
+ testSingleCase(chrono, "From December to May", new Date(2023, 4 - 1, 9), { forwardDate: true }, (result) => {
183
+ expect(result.start.get("year")).toBe(2023);
184
+ expect(result.start.get("month")).toBe(12);
185
+ expect(result.end.get("year")).toBe(2024);
186
+ expect(result.end.get("month")).toBe(5);
187
+ });
188
+ });
189
+
120
190
  test("Test - Month expression in context", function () {
121
191
  testSingleCase(chrono, "The date is Sep 2012 is the date", (result) => {
122
192
  expect(result.index).toBe(12);
@@ -300,6 +300,18 @@ test("Test - Before with reference", () => {
300
300
  });
301
301
  });
302
302
 
303
+ test("Test - Strict mode", function () {
304
+ testSingleCase(chrono.strict, "5 minutes ago", new Date(2012, 7, 10, 12, 14), (result, text) => {
305
+ expect(result.start.get("hour")).toBe(12);
306
+ expect(result.start.get("minute")).toBe(9);
307
+ expect(result.start).toBeDate(new Date(2012, 7, 10, 12, 9));
308
+ });
309
+
310
+ testUnexpectedResult(chrono.strict, "5m ago", new Date(2012, 7, 10, 12, 14));
311
+ testUnexpectedResult(chrono.strict, "5hr before", new Date(2012, 7, 10, 12, 14));
312
+ testUnexpectedResult(chrono.strict, "5 h ago", new Date(2012, 7, 10, 12, 14));
313
+ });
314
+
303
315
  test("Test - Negative cases", function () {
304
316
  testUnexpectedResult(chrono, "15 hours 29 min");
305
317
  testUnexpectedResult(chrono, "a few hour");