use crate::{context::HostHooks, js_string, value::IntegerOrInfinity, JsStr, JsString};
use boa_macros::js_str;
use std::{iter::Peekable, str::Chars};
use time::{macros::format_description, OffsetDateTime, PrimitiveDateTime};
const HOURS_PER_DAY: f64 = 24.0;
const MINUTES_PER_HOUR: f64 = 60.0;
const SECONDS_PER_MINUTE: f64 = 60.0;
const MS_PER_SECOND: f64 = 1000.0;
pub(super) const MS_PER_MINUTE: f64 = MS_PER_SECOND * SECONDS_PER_MINUTE;
const MS_PER_HOUR: f64 = MS_PER_MINUTE * MINUTES_PER_HOUR;
const MS_PER_DAY: f64 = MS_PER_HOUR * HOURS_PER_DAY;
pub(super) fn day(t: f64) -> f64 {
(t / MS_PER_DAY).floor()
}
pub(super) fn time_within_day(t: f64) -> f64 {
t.rem_euclid(MS_PER_DAY)
}
fn days_in_year(y: f64) -> u16 {
let ry = y;
if ry.rem_euclid(400.0) == 0.0 {
return 366;
}
if ry.rem_euclid(100.0) == 0.0 {
return 365;
}
if ry.rem_euclid(4.0) == 0.0 {
return 366;
}
365
}
fn day_from_year(y: f64) -> f64 {
let num_years_1 = y - 1970.0;
let num_years_4 = ((y - 1969.0) / 4.0).floor();
let num_years_100 = ((y - 1901.0) / 100.0).floor();
let num_years_400 = ((y - 1601.0) / 400.0).floor();
365.0 * num_years_1 + num_years_4 - num_years_100 + num_years_400
}
fn time_from_year(y: f64) -> f64 {
MS_PER_DAY * day_from_year(y)
}
pub(super) fn year_from_time(t: f64) -> i32 {
const MS_PER_AVERAGE_YEAR: f64 = 12.0 * 30.436_875 * MS_PER_DAY;
let mut year = (((t + MS_PER_AVERAGE_YEAR / 2.0) / MS_PER_AVERAGE_YEAR).floor()) as i32 + 1970;
if time_from_year(year.into()) > t {
year -= 1;
}
year
}
fn day_within_year(t: f64) -> u16 {
(day(t) - day_from_year(year_from_time(t).into())) as u16
}
fn in_leap_year(t: f64) -> u16 {
(days_in_year(year_from_time(t).into()) == 366).into()
}
pub(super) fn month_from_time(t: f64) -> u8 {
let in_leap_year = in_leap_year(t);
let day_within_year = day_within_year(t);
match day_within_year {
t if t < 31 => 0,
t if t < 59 + in_leap_year => 1,
t if t < 90 + in_leap_year => 2,
t if t < 120 + in_leap_year => 3,
t if t < 151 + in_leap_year => 4,
t if t < 181 + in_leap_year => 5,
t if t < 212 + in_leap_year => 6,
t if t < 243 + in_leap_year => 7,
t if t < 273 + in_leap_year => 8,
t if t < 304 + in_leap_year => 9,
t if t < 334 + in_leap_year => 10,
_ => 11,
}
}
pub(super) fn date_from_time(t: f64) -> u8 {
let in_leap_year = in_leap_year(t);
let day_within_year = day_within_year(t);
let month = month_from_time(t);
let date = match month {
0 => day_within_year + 1,
1 => day_within_year - 30,
2 => day_within_year - 58 - in_leap_year,
3 => day_within_year - 89 - in_leap_year,
4 => day_within_year - 119 - in_leap_year,
5 => day_within_year - 150 - in_leap_year,
6 => day_within_year - 180 - in_leap_year,
7 => day_within_year - 211 - in_leap_year,
8 => day_within_year - 242 - in_leap_year,
9 => day_within_year - 272 - in_leap_year,
10 => day_within_year - 303 - in_leap_year,
_ => day_within_year - 333 - in_leap_year,
};
date as u8
}
pub(super) fn week_day(t: f64) -> u8 {
(day(t) + 4.0).rem_euclid(7.0) as u8
}
pub(super) fn hour_from_time(t: f64) -> u8 {
((t / MS_PER_HOUR).floor()).rem_euclid(HOURS_PER_DAY) as u8
}
pub(super) fn min_from_time(t: f64) -> u8 {
((t / MS_PER_MINUTE).floor()).rem_euclid(MINUTES_PER_HOUR) as u8
}
pub(super) fn sec_from_time(t: f64) -> u8 {
((t / MS_PER_SECOND).floor()).rem_euclid(SECONDS_PER_MINUTE) as u8
}
pub(super) fn ms_from_time(t: f64) -> u16 {
t.rem_euclid(MS_PER_SECOND) as u16
}
pub(super) fn local_time(t: f64, hooks: &dyn HostHooks) -> f64 {
t + f64::from(local_timezone_offset_seconds(t, hooks)) * MS_PER_SECOND
}
pub(super) fn utc_t(t: f64, hooks: &dyn HostHooks) -> f64 {
if !t.is_finite() {
return f64::NAN;
}
t - f64::from(local_timezone_offset_seconds(t, hooks)) * MS_PER_SECOND
}
pub(super) fn make_time(hour: f64, min: f64, sec: f64, ms: f64) -> f64 {
if !hour.is_finite() || !min.is_finite() || !sec.is_finite() || !ms.is_finite() {
return f64::NAN;
}
let h = hour.abs().floor().copysign(hour);
let m = min.abs().floor().copysign(min);
let s = sec.abs().floor().copysign(sec);
let milli = ms.abs().floor().copysign(ms);
((h * MS_PER_HOUR + m * MS_PER_MINUTE) + s * MS_PER_SECOND) + milli
}
pub(super) fn make_day(year: f64, month: f64, date: f64) -> f64 {
if !year.is_finite() || !month.is_finite() || !date.is_finite() {
return f64::NAN;
}
let y = year.abs().floor().copysign(year);
let m = month.abs().floor().copysign(month);
let dt = date.abs().floor().copysign(date);
let ym = y + (m / 12.0).floor();
if !ym.is_finite() {
return f64::NAN;
}
let mn = m.rem_euclid(12.0) as u8;
let rest = if mn > 1 { 1.0 } else { 0.0 };
let days_within_year_to_end_of_month = match mn {
0 => 0.0,
1 => 31.0,
2 => 59.0,
3 => 90.0,
4 => 120.0,
5 => 151.0,
6 => 181.0,
7 => 212.0,
8 => 243.0,
9 => 273.0,
10 => 304.0,
11 => 334.0,
12 => 365.0,
_ => unreachable!(),
};
let t =
(day_from_year(ym + rest) - 365.0 * rest + days_within_year_to_end_of_month) * MS_PER_DAY;
day(t) + dt - 1.0
}
pub(super) fn make_date(day: f64, time: f64) -> f64 {
if !day.is_finite() || !time.is_finite() {
return f64::NAN;
}
let tv = day * MS_PER_DAY + time;
if !tv.is_finite() {
return f64::NAN;
}
tv
}
pub(super) fn make_full_year(year: f64) -> f64 {
if year.is_nan() {
return f64::NAN;
}
let truncated = IntegerOrInfinity::from(year);
match truncated {
IntegerOrInfinity::Integer(i) if (0..=99).contains(&i) => 1900.0 + i as f64,
IntegerOrInfinity::Integer(i) => i as f64,
IntegerOrInfinity::PositiveInfinity => f64::INFINITY,
IntegerOrInfinity::NegativeInfinity => f64::NEG_INFINITY,
}
}
pub(crate) fn time_clip(time: f64) -> f64 {
if !time.is_finite() {
return f64::NAN;
}
if time.abs() > 8.64e15 {
return f64::NAN;
}
let time = time.trunc();
if time.abs() == 0.0 {
return 0.0;
}
time
}
pub(super) fn time_string(tv: f64) -> JsString {
let mut binding = [0; 2];
let hour = pad_two(hour_from_time(tv), &mut binding);
let mut binding = [0; 2];
let minute = pad_two(min_from_time(tv), &mut binding);
let mut binding = [0; 2];
let second = pad_two(sec_from_time(tv), &mut binding);
js_string!(
hour,
js_str!(":"),
minute,
js_str!(":"),
second,
js_str!(" GMT")
)
}
pub(super) fn date_string(tv: f64) -> JsString {
let weekday = match week_day(tv) {
0 => js_str!("Sun"),
1 => js_str!("Mon"),
2 => js_str!("Tue"),
3 => js_str!("Wed"),
4 => js_str!("Thu"),
5 => js_str!("Fri"),
6 => js_str!("Sat"),
_ => unreachable!(),
};
let month = match month_from_time(tv) {
0 => js_str!("Jan"),
1 => js_str!("Feb"),
2 => js_str!("Mar"),
3 => js_str!("Apr"),
4 => js_str!("May"),
5 => js_str!("Jun"),
6 => js_str!("Jul"),
7 => js_str!("Aug"),
8 => js_str!("Sep"),
9 => js_str!("Oct"),
10 => js_str!("Nov"),
11 => js_str!("Dec"),
_ => unreachable!(),
};
let mut binding = [0; 2];
let day = pad_two(date_from_time(tv), &mut binding);
let yv = year_from_time(tv);
let year_sign = if yv >= 0 { js_str!("") } else { js_str!("-") };
let yv = yv.unsigned_abs();
let padded_year: JsString = if yv >= 100_000 {
pad_six(yv, &mut [0; 6]).into()
} else if yv >= 10000 {
pad_five(yv, &mut [0; 5]).into()
} else {
pad_four(yv, &mut [0; 4]).into()
};
js_string!(
weekday,
js_str!(" "),
month,
js_str!(" "),
day,
js_str!(" "),
year_sign,
&padded_year
)
}
pub(super) fn time_zone_string(t: f64, hooks: &dyn HostHooks) -> JsString {
let offset = f64::from(local_timezone_offset_seconds(t, hooks)) * MS_PER_SECOND;
let (offset_sign, abs_offset) = if offset >= 0.0 {
(js_str!("+"), offset)
}
else {
(js_str!("-"), -offset)
};
let mut binding = [0; 2];
let offset_min = pad_two(min_from_time(abs_offset), &mut binding);
let mut binding = [0; 2];
let offset_hour = pad_two(hour_from_time(abs_offset), &mut binding);
js_string!(offset_sign, offset_hour, offset_min)
}
pub(super) fn to_date_string_t(tv: f64, hooks: &dyn HostHooks) -> JsString {
if tv.is_nan() {
return js_string!("Invalid Date");
}
let t = local_time(tv, hooks);
js_string!(
&date_string(t),
js_str!(" "),
&time_string(t),
&time_zone_string(t, hooks)
)
}
fn local_timezone_offset_seconds(t: f64, hooks: &dyn HostHooks) -> i32 {
let millis = t.rem_euclid(MS_PER_SECOND);
let seconds = ((t - millis) / MS_PER_SECOND) as i64;
hooks.local_timezone_offset_seconds(seconds)
}
pub(super) fn pad_two(t: u8, output: &mut [u8; 2]) -> JsStr<'_> {
*output = if t < 10 {
[b'0', b'0' + t]
} else {
[b'0' + (t / 10), b'0' + (t % 10)]
};
debug_assert!(output.is_ascii());
JsStr::latin1(output)
}
pub(super) fn pad_three(t: u16, output: &mut [u8; 3]) -> JsStr<'_> {
*output = [
b'0' + (t / 100) as u8,
b'0' + ((t / 10) % 10) as u8,
b'0' + (t % 10) as u8,
];
JsStr::latin1(output)
}
pub(super) fn pad_four(t: u32, output: &mut [u8; 4]) -> JsStr<'_> {
*output = [
b'0' + (t / 1000) as u8,
b'0' + ((t / 100) % 10) as u8,
b'0' + ((t / 10) % 10) as u8,
b'0' + (t % 10) as u8,
];
JsStr::latin1(output)
}
pub(super) fn pad_five(t: u32, output: &mut [u8; 5]) -> JsStr<'_> {
*output = [
b'0' + (t / 10_000) as u8,
b'0' + ((t / 1000) % 10) as u8,
b'0' + ((t / 100) % 10) as u8,
b'0' + ((t / 10) % 10) as u8,
b'0' + (t % 10) as u8,
];
JsStr::latin1(output)
}
pub(super) fn pad_six(t: u32, output: &mut [u8; 6]) -> JsStr<'_> {
*output = [
b'0' + (t / 100_000) as u8,
b'0' + ((t / 10_000) % 10) as u8,
b'0' + ((t / 1000) % 10) as u8,
b'0' + ((t / 100) % 10) as u8,
b'0' + ((t / 10) % 10) as u8,
b'0' + (t % 10) as u8,
];
JsStr::latin1(output)
}
pub(super) fn parse_date(date: &JsString, hooks: &dyn HostHooks) -> Option<i64> {
let Ok(date) = date.to_std_string() else {
return None;
};
if let Some(dt) = DateParser::new(&date, hooks).parse() {
return Some(dt);
}
if let Ok(t) = OffsetDateTime::parse(&date, &format_description!("[weekday repr:short] [month repr:short] [day] [year] [hour]:[minute]:[second] GMT[offset_hour sign:mandatory][offset_minute][end]")) {
return Some(t.unix_timestamp() * 1000 + i64::from(t.millisecond()));
}
if let Ok(t) = PrimitiveDateTime::parse(&date, &format_description!("[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT[end]")) {
let t = t.assume_utc();
return Some(t.unix_timestamp() * 1000 + i64::from(t.millisecond()));
}
None
}
struct DateParser<'a> {
hooks: &'a dyn HostHooks,
input: Peekable<Chars<'a>>,
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
millisecond: u32,
offset: i64,
}
impl<'a> DateParser<'a> {
fn new(s: &'a str, hooks: &'a dyn HostHooks) -> Self {
Self {
hooks,
input: s.chars().peekable(),
year: 0,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
offset: 0,
}
}
fn next_expect(&mut self, expect: char) -> Option<()> {
self.input
.next()
.and_then(|c| if c == expect { Some(()) } else { None })
}
fn next_digit(&mut self) -> Option<u8> {
self.input.next().and_then(|c| {
if c.is_ascii_digit() {
Some((u32::from(c) - u32::from('0')) as u8)
} else {
None
}
})
}
fn finish(&mut self) -> Option<i64> {
if self.input.peek().is_some() {
return None;
}
let date = make_date(
make_day(self.year.into(), (self.month - 1).into(), self.day.into()),
make_time(
self.hour.into(),
self.minute.into(),
self.second.into(),
self.millisecond.into(),
),
);
let date = date + (self.offset as f64) * MS_PER_MINUTE;
let t = time_clip(date);
if t.is_finite() {
Some(t as i64)
} else {
None
}
}
fn finish_local(&mut self) -> Option<i64> {
if self.input.peek().is_some() {
return None;
}
let date = make_date(
make_day(self.year.into(), (self.month - 1).into(), self.day.into()),
make_time(
self.hour.into(),
self.minute.into(),
self.second.into(),
self.millisecond.into(),
),
);
let t = time_clip(utc_t(date, self.hooks));
if t.is_finite() {
Some(t as i64)
} else {
None
}
}
fn parse(&mut self) -> Option<i64> {
self.parse_year()?;
match self.input.peek() {
Some('T') => return self.parse_time(),
None => return self.finish(),
_ => {}
}
self.next_expect('-')?;
self.month = u32::from(self.next_digit()?) * 10 + u32::from(self.next_digit()?);
if self.month < 1 || self.month > 12 {
return None;
}
match self.input.peek() {
Some('T') => return self.parse_time(),
None => return self.finish(),
_ => {}
}
self.next_expect('-')?;
self.day = u32::from(self.next_digit()?) * 10 + u32::from(self.next_digit()?);
if self.day < 1 || self.day > 31 {
return None;
}
match self.input.peek() {
Some('T') => self.parse_time(),
_ => self.finish(),
}
}
fn parse_year(&mut self) -> Option<()> {
match self.input.next()? {
'+' => {
self.year = i32::from(self.next_digit()?) * 100_000
+ i32::from(self.next_digit()?) * 10000
+ i32::from(self.next_digit()?) * 1000
+ i32::from(self.next_digit()?) * 100
+ i32::from(self.next_digit()?) * 10
+ i32::from(self.next_digit()?);
Some(())
}
'-' => {
let year = i32::from(self.next_digit()?) * 100_000
+ i32::from(self.next_digit()?) * 10000
+ i32::from(self.next_digit()?) * 1000
+ i32::from(self.next_digit()?) * 100
+ i32::from(self.next_digit()?) * 10
+ i32::from(self.next_digit()?);
if year == 0 {
return None;
}
self.year = -year;
Some(())
}
c if c.is_ascii_digit() => {
self.year = i32::from((u32::from(c) - u32::from('0')) as u8) * 1000
+ i32::from(self.next_digit()?) * 100
+ i32::from(self.next_digit()?) * 10
+ i32::from(self.next_digit()?);
Some(())
}
_ => None,
}
}
fn parse_time(&mut self) -> Option<i64> {
self.next_expect('T')?;
self.hour = u32::from(self.next_digit()?) * 10 + u32::from(self.next_digit()?);
if self.hour > 24 {
return None;
}
self.next_expect(':')?;
self.minute = u32::from(self.next_digit()?) * 10 + u32::from(self.next_digit()?);
if self.minute > 59 {
return None;
}
match self.input.peek() {
Some(':') => {}
None => return self.finish_local(),
_ => {
self.parse_timezone()?;
return self.finish();
}
}
self.next_expect(':')?;
self.second = u32::from(self.next_digit()?) * 10 + u32::from(self.next_digit()?);
if self.second > 59 {
return None;
}
match self.input.peek() {
Some('.') => {}
None => return self.finish_local(),
_ => {
self.parse_timezone()?;
return self.finish();
}
}
self.next_expect('.')?;
self.millisecond = u32::from(self.next_digit()?) * 100
+ u32::from(self.next_digit()?) * 10
+ u32::from(self.next_digit()?);
if self.input.peek().is_some() {
self.parse_timezone()?;
self.finish()
} else {
self.finish_local()
}
}
fn parse_timezone(&mut self) -> Option<()> {
match self.input.next() {
Some('Z') => return Some(()),
Some('+') => {
let offset_hour =
i64::from(self.next_digit()?) * 10 + i64::from(self.next_digit()?);
if offset_hour > 23 {
return None;
}
self.offset = -offset_hour * 60;
if self.input.peek().is_none() {
return Some(());
}
self.next_expect(':')?;
let offset_minute =
i64::from(self.next_digit()?) * 10 + i64::from(self.next_digit()?);
if offset_minute > 59 {
return None;
}
self.offset += -offset_minute;
}
Some('-') => {
let offset_hour =
i64::from(self.next_digit()?) * 10 + i64::from(self.next_digit()?);
if offset_hour > 23 {
return None;
}
self.offset = offset_hour * 60;
if self.input.peek().is_none() {
return Some(());
}
self.next_expect(':')?;
let offset_minute =
i64::from(self.next_digit()?) * 10 + i64::from(self.next_digit()?);
if offset_minute > 59 {
return None;
}
self.offset += offset_minute;
}
_ => return None,
}
Some(())
}
}