UNPKG

12.7 kBMarkdownView Raw
1# Chrono (v2)
2
3A 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
8It 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
20With npm:
21```bash
22$ npm install --save chrono-node
23```
24
25```javascript
26import * as chrono from 'chrono-node';
27
28chrono.parseDate('An appointment on Sep 12-13');
29```
30For Node.js:
31```javascript
32const chrono = require('chrono-node');
33
34// or `import chrono from 'chrono-node'` for ECMAScript
35```
36
37### What's changed in the v2
38For 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
42For 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
51Simply pass a `string` to functions `chrono.parseDate` or `chrono.parse`.
52
53```javascript
54import * as chrono from 'chrono-node';
55
56chrono.parseDate('An appointment on Sep 12-13');
57// Fri Sep 12 2014 12:00:00 GMT-0500 (CDT)
58
59chrono.parse('An appointment on Sep 12-13');
60/* [{
61 index: 18,
62 text: 'Sep 12-13',
63 start: ...
64}] */
65```
66
67For more advanced usage, here is the typescript definition of the `parse` function:
68```typescript
69parse(text: string, ref?: ParsingReference, option?: ParsingOption): ParsedResult[] {...}
70```
71
72#### Parsing Reference (Date / Timezone)
73
74Today's "Friday" is different from last month's "Friday".
75The meaning of the referenced dates depends on when and where they are mentioned.
76Chrono lets you define the reference as `Date` or `ParsingReference` object:
77
78```javascript
79// (Note: the examples run on JST timezone)
80
81chrono.parseDate('Friday', new Date(2012, 8 - 1, 23));
82// Fri Aug 24 2012 12:00:00 GMT+0900 (JST)
83
84chrono.parseDate('Friday', new Date(2012, 8 - 1, 1));
85// Fri Aug 03 2012 12:00:00 GMT+0900 (JST)
86
87chrono.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
107const referenceDate = new Date(2012, 7, 25);
108// Sat Aug 25 2012 00:00:00 GMT+0900 -- The reference date was Saturday
109
110chrono.parseDate('Friday', referenceDate);
111// Fri Aug 24 2012 12:00:00 GMT+0900 (JST) -- The day before was Friday
112
113chrono.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
121chrono.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)
125chrono.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
129import { getLastDayOfMonthTransition } from "timezone";
130import { Weekday, Month } from "parsing";
131
132const 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
139chrono.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
143chrono.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
162For example:
163```js
164const results = chrono.parse('I have an appointment tomorrow from 10 to 11 AM');
165
166results[0].index; // 22
167results[0].text; // 'tomorrow from 10 to 11 AM'
168results[0].refDate; // Sat Dec 13 2014 21:50:14 GMT-0600 (CST)
169
170// `start` is Sat Dec 14 2014 10:00:00
171results[0].start.get('day'); // 14 (the 14th, the day after refDate)
172results[0].start.get('month'); // 12 (or December)
173results[0].start.get('hour'); // 10
174results[0].start.date(); // Sun Dec 14 2014 10:00:00 GMT-0600 (CST)
175
176...
177results[0].end.date(); // Sun Dec 14 2014 11:00:00 GMT-0600 (CST)
178```
179
180### Strict vs Casual configuration
181
182Chrono comes with `strict` mode that parse only formal date patterns.
183
184```js
185// 'strict' mode
186chrono.strict.parseDate('Today'); // null
187chrono.strict.parseDate('Friday'); // null
188chrono.strict.parseDate('2016-07-01'); // Fri Jul 01 2016 12:00:00 ...
189chrono.strict.parseDate('Jul 01 2016'); // Fri Jul 01 2016 12:00:00 ...
190
191// 'casual' mode (default)
192chrono.parseDate('Today'); // Thu Jun 30 2016 12:00:00 ...
193chrono.casual.parseDate('Friday'); // Fri Jul 01 2016 12:00:00 ...
194chrono.casual.parseDate('2016-07-01'); // Fri Jul 01 2016 12:00:00 ...
195chrono.casual.parseDate('Jul 01 2016'); // Fri Jul 01 2016 12:00:00 ...
196```
197
198### Locales
199
200By default, Chrono is configured to handle **only international English**.
201This differs from the previous version of Chrono that would try all locales by default.
202
203There are several locales supported contributed by multiple developers under `./locales` directory.
204
205```js
206// default English (US)
207chrono.parseDate('6/10/2018');
208
209chrono.en.parseDate('6/10/2018'); // June 10th, 2018
210chrono.en.GB.parseDate('6/10/2018'); // October 6th, 2018
211
212chrono.ja.parseDate('昭和64年1月7日');
213```
214
215Current 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
219Chrono 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```
221Invalid regular expression: /* omitted */: Invalid property name in character class
222```
223
224This is because the Intl module is required to handle special characters, such as Cyrillic (`ru`).
225
226To avoid this, you can specify only the locale(s) you want to import:
227```typescript
228// CommonJS (Node.js)
229const chrono = require('chrono-node/en')
230
231// ECMAScript
232import chrono from 'chrono-node/en'
233
234// TypeScript
235// Warning: `moduleResolution` must be set to `node16` or `nodeNext` in tsconfig.json`
236import * as chrono from 'chrono-node/en'
237```
238
239## Customize Chrono
240
241Chrono’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
249interface Parser {
250 pattern: (context: ParsingContext) => RegExp,
251 extract: (context: ParsingContext, match: RegExpMatchArray) =>
252 (ParsingComponents | ParsingResult | {[c in Component]?: number} | null)
253}
254```
255
256Parser is a module for low-level pattern-based parsing.
257Ideally, each parser should be designed to handle a single specific date format.
258
259User 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
263const custom = chrono.casual.clone();
264custom.parsers.push({
265 pattern: () => { return /\bChristmas\b/i },
266 extract: (context, match) => {
267 return {
268 day: 25, month: 12
269 }
270 }
271});
272
273custom.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
281interface Refiner {
282 refine: (context: ParsingContext, results: ParsingResult[]) => ParsingResult[]
283}
284```
285
286Refiner 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
289const custom = chrono.casual.clone();
290custom.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)
308custom.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)
312custom.parseDate("This is at 2.30 AM");
313```
314
315In the example, the custom refiner assigns PM to parsing results with ambiguous [meridiem](http://en.wikipedia.org/wiki/12-hour_clock).
316The `refine` method of the refiner class will be called with parsing [results](#parsedresult) (from [parsers](#parser) or other previous refiners).
317The method must return an array of the new results (which, in this case, we modified those results in place).
318
319### More documentation
320
321Checkout the Typescript Documentation in the project's [Github page](http://wanasit.github.io/chrono/).
322
323## Development Guides
324
325This 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
335Parsing date from text is complicated. A small change can have effects on unexpected places.
336So, Chrono is a heavily tested library.
337Commits that break a test shouldn't be allowed in any condition.
338
339Chrono'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
349Chrono's source files is in `src` directory.
350The 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