mod parse;
use crate::layout::TimeZone;
const MS_PER_SECOND: i64 = 1000;
const MS_PER_MINUTE: i64 = 60000;
const MS_PER_HOUR: i64 = 3600000;
const MS_PER_DAY: i64 = 86400000;
pub(crate) const MAX_TIME: f64 = 8.64e15;
const MAX_TIME_BEFORE_UTC: f64 = MAX_TIME + 864000000.0;
const MIN_YEAR: f64 = -1000000.0;
const MAX_YEAR: f64 = 1000000.0;
const MIN_MONTH: f64 = -10000000.0;
const MAX_MONTH: f64 = 10000000.0;
pub(crate) fn valid(t: f64) -> bool {
!t.is_nan()
}
fn to_integer(x: f64) -> f64 {
if x.is_nan() || x == 0.0 {
return 0.0;
}
if x.is_infinite() {
return x;
}
x.trunc() + 0.0
}
pub(crate) fn time_clip(t: f64) -> f64 {
if (-MAX_TIME..=MAX_TIME).contains(&t) {
to_integer(t)
} else {
f64::NAN
}
}
pub(crate) fn make_time(h: f64, m: f64, s: f64, ms: f64) -> f64 {
if h.is_finite() && m.is_finite() && s.is_finite() && ms.is_finite() {
to_integer(h) * MS_PER_HOUR as f64
+ to_integer(m) * MS_PER_MINUTE as f64
+ to_integer(s) * MS_PER_SECOND as f64
+ to_integer(ms)
} else {
f64::NAN
}
}
const DAYS_BEFORE_MONTH: [[i64; 12]; 2] = [
[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334],
[0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335],
];
pub(crate) fn make_day(year: f64, month: f64, date: f64) -> f64 {
if !((MIN_YEAR..=MAX_YEAR).contains(&year)
&& (MIN_MONTH..=MAX_MONTH).contains(&month)
&& date.is_finite())
{
return f64::NAN;
}
let mut y = year as i64;
let mut m = month as i64;
y += m / 12;
m %= 12;
if m < 0 {
m += 12;
y -= 1;
}
const YEAR_DELTA: i64 = 399999;
const BASE_DAY: i64 = 365 * (1970 + YEAR_DELTA) + (1970 + YEAR_DELTA) / 4
- (1970 + YEAR_DELTA) / 100
+ (1970 + YEAR_DELTA) / 400;
let mut day = 365 * (y + YEAR_DELTA) + (y + YEAR_DELTA) / 4 - (y + YEAR_DELTA) / 100
+ (y + YEAR_DELTA) / 400
- BASE_DAY;
let leap = usize::from(y % 4 == 0 && (y % 100 != 0 || y % 400 == 0));
day += DAYS_BEFORE_MONTH[leap][m as usize];
(day - 1) as f64 + to_integer(date)
}
pub(crate) fn make_date(day: f64, t: f64) -> f64 {
if !day.is_finite() || !t.is_finite() {
return f64::NAN;
}
if t == 0.0 && day != 0.0 {
return day * MS_PER_DAY as f64;
}
t + day * MS_PER_DAY as f64
}
#[derive(Clone, Copy)]
pub(crate) struct Zone<'a>(pub(crate) &'a dyn TimeZone);
struct Fields {
year: i64,
month: i64,
date: i64,
weekday: i64,
days: i64,
time_in_day: i64,
}
fn civil(days: i64) -> (i64, i64, i64) {
let z = days + 719468;
let era = z.div_euclid(146097);
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let mut 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 mut m = mp + 3;
if m > 12 {
m -= 12;
}
if m <= 2 {
y += 1;
}
(y, m - 1, d)
}
impl Zone<'_> {
fn offset(self, t: i64) -> i64 {
i64::from(self.0.offset(t.div_euclid(MS_PER_SECOND))) * MS_PER_SECOND
}
fn to_local(self, t: i64) -> i64 {
t + self.offset(t)
}
pub(crate) fn to_utc(self, local: i64) -> i64 {
let before = self.offset(local - MS_PER_DAY);
let after = self.offset(local + MS_PER_DAY);
if before == after {
return local - before;
}
let (early, late) = (local - before, local - after);
let early_fits = self.offset(early) == before;
let late_fits = self.offset(late) == after;
match (early_fits, late_fits) {
(true, true) => early.min(late),
(true, false) => early,
(false, true) => late,
(false, false) => early,
}
}
fn set_local(self, local: f64) -> f64 {
if (-MAX_TIME_BEFORE_UTC..=MAX_TIME_BEFORE_UTC).contains(&local) {
time_clip(self.to_utc(local as i64) as f64)
} else {
f64::NAN
}
}
fn break_down(self, t: f64) -> Fields {
let local = self.to_local(t as i64);
let days = local.div_euclid(MS_PER_DAY);
let (year, month, date) = civil(days);
Fields {
year,
month,
date,
weekday: (days + 4).rem_euclid(7),
days,
time_in_day: local - days * MS_PER_DAY,
}
}
fn field(self, t: f64, of: impl FnOnce(Fields) -> i64) -> f64 {
if t.is_nan() {
return t;
}
of(self.break_down(t)) as f64
}
pub(crate) fn full_year(self, t: f64) -> f64 {
self.field(t, |f| f.year)
}
pub(crate) fn month(self, t: f64) -> f64 {
self.field(t, |f| f.month)
}
pub(crate) fn date(self, t: f64) -> f64 {
self.field(t, |f| f.date)
}
pub(crate) fn day(self, t: f64) -> f64 {
self.field(t, |f| f.weekday)
}
pub(crate) fn hours(self, t: f64) -> f64 {
self.field(t, |f| f.time_in_day / MS_PER_HOUR)
}
pub(crate) fn minutes(self, t: f64) -> f64 {
self.field(t, |f| f.time_in_day / MS_PER_MINUTE % 60)
}
pub(crate) fn seconds(self, t: f64) -> f64 {
self.field(t, |f| f.time_in_day / MS_PER_SECOND % 60)
}
pub(crate) fn milliseconds(self, t: f64) -> f64 {
self.field(t, |f| f.time_in_day % MS_PER_SECOND)
}
pub(crate) fn timezone_offset(self, t: f64) -> f64 {
if t.is_nan() {
return t;
}
let ms = t as i64;
((ms - self.to_local(ms)) / MS_PER_MINUTE) as f64
}
pub(crate) fn set_date(self, t: f64, date: f64) -> f64 {
if t.is_nan() {
return t;
}
let f = self.break_down(t);
let day = make_day(f.year as f64, f.month as f64, date);
self.set_local(make_date(day, f.time_in_day as f64))
}
pub(crate) fn set_month(self, t: f64, month: f64) -> f64 {
if t.is_nan() {
return t;
}
let f = self.break_down(t);
let day = make_day(f.year as f64, month, f.date as f64);
self.set_local(make_date(day, f.time_in_day as f64))
}
pub(crate) fn set_month_date(self, t: f64, month: f64, date: f64) -> f64 {
if t.is_nan() {
return t;
}
let f = self.break_down(t);
let day = make_day(f.year as f64, month, date);
self.set_local(make_date(day, f.time_in_day as f64))
}
pub(crate) fn set_full_year(self, t: f64, year: f64) -> f64 {
let (m, d, tid) = if t.is_nan() {
(0.0, 1.0, 0.0)
} else {
let f = self.break_down(t);
(f.month as f64, f.date as f64, f.time_in_day as f64)
};
self.set_local(make_date(make_day(year, m, d), tid))
}
pub(crate) fn set_hours(self, t: f64, h: f64, m: f64, s: f64, ms: f64) -> f64 {
if t.is_nan() {
return t;
}
let f = self.break_down(t);
self.set_local(make_date(f.days as f64, make_time(h, m, s, ms)))
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn local(
self,
year: f64,
month: f64,
date: f64,
h: f64,
m: f64,
s: f64,
ms: f64,
) -> f64 {
let day = make_day(full_year(year), month, date);
self.set_local(make_date(day, make_time(h, m, s, ms)))
}
pub(crate) fn parse(self, s: &str) -> f64 {
let units: Vec<u16> = s.encode_utf16().collect();
let Some(out) = parse::parse_date(&units) else {
return f64::NAN;
};
let day = make_day(out.year, out.month, out.day);
let t = make_time(out.hour, out.minute, out.second, out.millisecond);
let mut date = make_date(day, t);
match out.utc_offset {
None => {
if !(-MAX_TIME_BEFORE_UTC..=MAX_TIME_BEFORE_UTC).contains(&date) {
return f64::NAN;
}
date = self.to_utc(date as i64) as f64;
}
Some(offset) => {
date -= offset * 1000.0;
if !(-MAX_TIME..=MAX_TIME).contains(&date) {
return f64::NAN;
}
}
}
time_clip(date)
}
}
pub(crate) fn utc_seconds(t: f64) -> f64 {
if t.is_nan() {
return t;
}
let ms = t as i64;
((ms - ms.div_euclid(MS_PER_DAY) * MS_PER_DAY) / MS_PER_SECOND % 60) as f64
}
pub(crate) fn set_time(v: f64) -> f64 {
time_clip(v)
}
fn full_year(year: f64) -> f64 {
if !year.is_nan() {
let y = to_integer(year);
if (0.0..=99.0).contains(&y) {
return 1900.0 + y;
}
}
year
}
pub(crate) fn utc(year: f64, month: f64, date: f64, h: f64, m: f64, s: f64, ms: f64) -> f64 {
time_clip(make_date(
make_day(full_year(year), month, date),
make_time(h, m, s, ms),
))
}
#[cfg(test)]
mod tests;