Skip to main content

chrono_node/core/
components.rs

1//! `ParsingComponents` (upstream `results.ts`): known and implied date/time components.
2
3use super::js_date::JsDate;
4use super::reference::ReferenceWithTimezone;
5use crate::calculation::duration::{add_duration, Duration, DurationUnit};
6
7/// upstream `Component`.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
9pub enum Component {
10    Year,
11    Month,
12    Day,
13    Weekday,
14    Hour,
15    Minute,
16    Second,
17    Millisecond,
18    Meridiem,
19    TimezoneOffset,
20}
21
22impl Component {
23    pub const ALL: [Component; 10] = [
24        Component::Year,
25        Component::Month,
26        Component::Day,
27        Component::Weekday,
28        Component::Hour,
29        Component::Minute,
30        Component::Second,
31        Component::Millisecond,
32        Component::Meridiem,
33        Component::TimezoneOffset,
34    ];
35
36    /// The upstream name (`"timezoneOffset"`), as used in golden data.
37    pub fn name(self) -> &'static str {
38        match self {
39            Component::Year => "year",
40            Component::Month => "month",
41            Component::Day => "day",
42            Component::Weekday => "weekday",
43            Component::Hour => "hour",
44            Component::Minute => "minute",
45            Component::Second => "second",
46            Component::Millisecond => "millisecond",
47            Component::Meridiem => "meridiem",
48            Component::TimezoneOffset => "timezoneOffset",
49        }
50    }
51
52    pub fn from_name(name: &str) -> Option<Component> {
53        Component::ALL.into_iter().find(|c| c.name() == name)
54    }
55}
56
57/// upstream `Meridiem`.
58pub mod meridiem {
59    pub const AM: i64 = 0;
60    pub const PM: i64 = 1;
61}
62
63/// upstream `Weekday` (0 = Sunday).
64pub mod weekday {
65    pub const SUNDAY: i64 = 0;
66    pub const MONDAY: i64 = 1;
67    pub const TUESDAY: i64 = 2;
68    pub const WEDNESDAY: i64 = 3;
69    pub const THURSDAY: i64 = 4;
70    pub const FRIDAY: i64 = 5;
71    pub const SATURDAY: i64 = 6;
72}
73
74/// A JS object `{ [c in Component]?: number }`, keeping insertion order like JS does.
75#[derive(Clone, Debug, Default, PartialEq, Eq)]
76pub struct ComponentMap(Vec<(Component, i64)>);
77
78impl ComponentMap {
79    pub fn new() -> ComponentMap {
80        ComponentMap(Vec::new())
81    }
82
83    pub fn from_pairs(pairs: &[(Component, i64)]) -> ComponentMap {
84        let mut map = ComponentMap::new();
85        for &(c, v) in pairs {
86            map.set(c, v);
87        }
88        map
89    }
90
91    pub fn get(&self, c: Component) -> Option<i64> {
92        self.0.iter().find(|(k, _)| *k == c).map(|(_, v)| *v)
93    }
94
95    pub fn contains(&self, c: Component) -> bool {
96        self.0.iter().any(|(k, _)| *k == c)
97    }
98
99    /// Assignment keeps an existing key's position, like a JS object.
100    pub fn set(&mut self, c: Component, v: i64) {
101        match self.0.iter_mut().find(|(k, _)| *k == c) {
102            Some(slot) => slot.1 = v,
103            None => self.0.push((c, v)),
104        }
105    }
106
107    pub fn remove(&mut self, c: Component) {
108        self.0.retain(|(k, _)| *k != c);
109    }
110
111    pub fn keys(&self) -> impl Iterator<Item = Component> + '_ {
112        self.0.iter().map(|(k, _)| *k)
113    }
114
115    pub fn iter(&self) -> impl Iterator<Item = (Component, i64)> + '_ {
116        self.0.iter().copied()
117    }
118
119    pub fn is_empty(&self) -> bool {
120        self.0.is_empty()
121    }
122}
123
124/// upstream `ParsingComponents`.
125///
126/// Rust's `Clone` copies everything, tags included. Upstream's `clone()` doesn't copy tags: port
127/// an upstream `.clone()` as [`ParsingComponents::js_clone`] (see `PORTING.md`).
128#[derive(Clone, Debug, PartialEq)]
129pub struct ParsingComponents {
130    reference: ReferenceWithTimezone,
131    known: ComponentMap,
132    implied: ComponentMap,
133    tags: Vec<String>,
134}
135
136impl ParsingComponents {
137    /// Components rebuilt from their parts (the harness feeds chrono-node's results to one refiner,
138    /// `chrono-rs-json --trace` with `isolate`).
139    pub fn from_parts(
140        reference: ReferenceWithTimezone,
141        known: ComponentMap,
142        implied: ComponentMap,
143        tags: Vec<String>,
144    ) -> ParsingComponents {
145        ParsingComponents {
146            reference,
147            known,
148            implied,
149            tags,
150        }
151    }
152
153    /// `new ParsingComponents(reference, knownComponents)`.
154    pub fn new(reference: ReferenceWithTimezone, known: &[(Component, i64)]) -> ParsingComponents {
155        let mut c = ParsingComponents {
156            reference,
157            known: ComponentMap::from_pairs(known),
158            implied: ComponentMap::new(),
159            tags: Vec::new(),
160        };
161        let date = reference.get_date_with_adjusted_timezone();
162        c.imply(Component::Day, date.get_date());
163        c.imply(Component::Month, date.get_month() + 1);
164        c.imply(Component::Year, date.get_full_year());
165        c.imply(Component::Hour, 12);
166        c.imply(Component::Minute, 0);
167        c.imply(Component::Second, 0);
168        c.imply(Component::Millisecond, 0);
169        c
170    }
171
172    /// `ParsingComponents.createRelativeFromReference(reference, duration)`.
173    /// Like upstream, `duration` is mutated by `add_duration` (short keys renamed, fractions cascaded).
174    pub fn create_relative_from_reference(
175        reference: ReferenceWithTimezone,
176        duration: &mut Duration,
177    ) -> ParsingComponents {
178        let date = add_duration(reference.get_date_with_adjusted_timezone(), duration);
179
180        let mut c = ParsingComponents::new(reference, &[]);
181        c.add_tag("result/relativeDate");
182        let has = |u| duration.has(u);
183        if has(DurationUnit::Hour)
184            || has(DurationUnit::Minute)
185            || has(DurationUnit::Second)
186            || has(DurationUnit::Millisecond)
187        {
188            c.add_tag("result/relativeDateAndTime");
189            crate::core::utils::dates::assign_similar_time(&mut c, date);
190            crate::core::utils::dates::assign_similar_date(&mut c, date);
191            c.assign(Component::TimezoneOffset, reference.get_timezone_offset());
192        } else {
193            crate::core::utils::dates::imply_similar_time(&mut c, date);
194            c.imply(Component::TimezoneOffset, reference.get_timezone_offset());
195            if has(DurationUnit::Day) {
196                c.assign(Component::Day, date.get_date());
197                c.assign(Component::Month, date.get_month() + 1);
198                c.assign(Component::Year, date.get_full_year());
199                c.assign(Component::Weekday, date.get_day());
200            } else if has(DurationUnit::Week) {
201                c.assign(Component::Day, date.get_date());
202                c.assign(Component::Month, date.get_month() + 1);
203                c.assign(Component::Year, date.get_full_year());
204                c.imply(Component::Weekday, date.get_day());
205            } else {
206                c.imply(Component::Day, date.get_date());
207                if has(DurationUnit::Month) {
208                    c.assign(Component::Month, date.get_month() + 1);
209                    c.assign(Component::Year, date.get_full_year());
210                } else {
211                    c.imply(Component::Month, date.get_month() + 1);
212                    if has(DurationUnit::Year) {
213                        c.assign(Component::Year, date.get_full_year());
214                    } else {
215                        c.imply(Component::Year, date.get_full_year());
216                    }
217                }
218            }
219        }
220        c
221    }
222
223    pub fn reference(&self) -> ReferenceWithTimezone {
224        self.reference
225    }
226
227    /// `get(component)`: known, else implied, else `None` (JS `null`).
228    pub fn get(&self, c: Component) -> Option<i64> {
229        self.known.get(c).or_else(|| self.implied.get(c))
230    }
231
232    /// `get(c)` where upstream relies on JS turning `null` into 0 in arithmetic.
233    pub fn get_or_zero(&self, c: Component) -> i64 {
234        self.get(c).unwrap_or(0)
235    }
236
237    pub fn is_certain(&self, c: Component) -> bool {
238        self.known.contains(c)
239    }
240
241    pub fn get_certain_components(&self) -> Vec<Component> {
242        self.known.keys().collect()
243    }
244
245    pub fn known_values(&self) -> &ComponentMap {
246        &self.known
247    }
248
249    pub fn implied_values(&self) -> &ComponentMap {
250        &self.implied
251    }
252
253    /// `imply(component, value)`: ignored if the component is already known.
254    pub fn imply(&mut self, c: Component, v: i64) -> &mut Self {
255        if !self.known.contains(c) {
256            self.implied.set(c, v);
257        }
258        self
259    }
260
261    /// `assign(component, value)`.
262    pub fn assign(&mut self, c: Component, v: i64) -> &mut Self {
263        self.known.set(c, v);
264        self.implied.remove(c);
265        self
266    }
267
268    /// `addDurationAsImplied(duration)`. Like upstream, `duration` is mutated by `add_duration`.
269    pub fn add_duration_as_implied(&mut self, duration: &mut Duration) -> &mut Self {
270        let current = self.date_without_timezone_adjustment();
271        let date = add_duration(current, duration);
272        let has = |u| duration.has(u);
273        if has(DurationUnit::Day) || has(DurationUnit::Week) || has(DurationUnit::Month) || has(DurationUnit::Year) {
274            self.delete(&[Component::Day, Component::Weekday, Component::Month, Component::Year]);
275            self.imply(Component::Day, date.get_date());
276            self.imply(Component::Weekday, date.get_day());
277            self.imply(Component::Month, date.get_month() + 1);
278            self.imply(Component::Year, date.get_full_year());
279        }
280        if has(DurationUnit::Second) || has(DurationUnit::Minute) || has(DurationUnit::Hour) {
281            self.delete(&[Component::Second, Component::Minute, Component::Hour]);
282            self.imply(Component::Second, date.get_seconds());
283            self.imply(Component::Minute, date.get_minutes());
284            self.imply(Component::Hour, date.get_hours());
285        }
286        self
287    }
288
289    pub fn delete(&mut self, components: &[Component]) {
290        for &c in components {
291            self.known.remove(c);
292            self.implied.remove(c);
293        }
294    }
295
296    pub fn is_only_date(&self) -> bool {
297        !self.is_certain(Component::Hour) && !self.is_certain(Component::Minute) && !self.is_certain(Component::Second)
298    }
299
300    pub fn is_only_time(&self) -> bool {
301        !self.is_certain(Component::Weekday)
302            && !self.is_certain(Component::Day)
303            && !self.is_certain(Component::Month)
304            && !self.is_certain(Component::Year)
305    }
306
307    pub fn is_only_weekday_component(&self) -> bool {
308        self.is_certain(Component::Weekday) && !self.is_certain(Component::Day) && !self.is_certain(Component::Month)
309    }
310
311    pub fn is_date_with_unknown_year(&self) -> bool {
312        self.is_certain(Component::Month) && !self.is_certain(Component::Year)
313    }
314
315    fn utc_from_fields(&self) -> JsDate {
316        let g = |c| self.get_or_zero(c);
317        let mut date = JsDate::utc(
318            g(Component::Year),
319            g(Component::Month) - 1,
320            g(Component::Day),
321            g(Component::Hour),
322            g(Component::Minute),
323            g(Component::Second),
324            g(Component::Millisecond),
325        );
326        date.set_full_year(g(Component::Year));
327        date
328    }
329
330    /// `isValidDate()`.
331    pub fn is_valid_date(&self) -> bool {
332        let date = self.utc_from_fields();
333        if !date.is_valid() {
334            return false;
335        }
336        let g = |c| self.get_or_zero(c);
337        if date.get_full_year() != g(Component::Year) {
338            return false;
339        }
340        if date.get_month() != g(Component::Month) - 1 {
341            return false;
342        }
343        if date.get_date() != g(Component::Day) {
344            return false;
345        }
346        if self.get(Component::Hour).is_some_and(|h| date.get_hours() != h) {
347            return false;
348        }
349        if self.get(Component::Minute).is_some_and(|m| date.get_minutes() != m) {
350            return false;
351        }
352        true
353    }
354
355    /// `date()`: the instant these components describe.
356    pub fn date(&self) -> JsDate {
357        let offset = self.get(Component::TimezoneOffset).or(self.reference.timezone_offset);
358        match offset {
359            None => self.date_without_timezone_adjustment(),
360            Some(offset) => match self.utc_from_fields().get_time() {
361                Some(t) => JsDate::from_millis(t - offset * 60_000),
362                None => JsDate::INVALID,
363            },
364        }
365    }
366
367    /// The components read as system-local time (UTC here), with `setFullYear` fixing 0–99 years.
368    pub fn date_without_timezone_adjustment(&self) -> JsDate {
369        let g = |c| self.get_or_zero(c);
370        let mut date = JsDate::local(
371            g(Component::Year),
372            g(Component::Month) - 1,
373            g(Component::Day),
374            g(Component::Hour),
375            g(Component::Minute),
376            g(Component::Second),
377            g(Component::Millisecond),
378        );
379        date.set_full_year(g(Component::Year));
380        date
381    }
382
383    pub fn add_tag(&mut self, tag: &str) -> &mut Self {
384        if !self.tags.iter().any(|t| t == tag) {
385            self.tags.push(tag.to_string());
386        }
387        self
388    }
389
390    pub fn add_tags<'a>(&mut self, tags: impl IntoIterator<Item = &'a str>) -> &mut Self {
391        for tag in tags {
392            self.add_tag(tag);
393        }
394        self
395    }
396
397    /// Tags in insertion order (a JS `Set`).
398    /// upstream `clone()`: the reference and the known and implied values, **without the tags**.
399    /// Use this wherever upstream calls `.clone()` on components.
400    pub fn js_clone(&self) -> ParsingComponents {
401        ParsingComponents {
402            reference: self.reference,
403            known: self.known.clone(),
404            implied: self.implied.clone(),
405            tags: Vec::new(),
406        }
407    }
408
409    pub fn tags(&self) -> &[String] {
410        &self.tags
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417
418    fn reference() -> ReferenceWithTimezone {
419        ReferenceWithTimezone::new(JsDate::parse_iso("2012-08-10T09:30:00.000Z").unwrap(), None)
420    }
421
422    #[test]
423    fn js_clone_keeps_values_and_drops_tags_like_upstream() {
424        let mut c = ParsingComponents::new(reference(), &[(Component::Day, 21), (Component::Month, 7)]);
425        c.imply(Component::Year, 2038);
426        c.add_tag("parser/ENMonthNameMiddleEndianParser");
427        let js = c.js_clone();
428        assert_eq!(js.get(Component::Day), Some(21));
429        assert_eq!(js.get(Component::Year), Some(2038));
430        assert!(js.is_certain(Component::Month));
431        assert!(!js.is_certain(Component::Year));
432        assert!(js.tags().is_empty(), "upstream clone() doesn't copy tags");
433        assert_eq!(c.clone().tags(), c.tags(), "Rust Clone does");
434    }
435
436    #[test]
437    fn new_implies_reference_day_and_noon() {
438        let c = ParsingComponents::new(reference(), &[(Component::Weekday, 1)]);
439        assert_eq!(c.get(Component::Day), Some(10));
440        assert_eq!(c.get(Component::Hour), Some(12));
441        assert!(c.is_certain(Component::Weekday));
442        assert!(!c.is_certain(Component::Day));
443        assert_eq!(c.get(Component::TimezoneOffset), None);
444        assert_eq!(c.date().to_iso_string().unwrap(), "2012-08-10T12:00:00.000Z");
445    }
446
447    #[test]
448    fn assign_and_imply() {
449        let mut c = ParsingComponents::new(reference(), &[]);
450        c.assign(Component::Hour, 5).imply(Component::Hour, 7);
451        assert_eq!(c.get(Component::Hour), Some(5));
452        c.assign(Component::TimezoneOffset, 60);
453        assert_eq!(c.date().to_iso_string().unwrap(), "2012-08-10T04:00:00.000Z");
454    }
455
456    #[test]
457    fn add_duration_as_implied_crosses_month() {
458        let mut c = ParsingComponents::new(reference(), &[]);
459        c.add_duration_as_implied(&mut Duration::from_pairs(&[(DurationUnit::Day, 25.0)]));
460        assert_eq!(
461            (
462                c.get(Component::Month),
463                c.get(Component::Day),
464                c.get(Component::Weekday)
465            ),
466            (Some(9), Some(4), Some(2))
467        );
468        assert!(!c.is_certain(Component::Day));
469    }
470
471    #[test]
472    fn validity() {
473        let c = ParsingComponents::new(reference(), &[(Component::Month, 2), (Component::Day, 30)]);
474        assert!(!c.is_valid_date());
475        let c = ParsingComponents::new(reference(), &[(Component::Month, 2), (Component::Day, 29)]);
476        assert!(c.is_valid_date()); // 2012 is a leap year
477    }
478}