1use crate::core::js_date::JsDate;
4
5#[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 ShortYear,
19 ShortMonth,
21 ShortMonthUpper,
23 ShortWeek,
25 ShortDay,
27 ShortHour,
29 ShortMinute,
31 ShortSecond,
33 ShortMillisecond,
35}
36
37impl DurationUnit {
38 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#[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 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 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
150pub fn js_round(x: f64) -> f64 {
152 (x + 0.5).floor()
153}
154
155pub 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 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
242pub 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 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 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}