Skip to main content

chrono_node/core/
chrono.rs

1//! upstream `chrono.ts`: the `Parser` and `Refiner` abstractions and the parse loop.
2
3use std::collections::BTreeSet;
4use std::sync::Arc;
5
6use super::components::{Component, ParsingComponents};
7use super::configuration::Flags;
8use super::context::{ParsingContext, ParsingOption};
9use super::js_date::JsDate;
10use super::reference::ReferenceInput;
11use super::regex::{JsRegex, RegexMatch};
12use super::result::ParsingResult;
13
14/// What `Parser::extract` returns (upstream returns one of these types, or `null`).
15#[derive(Clone, Debug)]
16pub enum Extracted {
17    /// `null`: no result here; chrono retries one character after `m.index`.
18    None,
19    Components(ParsingComponents),
20    Result(ParsingResult),
21    /// A plain `{ [component]: number }` object of known values.
22    Known(Vec<(Component, i64)>),
23}
24
25/// upstream `Parser`.
26pub trait Parser: Send + Sync {
27    /// `pattern(context)`. Return a clone of a cached regex; cloning is cheap.
28    fn pattern(&self, context: &ParsingContext) -> JsRegex;
29
30    /// `extract(context, match)`. `m.index` is the byte offset of the match in `context.text`.
31    /// Like upstream, an extractor may move `m.index` before returning `Extracted::None` to
32    /// control where the search resumes.
33    fn extract(&self, context: &ParsingContext, m: &mut RegexMatch) -> Extracted;
34}
35
36/// upstream `Refiner`.
37pub trait Refiner: Send + Sync {
38    fn refine(&self, context: &ParsingContext, results: Vec<ParsingResult>) -> Vec<ParsingResult>;
39}
40
41/// A parser in a configuration slot, with the upstream class name and constructor flags.
42#[derive(Clone)]
43pub struct NamedParser {
44    pub name: &'static str,
45    pub flags: Flags,
46    pub parser: Arc<dyn Parser>,
47}
48
49#[derive(Clone)]
50pub struct NamedRefiner {
51    pub name: &'static str,
52    pub flags: Flags,
53    pub refiner: Arc<dyn Refiner>,
54}
55
56/// upstream `Chrono`: an ordered list of parsers and refiners.
57#[derive(Clone, Default)]
58pub struct Chrono {
59    pub parsers: Vec<NamedParser>,
60    pub refiners: Vec<NamedRefiner>,
61    /// Slots in the configuration whose step isn't ported yet. They are skipped.
62    pub missing: Vec<&'static str>,
63}
64
65/// One step's name and its results (a parser's raw output, or the results after a refiner).
66pub type StepTrace = (&'static str, Vec<ParsingResult>);
67impl Chrono {
68    /// `parse(text, ref, option)`, with "now" taken from the system clock.
69    pub fn parse(&self, text: &str, reference: &ReferenceInput, option: &ParsingOption) -> Vec<ParsingResult> {
70        self.parse_with_now(text, reference, option, JsDate::now())
71    }
72
73    /// `parse` with an explicit "now" (used when the reference is `Now`), for reproducible runs.
74    pub fn parse_with_now(
75        &self,
76        text: &str,
77        reference: &ReferenceInput,
78        option: &ParsingOption,
79        now: JsDate,
80    ) -> Vec<ParsingResult> {
81        let context = ParsingContext::new(text, reference, option.clone(), now);
82        let mut results = Vec::new();
83        for p in &self.parsers {
84            results.extend(execute_parser(&context, p.parser.as_ref()));
85        }
86        // Stable sort, like JS Array.prototype.sort.
87        results.sort_by_key(|r| r.index);
88        for r in &self.refiners {
89            results = r.refiner.refine(&context, results);
90        }
91        results
92    }
93
94    /// `parse_with_now`, also returning each step's output (the harness's per-step comparison with
95    /// chrono-node, `harness/golden/frontier.js`): every parser's own results, then the results
96    /// after each refiner. Same order and semantics as `parse_with_now`.
97    pub fn trace_with_now(
98        &self,
99        text: &str,
100        reference: &ReferenceInput,
101        option: &ParsingOption,
102        now: JsDate,
103    ) -> (Vec<StepTrace>, Vec<StepTrace>) {
104        let context = ParsingContext::new(text, reference, option.clone(), now);
105        let mut parsers = Vec::new();
106        let mut results = Vec::new();
107        for p in &self.parsers {
108            let found = execute_parser(&context, p.parser.as_ref());
109            results.extend(found.iter().cloned());
110            parsers.push((p.name, found));
111        }
112        results.sort_by_key(|r| r.index);
113        let mut refiners = Vec::new();
114        for r in &self.refiners {
115            results = r.refiner.refine(&context, results);
116            refiners.push((r.name, results.clone()));
117        }
118        (parsers, refiners)
119    }
120
121    /// The `occurrence`-th refiner named `step` (0-based) run alone on `input`, or `None` if it isn't
122    /// ported. The harness feeds it chrono-node's input for that slot, so each refiner is checked on
123    /// every case even while an earlier step is unported (`harness/golden/frontier.js`).
124    pub fn refine_isolated(
125        &self,
126        context: &ParsingContext,
127        step: &str,
128        occurrence: usize,
129        input: Vec<ParsingResult>,
130    ) -> Option<Vec<ParsingResult>> {
131        let r = self.refiners.iter().filter(|r| r.name == step).nth(occurrence)?;
132        Some(r.refiner.refine(context, input))
133    }
134
135    /// `parseDate(text, ref, option)`.
136    pub fn parse_date(&self, text: &str, reference: &ReferenceInput, option: &ParsingOption) -> Option<JsDate> {
137        self.parse(text, reference, option).first().map(|r| r.start.date())
138    }
139
140    /// Upstream class names of the parsers and refiners actually present.
141    pub fn registered(&self) -> BTreeSet<&'static str> {
142        self.parsers
143            .iter()
144            .map(|p| p.name)
145            .chain(self.refiners.iter().map(|r| r.name))
146            .collect()
147    }
148}
149
150fn char_boundary_at_or_after(text: &str, i: usize) -> usize {
151    let mut i = i.min(text.len());
152    while !text.is_char_boundary(i) {
153        i += 1;
154    }
155    i
156}
157
158/// upstream `Chrono.executeParser`. Like upstream, each search runs on the **remaining
159/// substring**, so `^` and lookbehinds see the slice start, not the full text.
160pub fn execute_parser(context: &ParsingContext, parser: &dyn Parser) -> Vec<ParsingResult> {
161    let mut results = Vec::new();
162    let pattern = parser.pattern(context);
163    let text = context.text.as_str();
164    let mut offset = 0;
165    let mut found = pattern.exec(text);
166
167    while let Some(mut m) = found {
168        m.index += offset;
169        let parsed = match parser.extract(context, &mut m) {
170            Extracted::None => {
171                // `originalText.substring(match.index + 1)`
172                let next = m.index + 1;
173                if next > text.len() {
174                    break;
175                }
176                offset = char_boundary_at_or_after(text, next);
177                found = pattern.exec(&text[offset..]);
178                continue;
179            }
180            Extracted::Result(r) => r,
181            Extracted::Components(c) => {
182                let mut r = context.create_parsing_result(m.index, m.text(), None, None);
183                r.start = c;
184                r
185            }
186            Extracted::Known(known) => {
187                let start = context.create_parsing_components(&known);
188                context.create_parsing_result(m.index, m.text(), Some(start), None)
189            }
190        };
191        offset = char_boundary_at_or_after(text, parsed.index + parsed.text.len());
192        results.push(parsed);
193        found = pattern.exec(&text[offset..]);
194    }
195    results
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use crate::core::components::Component;
202
203    /// Matches `^?(\d+)`; accepts even numbers; odd numbers jump past themselves.
204    struct EvenNumbers {
205        anchored: bool,
206        skip_whole_match: bool,
207    }
208
209    impl Parser for EvenNumbers {
210        fn pattern(&self, _: &ParsingContext) -> JsRegex {
211            JsRegex::new(if self.anchored { r"^\s*(\d+)" } else { r"(\d+)" }, "")
212        }
213        fn extract(&self, _: &ParsingContext, m: &mut RegexMatch) -> Extracted {
214            let n: i64 = m.get(1).unwrap().parse().unwrap();
215            if n % 2 == 1 {
216                if self.skip_whole_match {
217                    m.index += m.text().len();
218                }
219                return Extracted::None;
220            }
221            Extracted::Known(vec![(Component::Day, n)])
222        }
223    }
224
225    fn run(p: EvenNumbers, text: &str) -> Vec<(usize, String)> {
226        let context = ParsingContext::new(
227            text,
228            &ReferenceInput::Date(JsDate::from_millis(0)),
229            ParsingOption::default(),
230            JsDate::INVALID,
231        );
232        execute_parser(&context, &p)
233            .into_iter()
234            .map(|r| (r.index, r.text))
235            .collect()
236    }
237
238    // Expected outputs below were computed by running the same toy parser through upstream's
239    // `executeParser` loop in Node.
240
241    #[test]
242    fn retries_one_char_after_a_rejected_match() {
243        let out = run(
244            EvenNumbers {
245                anchored: false,
246                skip_whole_match: false,
247            },
248            "13 24 6",
249        );
250        assert_eq!(out, vec![(3, "24".into()), (6, "6".into())]);
251        let out = run(
252            EvenNumbers {
253                anchored: false,
254                skip_whole_match: false,
255            },
256            "14x",
257        );
258        assert_eq!(out, vec![(0, "14".into())]);
259        let out = run(
260            EvenNumbers {
261                anchored: false,
262                skip_whole_match: false,
263            },
264            "35x",
265        );
266        assert_eq!(out, vec![]);
267    }
268
269    #[test]
270    fn extractor_can_move_the_retry_point() {
271        let out = run(
272            EvenNumbers {
273                anchored: false,
274                skip_whole_match: true,
275            },
276            "35x",
277        );
278        assert_eq!(out, vec![]);
279        let out = run(
280            EvenNumbers {
281                anchored: false,
282                skip_whole_match: true,
283            },
284            "3 4x",
285        );
286        assert_eq!(out, vec![(2, "4".into())]);
287    }
288
289    #[test]
290    fn caret_anchors_at_the_remaining_slice() {
291        // After "2" is consumed, the search restarts on " 4 x 6", so ^ matches again there.
292        let out = run(
293            EvenNumbers {
294                anchored: true,
295                skip_whole_match: false,
296            },
297            "2 4 x 6",
298        );
299        assert_eq!(out, vec![(0, "2".into()), (1, " 4".into())]);
300    }
301
302    #[test]
303    fn multibyte_text_keeps_byte_indexes_on_char_boundaries() {
304        let out = run(
305            EvenNumbers {
306                anchored: false,
307                skip_whole_match: false,
308            },
309            "é1é2",
310        );
311        assert_eq!(out, vec![(5, "2".into())]);
312    }
313
314    #[test]
315    fn empty_configuration_finds_nothing() {
316        let c = Chrono::default();
317        assert!(c
318            .parse("tomorrow", &ReferenceInput::Now, &ParsingOption::default())
319            .is_empty());
320    }
321}