UNPKG

6.8 kBPlain TextView Raw
1import { ReferenceWithTimezone, ParsingComponents, ParsingResult } from "./results";
2import { Component, ParsedResult, ParsingOption, ParsingReference } from "./types";
3import { AsyncDebugBlock, DebugHandler } from "./debugging";
4import ENDefaultConfiguration from "./locales/en/configuration";
5
6/**
7 * Chrono configuration.
8 * It is simply an ordered list of parsers and refiners
9 */
10export interface Configuration {
11 parsers: Parser[];
12 refiners: Refiner[];
13}
14
15/**
16 * An abstraction for Chrono *Parser*.
17 *
18 * Each parser should recognize and handle a certain date format.
19 * Chrono uses multiple parses (and refiners) together for parsing the input.
20 *
21 * The parser implementation must provide {@Link pattern | pattern()} for the date format.
22 *
23 * The {@Link extract | extract()} method is called with the pattern's *match*.
24 * The matching and extracting is controlled and adjusted to avoid for overlapping results.
25 */
26export interface Parser {
27 pattern(context: ParsingContext): RegExp;
28 extract(
29 context: ParsingContext,
30 match: RegExpMatchArray
31 ): ParsingComponents | ParsingResult | { [c in Component]?: number } | null;
32}
33
34/**
35 * A abstraction for Chrono *Refiner*.
36 *
37 * Each refiner takes the list of results (from parsers or other refiners) and returns another list of results.
38 * Chrono applies each refiner in order and return the output from the last refiner.
39 */
40export interface Refiner {
41 refine: (context: ParsingContext, results: ParsingResult[]) => ParsingResult[];
42}
43
44/**
45 * The Chrono object.
46 */
47export class Chrono {
48 parsers: Array<Parser>;
49 refiners: Array<Refiner>;
50
51 defaultConfig = new ENDefaultConfiguration();
52
53 constructor(configuration?: Configuration) {
54 configuration = configuration || this.defaultConfig.createCasualConfiguration();
55 this.parsers = [...configuration.parsers];
56 this.refiners = [...configuration.refiners];
57 }
58
59 /**
60 * Create a shallow copy of the Chrono object with the same configuration (`parsers` and `refiners`)
61 */
62 clone(): Chrono {
63 return new Chrono({
64 parsers: [...this.parsers],
65 refiners: [...this.refiners],
66 });
67 }
68
69 /**
70 * A shortcut for calling {@Link parse | parse() } then transform the result into Javascript's Date object
71 * @return Date object created from the first parse result
72 */
73 parseDate(text: string, referenceDate?: ParsingReference | Date, option?: ParsingOption): Date | null {
74 const results = this.parse(text, referenceDate, option);
75 return results.length > 0 ? results[0].start.date() : null;
76 }
77
78 parse(text: string, referenceDate?: ParsingReference | Date, option?: ParsingOption): ParsedResult[] {
79 const context = new ParsingContext(text, referenceDate, option);
80
81 let results = [];
82 this.parsers.forEach((parser) => {
83 const parsedResults = Chrono.executeParser(context, parser);
84 results = results.concat(parsedResults);
85 });
86
87 results.sort((a, b) => {
88 return a.index - b.index;
89 });
90
91 this.refiners.forEach(function (refiner) {
92 results = refiner.refine(context, results);
93 });
94
95 return results;
96 }
97
98 private static executeParser(context: ParsingContext, parser: Parser) {
99 const results = [];
100 const pattern = parser.pattern(context);
101
102 const originalText = context.text;
103 let remainingText = context.text;
104 let match = pattern.exec(remainingText);
105
106 while (match) {
107 // Calculate match index on the full text;
108 const index = match.index + originalText.length - remainingText.length;
109 match.index = index;
110
111 const result = parser.extract(context, match);
112 if (!result) {
113 // If fails, move on by 1
114 remainingText = originalText.substring(match.index + 1);
115 match = pattern.exec(remainingText);
116 continue;
117 }
118
119 let parsedResult: ParsingResult = null;
120 if (result instanceof ParsingResult) {
121 parsedResult = result;
122 } else if (result instanceof ParsingComponents) {
123 parsedResult = context.createParsingResult(match.index, match[0]);
124 parsedResult.start = result;
125 } else {
126 parsedResult = context.createParsingResult(match.index, match[0], result);
127 }
128
129 const parsedIndex = parsedResult.index;
130 const parsedText = parsedResult.text;
131 context.debug(() =>
132 console.log(`${parser.constructor.name} extracted (at index=${parsedIndex}) '${parsedText}'`)
133 );
134
135 results.push(parsedResult);
136 remainingText = originalText.substring(parsedIndex + parsedText.length);
137 match = pattern.exec(remainingText);
138 }
139
140 return results;
141 }
142}
143
144export class ParsingContext implements DebugHandler {
145 readonly text: string;
146 readonly option: ParsingOption;
147 readonly reference: ReferenceWithTimezone;
148
149 /**
150 * @deprecated. Use `reference.instant` instead.
151 */
152 readonly refDate: Date;
153
154 constructor(text: string, refDate?: ParsingReference | Date, option?: ParsingOption) {
155 this.text = text;
156 this.reference = new ReferenceWithTimezone(refDate);
157 this.option = option ?? {};
158
159 this.refDate = this.reference.instant;
160 }
161
162 createParsingComponents(components?: { [c in Component]?: number } | ParsingComponents): ParsingComponents {
163 if (components instanceof ParsingComponents) {
164 return components;
165 }
166
167 return new ParsingComponents(this.reference, components);
168 }
169
170 createParsingResult(
171 index: number,
172 textOrEndIndex: number | string,
173 startComponents?: { [c in Component]?: number } | ParsingComponents,
174 endComponents?: { [c in Component]?: number } | ParsingComponents
175 ): ParsingResult {
176 const text = typeof textOrEndIndex === "string" ? textOrEndIndex : this.text.substring(index, textOrEndIndex);
177
178 const start = startComponents ? this.createParsingComponents(startComponents) : null;
179 const end = endComponents ? this.createParsingComponents(endComponents) : null;
180
181 return new ParsingResult(this.reference, index, text, start, end);
182 }
183
184 debug(block: AsyncDebugBlock): void {
185 if (this.option.debug) {
186 if (this.option.debug instanceof Function) {
187 this.option.debug(block);
188 } else {
189 const handler: DebugHandler = <DebugHandler>this.option.debug;
190 handler.debug(block);
191 }
192 }
193 }
194}