use std::time::{SystemTime, UNIX_EPOCH};
const MS_PER_DAY: i128 = 86_400_000;
const MAX_TIME: i128 = 8_640_000_000_000_000;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct JsDate {
t: Option<i64>,
}
fn floor_div(a: i128, b: i128) -> i128 {
let q = a / b;
if (a % b != 0) && ((a < 0) != (b < 0)) {
q - 1
} else {
q
}
}
fn floor_mod(a: i128, b: i128) -> i128 {
a - floor_div(a, b) * b
}
fn days_from_civil(y: i128, m: i128, d: i128) -> i128 {
let y = if m <= 2 { y - 1 } else { y };
let era = floor_div(y, 400);
let yoe = y - era * 400;
let mp = (m + 9) % 12;
let doy = (153 * mp + 2) / 5 + d - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
fn civil_from_days(z: i128) -> (i128, i128, i128) {
let z = z + 719_468;
let era = floor_div(z, 146_097);
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
(if m <= 2 { y + 1 } else { y }, m, d)
}
fn make_day(year: i64, month: i64, date: i64) -> Option<i128> {
let (year, month, date) = (year as i128, month as i128, date as i128);
let ym = year + floor_div(month, 12);
if ym.abs() > 400_000 {
return None;
}
let mn = floor_mod(month, 12);
Some(days_from_civil(ym, mn + 1, 1) + date - 1)
}
fn make_time(h: i64, m: i64, s: i64, ms: i64) -> i128 {
h as i128 * 3_600_000 + m as i128 * 60_000 + s as i128 * 1000 + ms as i128
}
fn time_clip(t: i128) -> Option<i64> {
(t.abs() <= MAX_TIME).then_some(t as i64)
}
fn make_date(day: Option<i128>, time: i128) -> Option<i64> {
day.and_then(|day| time_clip(day * MS_PER_DAY + time))
}
impl JsDate {
pub const INVALID: JsDate = JsDate { t: None };
pub fn from_millis(ms: i64) -> JsDate {
JsDate {
t: time_clip(ms as i128),
}
}
pub fn now() -> JsDate {
let ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
JsDate::from_millis(ms)
}
pub fn local(year: i64, month: i64, day: i64, hour: i64, minute: i64, second: i64, ms: i64) -> JsDate {
let year = if (0..=99).contains(&year) { 1900 + year } else { year };
JsDate {
t: make_date(make_day(year, month, day), make_time(hour, minute, second, ms)),
}
}
pub fn utc(year: i64, month: i64, day: i64, hour: i64, minute: i64, second: i64, ms: i64) -> JsDate {
JsDate::local(year, month, day, hour, minute, second, ms)
}
pub fn is_valid(&self) -> bool {
self.t.is_some()
}
pub fn get_time(&self) -> Option<i64> {
self.t
}
fn parts(&self) -> Option<(i128, i128, i128, i128, i128)> {
let t = self.t? as i128;
let day = floor_div(t, MS_PER_DAY);
let (y, m, d) = civil_from_days(day);
Some((y, m, d, day, t - day * MS_PER_DAY))
}
pub fn get_full_year(&self) -> i64 {
self.parts().map_or(0, |p| p.0 as i64)
}
pub fn get_month(&self) -> i64 {
self.parts().map_or(0, |p| p.1 as i64 - 1)
}
pub fn get_date(&self) -> i64 {
self.parts().map_or(0, |p| p.2 as i64)
}
pub fn get_day(&self) -> i64 {
self.parts().map_or(0, |p| floor_mod(p.3 + 4, 7) as i64)
}
pub fn get_hours(&self) -> i64 {
self.parts().map_or(0, |p| (p.4 / 3_600_000) as i64)
}
pub fn get_minutes(&self) -> i64 {
self.parts().map_or(0, |p| (p.4 / 60_000 % 60) as i64)
}
pub fn get_seconds(&self) -> i64 {
self.parts().map_or(0, |p| (p.4 / 1000 % 60) as i64)
}
pub fn get_milliseconds(&self) -> i64 {
self.parts().map_or(0, |p| (p.4 % 1000) as i64)
}
pub fn get_timezone_offset(&self) -> i64 {
0
}
fn time_within_day(&self) -> i128 {
self.parts().map_or(0, |p| p.4)
}
fn rebuild(&mut self, year: i64, month: i64, day: i64, time: i128) {
self.t = make_date(make_day(year, month, day), time);
}
pub fn set_full_year(&mut self, year: i64) {
if self.t.is_none() {
self.t = Some(0);
}
let (m, d, time) = (self.get_month(), self.get_date(), self.time_within_day());
self.rebuild(year, m, d, time);
}
pub fn set_month(&mut self, month: i64) {
if self.t.is_some() {
let (y, d, time) = (self.get_full_year(), self.get_date(), self.time_within_day());
self.rebuild(y, month, d, time);
}
}
pub fn set_date(&mut self, day: i64) {
if self.t.is_some() {
let (y, m, time) = (self.get_full_year(), self.get_month(), self.time_within_day());
self.rebuild(y, m, day, time);
}
}
fn set_time_fields(&mut self, h: i64, mi: i64, s: i64, ms: i64) {
if let Some((y, m, d, _, _)) = self.parts() {
self.t = make_date(make_day(y as i64, m as i64 - 1, d as i64), make_time(h, mi, s, ms));
}
}
pub fn set_hours(&mut self, hour: i64) {
let (mi, s, ms) = (self.get_minutes(), self.get_seconds(), self.get_milliseconds());
self.set_time_fields(hour, mi, s, ms);
}
pub fn set_minutes(&mut self, minute: i64) {
let (h, s, ms) = (self.get_hours(), self.get_seconds(), self.get_milliseconds());
self.set_time_fields(h, minute, s, ms);
}
pub fn set_seconds(&mut self, second: i64) {
let (h, mi, ms) = (self.get_hours(), self.get_minutes(), self.get_milliseconds());
self.set_time_fields(h, mi, second, ms);
}
pub fn set_milliseconds(&mut self, ms: i64) {
let (h, mi, s) = (self.get_hours(), self.get_minutes(), self.get_seconds());
self.set_time_fields(h, mi, s, ms);
}
pub fn to_iso_string(&self) -> Option<String> {
let (y, m, d, _, tod) = self.parts()?;
let (h, mi, s, ms) = (tod / 3_600_000, tod / 60_000 % 60, tod / 1000 % 60, tod % 1000);
let year = if (0..=9999).contains(&y) {
format!("{y:04}")
} else if y < 0 {
format!("-{:06}", -y)
} else {
format!("+{y:06}")
};
Some(format!("{year}-{m:02}-{d:02}T{h:02}:{mi:02}:{s:02}.{ms:03}Z"))
}
pub fn parse_iso(s: &str) -> Option<JsDate> {
let (sign, rest) = match s.as_bytes().first()? {
b'+' => (1, &s[1..]),
b'-' => (-1, &s[1..]),
_ => (1, s),
};
let (date, time) = rest.strip_suffix('Z')?.split_once('T')?;
let mut dp = date.split('-');
let year: i64 = dp.next()?.parse().ok()?;
let month: i64 = dp.next()?.parse().ok()?;
let day: i64 = dp.next()?.parse().ok()?;
let (hms, ms) = time.split_once('.').unwrap_or((time, "0"));
let mut tp = hms.split(':');
let h: i64 = tp.next()?.parse().ok()?;
let mi: i64 = tp.next()?.parse().ok()?;
let sec: i64 = tp.next()?.parse().ok()?;
let ms: i64 = ms.parse().ok()?;
let t = make_date(make_day(sign * year, month - 1, day), make_time(h, mi, sec, ms))?;
Some(JsDate::from_millis(t))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn epoch_and_iso() {
let d = JsDate::from_millis(0);
assert_eq!(d.to_iso_string().unwrap(), "1970-01-01T00:00:00.000Z");
assert_eq!(d.get_day(), 4); let d = JsDate::parse_iso("2012-08-09T00:00:00.000Z").unwrap();
assert_eq!(
(d.get_full_year(), d.get_month(), d.get_date(), d.get_day()),
(2012, 7, 9, 4)
);
assert_eq!(
JsDate::parse_iso("-000001-01-01T00:00:00.000Z")
.unwrap()
.to_iso_string()
.unwrap(),
"-000001-01-01T00:00:00.000Z"
);
assert_eq!(
JsDate::parse_iso("+012345-06-07T08:09:10.011Z")
.unwrap()
.to_iso_string()
.unwrap(),
"+012345-06-07T08:09:10.011Z"
);
}
#[test]
fn js_overflow_rules() {
let mut d = JsDate::local(2012, 0, 31, 0, 0, 0, 0);
d.set_month(1);
assert_eq!(d.to_iso_string().unwrap(), "2012-03-02T00:00:00.000Z");
let mut d = JsDate::local(2013, 2, 15, 12, 0, 0, 0);
d.set_date(0);
assert_eq!(d.to_iso_string().unwrap(), "2013-02-28T12:00:00.000Z");
let d = JsDate::local(2013, -1, 1, 0, 0, 0, 0);
assert_eq!(d.to_iso_string().unwrap(), "2012-12-01T00:00:00.000Z");
let mut d = JsDate::local(2013, 11, 31, 23, 0, 0, 0);
d.set_hours(25);
assert_eq!(d.to_iso_string().unwrap(), "2014-01-01T01:00:00.000Z");
let mut d = JsDate::local(2013, 0, 1, 0, 0, 0, 0);
d.set_minutes(-1);
assert_eq!(d.to_iso_string().unwrap(), "2012-12-31T23:59:00.000Z");
}
#[test]
fn two_digit_years_map_to_1900s_like_js() {
assert_eq!(JsDate::local(99, 0, 1, 0, 0, 0, 0).get_full_year(), 1999);
let mut d = JsDate::local(0, 1, 29, 0, 0, 0, 0); d.set_full_year(0);
assert_eq!(d.to_iso_string().unwrap(), "0000-03-01T00:00:00.000Z");
}
#[test]
fn time_clip_makes_invalid() {
assert!(!JsDate::local(300_000, 0, 1, 0, 0, 0, 0).is_valid());
assert!(JsDate::from_millis(8_640_000_000_000_000).is_valid());
assert!(!JsDate::from_millis(8_640_000_000_000_001).is_valid());
let mut d = JsDate::INVALID;
d.set_full_year(2000);
assert_eq!(d.to_iso_string().unwrap(), "2000-01-01T00:00:00.000Z");
}
}