1 | # Chrono (v2)
|
2 |
|
3 | A natural language date parser in Javascript.
|
4 |
|
5 | ![Build Status](https://github.com/github/docs/actions/workflows/test.yml/badge.svg)
|
6 | [![Coverage Status](https://img.shields.io/coverallsCoverage/github/wanasit/chrono.svg)](https://coveralls.io/r/wanasit/chrono?branch=master)
|
7 |
|
8 | It is designed to handle most date/time format and extract information from any given text:
|
9 |
|
10 | * _Today_, _Tomorrow_, _Yesterday_, _Last Friday_, etc
|
11 | * _17 August 2013 - 19 August 2013_
|
12 | * _This Friday from 13:00 - 16.00_
|
13 | * _5 days ago_
|
14 | * _2 weeks from now_
|
15 | * _Sat Aug 17 2013 18:40:39 GMT+0900 (JST)_
|
16 | * _2014-11-30T08:15:30-05:30_
|
17 |
|
18 | ### Installation
|
19 |
|
20 | With npm:
|
21 | ```bash
|
22 | $ npm install --save chrono-node
|
23 | ```
|
24 |
|
25 | ```javascript
|
26 | import * as chrono from 'chrono-node';
|
27 |
|
28 | chrono.parseDate('An appointment on Sep 12-13');
|
29 | ```
|
30 | For Node.js:
|
31 | ```javascript
|
32 | const chrono = require('chrono-node');
|
33 |
|
34 | // or `import chrono from 'chrono-node'` for ECMAScript
|
35 | ```
|
36 |
|
37 | ### What's changed in the v2
|
38 | For Users
|
39 | * Chrono’s default now handles only international English. While in the previous version, it tried to parse with all known languages.
|
40 | * The current fully supported languages are `en`, `ja`, `fr`, `nl`, `ru` and `uk` (`de`, `pt`, and `zh.hant` are partially supported).
|
41 |
|
42 | For contributors and advanced users
|
43 | * The project is rewritten in TypeScript
|
44 | * New [Parser](#parser) and [Refiner](#refiner) interface
|
45 | * New source code structure. All parsers, refiners, and configuration should be under a locale directory (See. `locales/en`)
|
46 |
|
47 | **Note: [v1.x.x](https://github.com/wanasit/chrono/tree/v1.x.x) will still be supported for the time being.**
|
48 |
|
49 | ## Usage
|
50 |
|
51 | Simply pass a `string` to functions `chrono.parseDate` or `chrono.parse`.
|
52 |
|
53 | ```javascript
|
54 | import * as chrono from 'chrono-node';
|
55 |
|
56 | chrono.parseDate('An appointment on Sep 12-13');
|
57 | // Fri Sep 12 2014 12:00:00 GMT-0500 (CDT)
|
58 |
|
59 | chrono.parse('An appointment on Sep 12-13');
|
60 | /* [{
|
61 | index: 18,
|
62 | text: 'Sep 12-13',
|
63 | start: ...
|
64 | }] */
|
65 | ```
|
66 |
|
67 | For more advanced usage, here is the typescript definition of the `parse` function:
|
68 | ```typescript
|
69 | parse(text: string, ref?: ParsingReference, option?: ParsingOption): ParsedResult[] {...}
|
70 | ```
|
71 |
|
72 | #### Parsing Reference (Date / Timezone)
|
73 |
|
74 | Today's "Friday" is different from last month's "Friday".
|
75 | The meaning of the referenced dates depends on when and where they are mentioned.
|
76 | Chrono lets you define the reference as `Date` or `ParsingReference` object:
|
77 |
|
78 | ```javascript
|
79 | // (Note: the examples run on JST timezone)
|
80 |
|
81 | chrono.parseDate('Friday', new Date(2012, 8 - 1, 23));
|
82 | // Fri Aug 24 2012 12:00:00 GMT+0900 (JST)
|
83 |
|
84 | chrono.parseDate('Friday', new Date(2012, 8 - 1, 1));
|
85 | // Fri Aug 03 2012 12:00:00 GMT+0900 (JST)
|
86 |
|
87 | chrono.parseDate("Friday at 4pm", {
|
88 | // Wed Jun 09 2021 21:00:00 GMT+0900 (JST)
|
89 | // = Wed Jun 09 2021 07:00:00 GMT-0500 (CDT)
|
90 | instant: new Date(1623240000000),
|
91 | timezone: "CDT",
|
92 | })
|
93 | // Sat Jun 12 2021 06:00:00 GMT+0900 (JST)
|
94 | // = Fri Jun 11 2021 16:00:00 GMT-0500 (CDT)
|
95 | ```
|
96 |
|
97 | #### ParsingReference
|
98 | * `instant?: Date` The instant when the input is written or mentioned
|
99 | * `timezone?: string | number` The timezone where the input is written or mentioned.
|
100 | Support minute-offset (number) and timezone name (e.g. "GMT", "CDT")
|
101 |
|
102 | ### Parsing Options
|
103 |
|
104 | `forwardDate` (boolean) to assume the results should happen after the reference date (forward into the future)
|
105 |
|
106 | ```javascript
|
107 | const referenceDate = new Date(2012, 7, 25);
|
108 | // Sat Aug 25 2012 00:00:00 GMT+0900 -- The reference date was Saturday
|
109 |
|
110 | chrono.parseDate('Friday', referenceDate);
|
111 | // Fri Aug 24 2012 12:00:00 GMT+0900 (JST) -- The day before was Friday
|
112 |
|
113 | chrono.parseDate('Friday', referenceDate, { forwardDate: true });
|
114 | // Fri Aug 31 2012 12:00:00 GMT+0900 (JST) -- The following Friday
|
115 | ```
|
116 |
|
117 | `timezones` Override or add custom mappings between timezone abbreviations and offsets. Use this when you want Chrono to parse certain text into a given timezone offset. Chrono supports both unambiguous (normal) timezone mappings and ambigous mappings where the offset is different during and outside of daylight savings.
|
118 |
|
119 | ```javascript
|
120 | // Chrono doesn't understand XYZ, so no timezone is parsed
|
121 | chrono.parse('at 10:00 XYZ', new Date(2023, 3, 20))
|
122 | // "knownValues": {"hour": 10, "minute": 0}
|
123 |
|
124 | // Make Chrono parse XYZ as offset GMT-0300 (180 minutes)
|
125 | chrono.parse('at 10:00 XYZ', new Date(2023, 3, 20), { timezones: { XYZ: -180 } })
|
126 | // "knownValues": {"hour": 10, "minute": 0, "timezoneOffset": -180}
|
127 |
|
128 | // Make Chrono parse XYZ as offset GMT-0300 outside of DST, and GMT-0200 during DST. Assume DST is between
|
129 | import { getLastDayOfMonthTransition } from "timezone";
|
130 | import { Weekday, Month } from "parsing";
|
131 |
|
132 | const parseXYZAsAmbiguousTz = {
|
133 | timezoneOffsetDuringDst: -120,
|
134 | timezoneOffsetNonDst: -180,
|
135 | dstStart: (year: number) => getLastWeekdayOfMonth(year, Month.FEBRUARY, Weekday.SUNDAY, 2),
|
136 | dstEnd: (year: number) => getLastWeekdayOfMonth(year, Month.SEPTEMBER, Weekday.SUNDAY, 3)
|
137 | };
|
138 | // Parsing a date which falls within DST
|
139 | chrono.parse('Jan 1st 2023 at 10:00 XYZ', new Date(2023, 3, 20), { timezones: { XYZ: parseXYZAsAmbiguousTz } })
|
140 | // "knownValues": {"month": 1, ..., "timezoneOffset": -180}
|
141 |
|
142 | // Parsing a non-DST date
|
143 | chrono.parse('Jun 1st 2023 at 10:00 XYZ', new Date(2023, 3, 20), { timezones: { XYZ: parseXYZAsAmbiguousTz } })
|
144 | // "knownValues": {"month": 6, ..., "timezoneOffset": -120}
|
145 | ```
|
146 |
|
147 | ### Parsed Results and Components
|
148 |
|
149 | #### ParsedResult
|
150 | * `refDate: Date` The [reference date](#reference-date) of this result
|
151 | * `index: number` The location within the input text of this result
|
152 | * `text: string` The text this result that appears in the input
|
153 | * `start: ParsedComponents` The parsed date components as a [ParsedComponents](#parsedcomponents) object
|
154 | * `end?: ParsedComponents` Similar to `start`
|
155 | * `date: () => Date` Create a javascript Date
|
156 |
|
157 | #### ParsedComponents
|
158 | * `get: (c: Component) => number | null` Get known or implied value for the component
|
159 | * `isCertain: (c: Component) => boolean` Check if the component has a known value
|
160 | * `date: () => Date` Create a javascript Date
|
161 |
|
162 | For example:
|
163 | ```js
|
164 | const results = chrono.parse('I have an appointment tomorrow from 10 to 11 AM');
|
165 |
|
166 | results[0].index; // 22
|
167 | results[0].text; // 'tomorrow from 10 to 11 AM'
|
168 | results[0].refDate; // Sat Dec 13 2014 21:50:14 GMT-0600 (CST)
|
169 |
|
170 | // `start` is Sat Dec 14 2014 10:00:00
|
171 | results[0].start.get('day'); // 14 (the 14th, the day after refDate)
|
172 | results[0].start.get('month'); // 12 (or December)
|
173 | results[0].start.get('hour'); // 10
|
174 | results[0].start.date(); // Sun Dec 14 2014 10:00:00 GMT-0600 (CST)
|
175 |
|
176 | ...
|
177 | results[0].end.date(); // Sun Dec 14 2014 11:00:00 GMT-0600 (CST)
|
178 | ```
|
179 |
|
180 | ### Strict vs Casual configuration
|
181 |
|
182 | Chrono comes with `strict` mode that parse only formal date patterns.
|
183 |
|
184 | ```js
|
185 | // 'strict' mode
|
186 | chrono.strict.parseDate('Today'); // null
|
187 | chrono.strict.parseDate('Friday'); // null
|
188 | chrono.strict.parseDate('2016-07-01'); // Fri Jul 01 2016 12:00:00 ...
|
189 | chrono.strict.parseDate('Jul 01 2016'); // Fri Jul 01 2016 12:00:00 ...
|
190 |
|
191 | // 'casual' mode (default)
|
192 | chrono.parseDate('Today'); // Thu Jun 30 2016 12:00:00 ...
|
193 | chrono.casual.parseDate('Friday'); // Fri Jul 01 2016 12:00:00 ...
|
194 | chrono.casual.parseDate('2016-07-01'); // Fri Jul 01 2016 12:00:00 ...
|
195 | chrono.casual.parseDate('Jul 01 2016'); // Fri Jul 01 2016 12:00:00 ...
|
196 | ```
|
197 |
|
198 | ### Locales
|
199 |
|
200 | By default, Chrono is configured to handle **only international English**.
|
201 | This differs from the previous version of Chrono that would try all locales by default.
|
202 |
|
203 | There are several locales supported contributed by multiple developers under `./locales` directory.
|
204 |
|
205 | ```js
|
206 | // default English (US)
|
207 | chrono.parseDate('6/10/2018');
|
208 |
|
209 | chrono.en.parseDate('6/10/2018'); // June 10th, 2018
|
210 | chrono.en.GB.parseDate('6/10/2018'); // October 6th, 2018
|
211 |
|
212 | chrono.ja.parseDate('昭和64年1月7日');
|
213 | ```
|
214 |
|
215 | Current supported locale options are: `en`, `ja`, `fr`, `nl`, `ru` and `uk` (`de`, `pt`, and `zh.hant` are partially supported).
|
216 |
|
217 | #### Importing specific locales
|
218 |
|
219 | Chrono exports all locale options by default for simplicity. However, this can cause issues when using Chrono if you're using a Node.js runtime that was built with the Intl module disabled (with the `--without-intl` flag), such as:
|
220 | ```
|
221 | Invalid regular expression: /* omitted */: Invalid property name in character class
|
222 | ```
|
223 |
|
224 | This is because the Intl module is required to handle special characters, such as Cyrillic (`ru`).
|
225 |
|
226 | To avoid this, you can specify only the locale(s) you want to import:
|
227 | ```typescript
|
228 | // CommonJS (Node.js)
|
229 | const chrono = require('chrono-node/en')
|
230 |
|
231 | // ECMAScript
|
232 | import chrono from 'chrono-node/en'
|
233 |
|
234 | // TypeScript
|
235 | // Warning: `moduleResolution` must be set to `node16` or `nodeNext` in tsconfig.json`
|
236 | import * as chrono from 'chrono-node/en'
|
237 | ```
|
238 |
|
239 | ## Customize Chrono
|
240 |
|
241 | Chrono’s extraction pipeline configuration consists of `parsers: Parser[]` and `refiners: Refiner[]`.
|
242 |
|
243 | * First, each parser independently extracts patterns from input text input and create parsing results ([ParsingResult](#parsedresult)).
|
244 | * Then, the parsing results are combined, sorted, and refined with the refiners. In the refining phase, the results can be filtered-out, merged, or attached with additional information.
|
245 |
|
246 | ### Parser
|
247 |
|
248 | ```typescript
|
249 | interface Parser {
|
250 | pattern: (context: ParsingContext) => RegExp,
|
251 | extract: (context: ParsingContext, match: RegExpMatchArray) =>
|
252 | (ParsingComponents | ParsingResult | {[c in Component]?: number} | null)
|
253 | }
|
254 | ```
|
255 |
|
256 | Parser is a module for low-level pattern-based parsing.
|
257 | Ideally, each parser should be designed to handle a single specific date format.
|
258 |
|
259 | User can create a new parser for supporting new date formats or languages
|
260 | by providing RegExp pattern `pattern()` and extracting result or components from the RegExp match `extract()`.
|
261 |
|
262 | ```javascript
|
263 | const custom = chrono.casual.clone();
|
264 | custom.parsers.push({
|
265 | pattern: () => { return /\bChristmas\b/i },
|
266 | extract: (context, match) => {
|
267 | return {
|
268 | day: 25, month: 12
|
269 | }
|
270 | }
|
271 | });
|
272 |
|
273 | custom.parseDate("I'll arrive at 2.30AM on Christmas night");
|
274 | // Wed Dec 25 2013 02:30:00 GMT+0900 (JST)
|
275 | // 'at 2.30AM on Christmas'
|
276 | ```
|
277 |
|
278 | ### Refiner
|
279 |
|
280 | ```typescript
|
281 | interface Refiner {
|
282 | refine: (context: ParsingContext, results: ParsingResult[]) => ParsingResult[]
|
283 | }
|
284 | ```
|
285 |
|
286 | Refiner is a higher level module for improving or manipulating the results. User can add a new type of refiner to customize Chrono's results or to add some custom logic to Chrono.
|
287 |
|
288 | ```javascript
|
289 | const custom = chrono.casual.clone();
|
290 | custom.refiners.push({
|
291 | refine: (context, results) => {
|
292 | // If there is no AM/PM (meridiem) specified,
|
293 | // let all time between 1:00 - 4:00 be PM (13.00 - 16.00)
|
294 | results.forEach((result) => {
|
295 | if (!result.start.isCertain('meridiem') &&
|
296 | result.start.get('hour') >= 1 && result.start.get('hour') < 4) {
|
297 |
|
298 | result.start.assign('meridiem', 1);
|
299 | result.start.assign('hour', result.start.get('hour') + 12);
|
300 | }
|
301 | });
|
302 | return results;
|
303 | }
|
304 | });
|
305 |
|
306 | // This will be parsed as PM.
|
307 | // > Tue Dec 16 2014 14:30:00 GMT-0600 (CST)
|
308 | custom.parseDate("This is at 2.30");
|
309 |
|
310 | // Unless the 'AM' part is specified
|
311 | // > Tue Dec 16 2014 02:30:00 GMT-0600 (CST)
|
312 | custom.parseDate("This is at 2.30 AM");
|
313 | ```
|
314 |
|
315 | In the example, the custom refiner assigns PM to parsing results with ambiguous [meridiem](http://en.wikipedia.org/wiki/12-hour_clock).
|
316 | The `refine` method of the refiner class will be called with parsing [results](#parsedresult) (from [parsers](#parser) or other previous refiners).
|
317 | The method must return an array of the new results (which, in this case, we modified those results in place).
|
318 |
|
319 | ### More documentation
|
320 |
|
321 | Checkout the Typescript Documentation in the project's [Github page](http://wanasit.github.io/chrono/).
|
322 |
|
323 | ## Development Guides
|
324 |
|
325 | This guide explains how to set up chrono project for prospective contributors.
|
326 |
|
327 | ```bash
|
328 | # Clone and install library
|
329 | $ git clone https://github.com/wanasit/chrono.git chrono
|
330 | $ cd chrono
|
331 | $ npm install
|
332 |
|
333 | ```
|
334 |
|
335 | Parsing date from text is complicated. A small change can have effects on unexpected places.
|
336 | So, Chrono is a heavily tested library.
|
337 | Commits that break a test shouldn't be allowed in any condition.
|
338 |
|
339 | Chrono's unit testing is based-on [Jest](https://facebook.github.io/jest/).
|
340 |
|
341 | ```bash
|
342 | # Run the test
|
343 | $ npm run test
|
344 |
|
345 | # Run the test in watch mode
|
346 | $ npm run watch
|
347 | ```
|
348 |
|
349 | Chrono's source files is in `src` directory.
|
350 | The built bundle (`dist/*`) is created by running [Webpack](https://webpack.js.org/) via the following command
|
351 |
|
352 | ```bash
|
353 | $ npm run build
|
354 | ```
|
355 |
|