Skip to main content

chrono_node/calculation/
duration.rs

1//! upstream `calculation/duration.ts`.
2
3use crate::core::js_date::JsDate;
4
5/// A key of upstream's `Duration` object, including the short aliases.
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
7pub enum DurationUnit {
8    Year,
9    Quarter,
10    Month,
11    Week,
12    Day,
13    Hour,
14    Minute,
15    Second,
16    Millisecond,
17    /// `y`
18    ShortYear,
19    /// `mo`
20    ShortMonth,
21    /// `M`
22    ShortMonthUpper,
23    /// `w`
24    ShortWeek,
25    /// `d`
26    ShortDay,
27    /// `h`
28    ShortHour,
29    /// `m`
30    ShortMinute,
31    /// `s`
32    ShortSecond,
33    /// `ms`
34    ShortMillisecond,
35}
36
37impl DurationUnit {
38    /// The upstream key (`"quarter"`, `"mo"`, ...).
39    pub fn key(self) -> &'static str {
40        use DurationUnit::*;
41        match self {
42            Year => "year",
43            Quarter => "quarter",
44            Month => "month",
45            Week => "week",
46            Day => "day",
47            Hour => "hour",
48            Minute => "minute",
49            Second => "second",
50            Millisecond => "millisecond",
51            ShortYear => "y",
52            ShortMonth => "mo",
53            ShortMonthUpper => "M",
54            ShortWeek => "w",
55            ShortDay => "d",
56            ShortHour => "h",
57            ShortMinute => "m",
58            ShortSecond => "s",
59            ShortMillisecond => "ms",
60        }
61    }
62
63    pub fn from_key(key: &str) -> Option<DurationUnit> {
64        use DurationUnit::*;
65        [
66            Year,
67            Quarter,
68            Month,
69            Week,
70            Day,
71            Hour,
72            Minute,
73            Second,
74            Millisecond,
75            ShortYear,
76            ShortMonth,
77            ShortMonthUpper,
78            ShortWeek,
79            ShortDay,
80            ShortHour,
81            ShortMinute,
82            ShortSecond,
83            ShortMillisecond,
84        ]
85        .into_iter()
86        .find(|u| u.key() == key)
87    }
88}
89
90/// upstream `Duration`: a JS object of unit -> number, keeping insertion order.
91#[derive(Clone, Debug, Default, PartialEq)]
92pub struct Duration(Vec<(DurationUnit, f64)>);
93
94impl Duration {
95    pub fn new() -> Duration {
96        Duration(Vec::new())
97    }
98
99    pub fn from_pairs(pairs: &[(DurationUnit, f64)]) -> Duration {
100        let mut d = Duration::new();
101        for &(u, v) in pairs {
102            d.set(u, v);
103        }
104        d
105    }
106
107    /// upstream `EmptyDuration`: `{ day: 0, second: 0, millisecond: 0 }`.
108    pub fn empty() -> Duration {
109        Duration::from_pairs(&[
110            (DurationUnit::Day, 0.0),
111            (DurationUnit::Second, 0.0),
112            (DurationUnit::Millisecond, 0.0),
113        ])
114    }
115
116    /// `unit in duration`.
117    pub fn has(&self, unit: DurationUnit) -> bool {
118        self.0.iter().any(|(u, _)| *u == unit)
119    }
120
121    pub fn get(&self, unit: DurationUnit) -> Option<f64> {
122        self.0.iter().find(|(u, _)| *u == unit).map(|(_, v)| *v)
123    }
124
125    pub fn set(&mut self, unit: DurationUnit, value: f64) {
126        match self.0.iter_mut().find(|(u, _)| *u == unit) {
127            Some(slot) => slot.1 = value,
128            None => self.0.push((unit, value)),
129        }
130    }
131
132    pub fn remove(&mut self, unit: DurationUnit) {
133        self.0.retain(|(u, _)| *u != unit);
134    }
135
136    pub fn iter(&self) -> impl Iterator<Item = (DurationUnit, f64)> + '_ {
137        self.0.iter().copied()
138    }
139
140    pub fn is_empty(&self) -> bool {
141        self.0.is_empty()
142    }
143
144    fn add_to(&mut self, unit: DurationUnit, amount: f64) {
145        let current = self.get(unit).unwrap_or(0.0);
146        self.set(unit, current + amount);
147    }
148}
149
150/// JS `Math.round`: halves round up (towards +infinity).
151pub fn js_round(x: f64) -> f64 {
152    (x + 0.5).floor()
153}
154
155/// `addDuration(ref, duration)`. Mutates `duration` exactly like upstream.
156pub fn add_duration(reference: JsDate, duration: &mut Duration) -> JsDate {
157    use DurationUnit::*;
158    let mut date = reference;
159
160    for (short, long) in [
161        (ShortYear, Year),
162        (ShortMonth, Month),
163        (ShortMonthUpper, Month),
164        (ShortWeek, Week),
165        (ShortDay, Day),
166        (ShortHour, Hour),
167        (ShortMinute, Minute),
168        (ShortSecond, Second),
169        (ShortMillisecond, Millisecond),
170    ] {
171        // `if (duration["y"])`: JS truthiness, so 0 and NaN keep the short key.
172        if let Some(v) = duration.get(short) {
173            if v != 0.0 && !v.is_nan() {
174                duration.set(long, v);
175                duration.remove(short);
176            }
177        }
178    }
179
180    if let Some(v) = duration.get(Year) {
181        let floor = v.floor();
182        date.set_full_year(date.get_full_year() + floor as i64);
183        if v - floor > 0.0 {
184            duration.add_to(Month, (v - floor) * 12.0);
185        }
186    }
187    if let Some(v) = duration.get(Quarter) {
188        let floor = v.floor();
189        date.set_month(date.get_month() + floor as i64 * 3);
190        if v - floor > 0.0 {
191            duration.add_to(Month, (v - floor) * 3.0);
192        }
193    }
194    if let Some(v) = duration.get(Month) {
195        let floor = v.floor();
196        date.set_month(date.get_month() + floor as i64);
197        if v - floor > 0.0 {
198            duration.add_to(Week, (v - floor) * 4.0);
199        }
200    }
201    if let Some(v) = duration.get(Week) {
202        let floor = v.floor();
203        date.set_date(date.get_date() + floor as i64 * 7);
204        if v - floor > 0.0 {
205            duration.add_to(Day, js_round((v - floor) * 7.0));
206        }
207    }
208    if let Some(v) = duration.get(Day) {
209        let floor = v.floor();
210        date.set_date(date.get_date() + floor as i64);
211        if v - floor > 0.0 {
212            duration.add_to(Hour, js_round((v - floor) * 24.0));
213        }
214    }
215    if let Some(v) = duration.get(Hour) {
216        let floor = v.floor();
217        date.set_hours(date.get_hours() + floor as i64);
218        if v - floor > 0.0 {
219            duration.add_to(Minute, js_round((v - floor) * 60.0));
220        }
221    }
222    if let Some(v) = duration.get(Minute) {
223        let floor = v.floor();
224        date.set_minutes(date.get_minutes() + floor as i64);
225        if v - floor > 0.0 {
226            duration.add_to(Second, js_round((v - floor) * 60.0));
227        }
228    }
229    if let Some(v) = duration.get(Second) {
230        let floor = v.floor();
231        date.set_seconds(date.get_seconds() + floor as i64);
232        if v - floor > 0.0 {
233            duration.add_to(Millisecond, js_round((v - floor) * 1000.0));
234        }
235    }
236    if let Some(v) = duration.get(Millisecond) {
237        date.set_milliseconds(date.get_milliseconds() + v.floor() as i64);
238    }
239    date
240}
241
242/// `reverseDuration(duration)`.
243pub fn reverse_duration(duration: &Duration) -> Duration {
244    Duration(duration.iter().map(|(u, v)| (u, -v)).collect())
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use DurationUnit::*;
251
252    fn at(iso: &str) -> JsDate {
253        JsDate::parse_iso(iso).unwrap()
254    }
255
256    #[test]
257    fn whole_units() {
258        let mut d = Duration::from_pairs(&[(Month, 1.0)]);
259        assert_eq!(
260            add_duration(at("2012-01-31T00:00:00.000Z"), &mut d)
261                .to_iso_string()
262                .unwrap(),
263            "2012-03-02T00:00:00.000Z"
264        );
265        let mut d = Duration::from_pairs(&[(Day, -1.0), (Hour, 2.0)]);
266        assert_eq!(
267            add_duration(at("2012-03-01T23:00:00.000Z"), &mut d)
268                .to_iso_string()
269                .unwrap(),
270            "2012-03-01T01:00:00.000Z"
271        );
272    }
273
274    #[test]
275    fn fractions_cascade_and_mutate_like_js() {
276        // 1.5 days -> 1 day + round(0.5 * 24) = 12 hours; upstream adds "hour" to the object.
277        let mut d = Duration::from_pairs(&[(Day, 1.5)]);
278        let out = add_duration(at("2012-08-10T00:00:00.000Z"), &mut d);
279        assert_eq!(out.to_iso_string().unwrap(), "2012-08-11T12:00:00.000Z");
280        assert_eq!(d.get(Hour), Some(12.0));
281        // Short keys are renamed in place.
282        let mut d = Duration::from_pairs(&[(ShortDay, 2.0), (ShortYear, 0.0)]);
283        add_duration(at("2012-08-10T00:00:00.000Z"), &mut d);
284        assert!(d.has(Day) && !d.has(ShortDay) && d.has(ShortYear));
285    }
286
287    #[test]
288    fn js_round_halves_up() {
289        assert_eq!(js_round(2.5), 3.0);
290        assert_eq!(js_round(-2.5), -2.0);
291    }
292
293    #[test]
294    fn reverse() {
295        let d = reverse_duration(&Duration::from_pairs(&[(Week, 2.0), (Day, 1.0)]));
296        assert_eq!(d, Duration::from_pairs(&[(Week, -2.0), (Day, -1.0)]));
297    }
298}