use crate::{JsStr, JsString, context::HostHooks, js_string, value::IntegerOrInfinity};
use boa_macros::js_str;
use boa_string::JsStrVariant;
use std::slice::Iter;
use std::str;
use std::{borrow::Cow, iter::Peekable};
use time::{OffsetDateTime, PrimitiveDateTime, macros::format_description};
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 owned_js_str = date.as_str();
let date = match owned_js_str.variant() {
JsStrVariant::Latin1(s) => {
if !s.is_ascii() {
return None;
}
Cow::Borrowed(unsafe { str::from_utf8_unchecked(s) })
}
JsStrVariant::Utf16(s) => {
let date = String::from_utf16(s).ok()?;
if !date.is_ascii() {
return None;
}
Cow::Owned(date)
}
};
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<Iter<'a, u8>>,
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
millisecond: u32,
offset: i64,
}
#[doc(hidden)]
#[allow(clippy::inline_always)]
pub(in crate::builtins::date) mod fast_atoi {
#[inline(always)]
pub(in crate::builtins::date) const fn process_8(mut val: u64, len: usize) -> u64 {
val <<= 64_usize.saturating_sub(len << 3); val = (val & 0x0F0F_0F0F_0F0F_0F0F).wrapping_mul(0xA01) >> 8;
val = (val & 0x00FF_00FF_00FF_00FF).wrapping_mul(0x64_0001) >> 16;
(val & 0x0000_FFFF_0000_FFFF).wrapping_mul(0x2710_0000_0001) >> 32
}
#[inline(always)]
pub(in crate::builtins::date) const fn process_4(mut val: u32, len: usize) -> u32 {
val <<= 32_usize.saturating_sub(len << 3); val = (val & 0x0F0F_0F0F).wrapping_mul(0xA01) >> 8;
(val & 0x00FF_00FF).wrapping_mul(0x64_0001) >> 16
}
}
impl<'a> DateParser<'a> {
fn new(s: &'a str, hooks: &'a dyn HostHooks) -> Self {
Self {
hooks,
input: s.as_bytes().iter().peekable(),
year: 0,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
offset: 0,
}
}
fn next_expect(&mut self, expect: u8) -> Option<()> {
self.input
.next()
.and_then(|c| if *c == expect { Some(()) } else { None })
}
fn next_ascii_digit(&mut self) -> Option<u8> {
self.input
.next()
.and_then(|c| if c.is_ascii_digit() { Some(*c) } else { None })
}
fn next_n_ascii_digits<const N: usize>(&mut self) -> Option<[u8; N]> {
let mut digits = [0; N];
for digit in &mut digits {
*digit = self.next_ascii_digit()?;
}
Some(digits)
}
fn parse_n_ascii_digits<const N: usize>(&mut self) -> Option<u64> {
assert!(N <= 8, "parse_n_ascii_digits parses no more than 8 digits");
if N == 0 {
return None;
}
let ascii_digits = self.next_n_ascii_digits::<N>()?;
match N {
1..4 => {
let mut res = 0;
for digit in ascii_digits {
res = res * 10 + u64::from(digit & 0xF);
}
Some(res)
}
4 => {
let mut src = [0; 4];
src[..N].copy_from_slice(&ascii_digits);
let val = u32::from_le_bytes(src);
Some(u64::from(fast_atoi::process_4(val, N)))
}
_ => {
let mut src = [0; 8];
src[..N].copy_from_slice(&ascii_digits);
let val = u64::from_le_bytes(src);
Some(fast_atoi::process_8(val, N))
}
}
}
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 }
}
#[allow(clippy::as_conversions)]
fn parse(&mut self) -> Option<i64> {
self.parse_year()?;
match self.input.peek() {
Some(b'T') => return self.parse_time(),
None => return self.finish(),
_ => {}
}
self.next_expect(b'-')?;
self.month = self.parse_n_ascii_digits::<2>()? as u32;
if self.month < 1 || self.month > 12 {
return None;
}
match self.input.peek() {
Some(b'T') => return self.parse_time(),
None => return self.finish(),
_ => {}
}
self.next_expect(b'-')?;
self.day = self.parse_n_ascii_digits::<2>()? as u32;
if self.day < 1 || self.day > 31 {
return None;
}
match self.input.peek() {
Some(b'T') => self.parse_time(),
_ => self.finish(),
}
}
#[allow(clippy::as_conversions)]
fn parse_year(&mut self) -> Option<()> {
if let &&sign @ (b'+' | b'-') = self.input.peek()? {
self.input.next();
let year = self.parse_n_ascii_digits::<6>()? as i32;
let neg = sign == b'-';
if neg && year == 0 {
return None;
}
self.year = if neg { -year } else { year };
} else {
self.year = self.parse_n_ascii_digits::<4>()? as i32;
}
Some(())
}
#[allow(clippy::as_conversions)]
fn parse_time(&mut self) -> Option<i64> {
self.next_expect(b'T')?;
self.hour = self.parse_n_ascii_digits::<2>()? as u32;
if self.hour > 24 {
return None;
}
self.next_expect(b':')?;
self.minute = self.parse_n_ascii_digits::<2>()? as u32;
if self.minute > 59 {
return None;
}
match self.input.peek() {
Some(b':') => self.input.next(),
None => return self.finish_local(),
_ => {
self.parse_timezone()?;
return self.finish();
}
};
self.second = self.parse_n_ascii_digits::<2>()? as u32;
if self.second > 59 {
return None;
}
match self.input.peek() {
Some(b'.') => self.input.next(),
None => return self.finish_local(),
_ => {
self.parse_timezone()?;
return self.finish();
}
};
self.millisecond = self.parse_n_ascii_digits::<3>()? as u32;
if self.input.peek().is_some() {
self.parse_timezone()?;
self.finish()
} else {
self.finish_local()
}
}
#[allow(clippy::as_conversions)]
fn parse_timezone(&mut self) -> Option<()> {
match self.input.next() {
Some(b'Z') => return Some(()),
Some(sign @ (b'+' | b'-')) => {
let neg = *sign == b'-';
let offset_hour = self.parse_n_ascii_digits::<2>()? as i64;
if offset_hour > 23 {
return None;
}
self.offset = if neg { offset_hour } else { -offset_hour } * 60;
if self.input.peek().is_none() {
return Some(());
}
self.next_expect(b':')?;
let offset_minute = self.parse_n_ascii_digits::<2>()? as i64;
if offset_minute > 59 {
return None;
}
self.offset += if neg { offset_minute } else { -offset_minute };
}
_ => return None,
}
Some(())
}
}