1use std::time::{SystemTime, UNIX_EPOCH};
12
13const MS_PER_DAY: i128 = 86_400_000;
14const MAX_TIME: i128 = 8_640_000_000_000_000;
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub struct JsDate {
20 t: Option<i64>,
21}
22
23fn floor_div(a: i128, b: i128) -> i128 {
24 let q = a / b;
25 if (a % b != 0) && ((a < 0) != (b < 0)) {
26 q - 1
27 } else {
28 q
29 }
30}
31
32fn floor_mod(a: i128, b: i128) -> i128 {
33 a - floor_div(a, b) * b
34}
35
36fn days_from_civil(y: i128, m: i128, d: i128) -> i128 {
38 let y = if m <= 2 { y - 1 } else { y };
39 let era = floor_div(y, 400);
40 let yoe = y - era * 400;
41 let mp = (m + 9) % 12;
42 let doy = (153 * mp + 2) / 5 + d - 1;
43 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
44 era * 146_097 + doe - 719_468
45}
46
47fn civil_from_days(z: i128) -> (i128, i128, i128) {
49 let z = z + 719_468;
50 let era = floor_div(z, 146_097);
51 let doe = z - era * 146_097;
52 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
53 let y = yoe + era * 400;
54 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
55 let mp = (5 * doy + 2) / 153;
56 let d = doy - (153 * mp + 2) / 5 + 1;
57 let m = if mp < 10 { mp + 3 } else { mp - 9 };
58 (if m <= 2 { y + 1 } else { y }, m, d)
59}
60
61fn make_day(year: i64, month: i64, date: i64) -> Option<i128> {
63 let (year, month, date) = (year as i128, month as i128, date as i128);
64 let ym = year + floor_div(month, 12);
65 if ym.abs() > 400_000 {
66 return None;
67 }
68 let mn = floor_mod(month, 12);
69 Some(days_from_civil(ym, mn + 1, 1) + date - 1)
70}
71
72fn make_time(h: i64, m: i64, s: i64, ms: i64) -> i128 {
73 h as i128 * 3_600_000 + m as i128 * 60_000 + s as i128 * 1000 + ms as i128
74}
75
76fn time_clip(t: i128) -> Option<i64> {
77 (t.abs() <= MAX_TIME).then_some(t as i64)
78}
79
80fn make_date(day: Option<i128>, time: i128) -> Option<i64> {
81 day.and_then(|day| time_clip(day * MS_PER_DAY + time))
82}
83
84impl JsDate {
85 pub const INVALID: JsDate = JsDate { t: None };
87
88 pub fn from_millis(ms: i64) -> JsDate {
90 JsDate {
91 t: time_clip(ms as i128),
92 }
93 }
94
95 pub fn now() -> JsDate {
97 let ms = SystemTime::now()
98 .duration_since(UNIX_EPOCH)
99 .map(|d| d.as_millis() as i64)
100 .unwrap_or(0);
101 JsDate::from_millis(ms)
102 }
103
104 pub fn local(year: i64, month: i64, day: i64, hour: i64, minute: i64, second: i64, ms: i64) -> JsDate {
106 let year = if (0..=99).contains(&year) { 1900 + year } else { year };
107 JsDate {
108 t: make_date(make_day(year, month, day), make_time(hour, minute, second, ms)),
109 }
110 }
111
112 pub fn utc(year: i64, month: i64, day: i64, hour: i64, minute: i64, second: i64, ms: i64) -> JsDate {
114 JsDate::local(year, month, day, hour, minute, second, ms)
115 }
116
117 pub fn is_valid(&self) -> bool {
118 self.t.is_some()
119 }
120
121 pub fn get_time(&self) -> Option<i64> {
123 self.t
124 }
125
126 fn parts(&self) -> Option<(i128, i128, i128, i128, i128)> {
127 let t = self.t? as i128;
128 let day = floor_div(t, MS_PER_DAY);
129 let (y, m, d) = civil_from_days(day);
130 Some((y, m, d, day, t - day * MS_PER_DAY))
131 }
132
133 pub fn get_full_year(&self) -> i64 {
137 self.parts().map_or(0, |p| p.0 as i64)
138 }
139 pub fn get_month(&self) -> i64 {
141 self.parts().map_or(0, |p| p.1 as i64 - 1)
142 }
143 pub fn get_date(&self) -> i64 {
144 self.parts().map_or(0, |p| p.2 as i64)
145 }
146 pub fn get_day(&self) -> i64 {
148 self.parts().map_or(0, |p| floor_mod(p.3 + 4, 7) as i64)
149 }
150 pub fn get_hours(&self) -> i64 {
151 self.parts().map_or(0, |p| (p.4 / 3_600_000) as i64)
152 }
153 pub fn get_minutes(&self) -> i64 {
154 self.parts().map_or(0, |p| (p.4 / 60_000 % 60) as i64)
155 }
156 pub fn get_seconds(&self) -> i64 {
157 self.parts().map_or(0, |p| (p.4 / 1000 % 60) as i64)
158 }
159 pub fn get_milliseconds(&self) -> i64 {
160 self.parts().map_or(0, |p| (p.4 % 1000) as i64)
161 }
162 pub fn get_timezone_offset(&self) -> i64 {
164 0
165 }
166
167 fn time_within_day(&self) -> i128 {
168 self.parts().map_or(0, |p| p.4)
169 }
170
171 fn rebuild(&mut self, year: i64, month: i64, day: i64, time: i128) {
172 self.t = make_date(make_day(year, month, day), time);
173 }
174
175 pub fn set_full_year(&mut self, year: i64) {
177 if self.t.is_none() {
178 self.t = Some(0);
179 }
180 let (m, d, time) = (self.get_month(), self.get_date(), self.time_within_day());
181 self.rebuild(year, m, d, time);
182 }
183
184 pub fn set_month(&mut self, month: i64) {
186 if self.t.is_some() {
187 let (y, d, time) = (self.get_full_year(), self.get_date(), self.time_within_day());
188 self.rebuild(y, month, d, time);
189 }
190 }
191
192 pub fn set_date(&mut self, day: i64) {
194 if self.t.is_some() {
195 let (y, m, time) = (self.get_full_year(), self.get_month(), self.time_within_day());
196 self.rebuild(y, m, day, time);
197 }
198 }
199
200 fn set_time_fields(&mut self, h: i64, mi: i64, s: i64, ms: i64) {
201 if let Some((y, m, d, _, _)) = self.parts() {
202 self.t = make_date(make_day(y as i64, m as i64 - 1, d as i64), make_time(h, mi, s, ms));
203 }
204 }
205
206 pub fn set_hours(&mut self, hour: i64) {
207 let (mi, s, ms) = (self.get_minutes(), self.get_seconds(), self.get_milliseconds());
208 self.set_time_fields(hour, mi, s, ms);
209 }
210 pub fn set_minutes(&mut self, minute: i64) {
211 let (h, s, ms) = (self.get_hours(), self.get_seconds(), self.get_milliseconds());
212 self.set_time_fields(h, minute, s, ms);
213 }
214 pub fn set_seconds(&mut self, second: i64) {
215 let (h, mi, ms) = (self.get_hours(), self.get_minutes(), self.get_milliseconds());
216 self.set_time_fields(h, mi, second, ms);
217 }
218 pub fn set_milliseconds(&mut self, ms: i64) {
219 let (h, mi, s) = (self.get_hours(), self.get_minutes(), self.get_seconds());
220 self.set_time_fields(h, mi, s, ms);
221 }
222
223 pub fn to_iso_string(&self) -> Option<String> {
225 let (y, m, d, _, tod) = self.parts()?;
226 let (h, mi, s, ms) = (tod / 3_600_000, tod / 60_000 % 60, tod / 1000 % 60, tod % 1000);
227 let year = if (0..=9999).contains(&y) {
228 format!("{y:04}")
229 } else if y < 0 {
230 format!("-{:06}", -y)
231 } else {
232 format!("+{y:06}")
233 };
234 Some(format!("{year}-{m:02}-{d:02}T{h:02}:{mi:02}:{s:02}.{ms:03}Z"))
235 }
236
237 pub fn parse_iso(s: &str) -> Option<JsDate> {
239 let (sign, rest) = match s.as_bytes().first()? {
240 b'+' => (1, &s[1..]),
241 b'-' => (-1, &s[1..]),
242 _ => (1, s),
243 };
244 let (date, time) = rest.strip_suffix('Z')?.split_once('T')?;
245 let mut dp = date.split('-');
246 let year: i64 = dp.next()?.parse().ok()?;
247 let month: i64 = dp.next()?.parse().ok()?;
248 let day: i64 = dp.next()?.parse().ok()?;
249 let (hms, ms) = time.split_once('.').unwrap_or((time, "0"));
250 let mut tp = hms.split(':');
251 let h: i64 = tp.next()?.parse().ok()?;
252 let mi: i64 = tp.next()?.parse().ok()?;
253 let sec: i64 = tp.next()?.parse().ok()?;
254 let ms: i64 = ms.parse().ok()?;
255 let t = make_date(make_day(sign * year, month - 1, day), make_time(h, mi, sec, ms))?;
256 Some(JsDate::from_millis(t))
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn epoch_and_iso() {
266 let d = JsDate::from_millis(0);
267 assert_eq!(d.to_iso_string().unwrap(), "1970-01-01T00:00:00.000Z");
268 assert_eq!(d.get_day(), 4); let d = JsDate::parse_iso("2012-08-09T00:00:00.000Z").unwrap();
270 assert_eq!(
271 (d.get_full_year(), d.get_month(), d.get_date(), d.get_day()),
272 (2012, 7, 9, 4)
273 );
274 assert_eq!(
275 JsDate::parse_iso("-000001-01-01T00:00:00.000Z")
276 .unwrap()
277 .to_iso_string()
278 .unwrap(),
279 "-000001-01-01T00:00:00.000Z"
280 );
281 assert_eq!(
282 JsDate::parse_iso("+012345-06-07T08:09:10.011Z")
283 .unwrap()
284 .to_iso_string()
285 .unwrap(),
286 "+012345-06-07T08:09:10.011Z"
287 );
288 }
289
290 #[test]
291 fn js_overflow_rules() {
292 let mut d = JsDate::local(2012, 0, 31, 0, 0, 0, 0);
294 d.set_month(1);
295 assert_eq!(d.to_iso_string().unwrap(), "2012-03-02T00:00:00.000Z");
296 let mut d = JsDate::local(2013, 2, 15, 12, 0, 0, 0);
298 d.set_date(0);
299 assert_eq!(d.to_iso_string().unwrap(), "2013-02-28T12:00:00.000Z");
300 let d = JsDate::local(2013, -1, 1, 0, 0, 0, 0);
302 assert_eq!(d.to_iso_string().unwrap(), "2012-12-01T00:00:00.000Z");
303 let mut d = JsDate::local(2013, 11, 31, 23, 0, 0, 0);
305 d.set_hours(25);
306 assert_eq!(d.to_iso_string().unwrap(), "2014-01-01T01:00:00.000Z");
307 let mut d = JsDate::local(2013, 0, 1, 0, 0, 0, 0);
308 d.set_minutes(-1);
309 assert_eq!(d.to_iso_string().unwrap(), "2012-12-31T23:59:00.000Z");
310 }
311
312 #[test]
313 fn two_digit_years_map_to_1900s_like_js() {
314 assert_eq!(JsDate::local(99, 0, 1, 0, 0, 0, 0).get_full_year(), 1999);
315 let mut d = JsDate::local(0, 1, 29, 0, 0, 0, 0); d.set_full_year(0);
317 assert_eq!(d.to_iso_string().unwrap(), "0000-03-01T00:00:00.000Z");
318 }
319
320 #[test]
321 fn time_clip_makes_invalid() {
322 assert!(!JsDate::local(300_000, 0, 1, 0, 0, 0, 0).is_valid());
323 assert!(JsDate::from_millis(8_640_000_000_000_000).is_valid());
324 assert!(!JsDate::from_millis(8_640_000_000_000_001).is_valid());
325 let mut d = JsDate::INVALID;
326 d.set_full_year(2000);
327 assert_eq!(d.to_iso_string().unwrap(), "2000-01-01T00:00:00.000Z");
328 }
329}