mirror of https://github.com/boa-dev/boa.git
Browse Source
* Build out ZonedDateTime, TimeZone, and Instant * Post rebase fix on zdtpull/3507/head
Kevin
12 months ago
committed by
GitHub
9 changed files with 747 additions and 93 deletions
@ -0,0 +1,94 @@
|
||||
//! An implementation of the Temporal Instant.
|
||||
|
||||
use crate::{TemporalError, TemporalResult}; |
||||
|
||||
use num_bigint::BigInt; |
||||
use num_traits::ToPrimitive; |
||||
|
||||
/// A Temporal Instant
|
||||
#[derive(Debug, Clone)] |
||||
pub struct Instant { |
||||
pub(crate) nanos: BigInt, |
||||
} |
||||
|
||||
// ==== Public API ====
|
||||
|
||||
impl Instant { |
||||
/// Create a new validated `Instant`.
|
||||
#[inline] |
||||
pub fn new(nanos: BigInt) -> TemporalResult<Self> { |
||||
if !is_valid_epoch_nanos(&nanos) { |
||||
return Err(TemporalError::range() |
||||
.with_message("Instant nanoseconds are not within a valid epoch range.")); |
||||
} |
||||
Ok(Self { nanos }) |
||||
} |
||||
|
||||
/// Returns the `epochSeconds` value for this `Instant`.
|
||||
#[must_use] |
||||
pub fn epoch_seconds(&self) -> f64 { |
||||
(&self.nanos / BigInt::from(1_000_000_000)) |
||||
.to_f64() |
||||
.expect("A validated Instant should be within a valid f64") |
||||
.floor() |
||||
} |
||||
|
||||
/// Returns the `epochMilliseconds` value for this `Instant`.
|
||||
#[must_use] |
||||
pub fn epoch_milliseconds(&self) -> f64 { |
||||
(&self.nanos / BigInt::from(1_000_000)) |
||||
.to_f64() |
||||
.expect("A validated Instant should be within a valid f64") |
||||
.floor() |
||||
} |
||||
|
||||
/// Returns the `epochMicroseconds` value for this `Instant`.
|
||||
#[must_use] |
||||
pub fn epoch_microseconds(&self) -> f64 { |
||||
(&self.nanos / BigInt::from(1_000)) |
||||
.to_f64() |
||||
.expect("A validated Instant should be within a valid f64") |
||||
.floor() |
||||
} |
||||
|
||||
/// Returns the `epochNanoseconds` value for this `Instant`.
|
||||
#[must_use] |
||||
pub fn epoch_nanoseconds(&self) -> f64 { |
||||
self.nanos |
||||
.to_f64() |
||||
.expect("A validated Instant should be within a valid f64") |
||||
} |
||||
} |
||||
|
||||
/// Utility for determining if the nanos are within a valid range.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub(crate) fn is_valid_epoch_nanos(nanos: &BigInt) -> bool { |
||||
nanos <= &BigInt::from(crate::NS_MAX_INSTANT) && nanos >= &BigInt::from(crate::NS_MIN_INSTANT) |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use crate::{instant::Instant, NS_MAX_INSTANT, NS_MIN_INSTANT}; |
||||
use num_bigint::BigInt; |
||||
use num_traits::ToPrimitive; |
||||
|
||||
#[test] |
||||
#[allow(clippy::float_cmp)] |
||||
fn max_and_minimum_instant_bounds() { |
||||
// This test is primarily to assert that the `expect` in the epoch methods is valid.
|
||||
let max = BigInt::from(NS_MAX_INSTANT); |
||||
let min = BigInt::from(NS_MIN_INSTANT); |
||||
let max_instant = Instant::new(max.clone()).unwrap(); |
||||
let min_instant = Instant::new(min.clone()).unwrap(); |
||||
|
||||
assert_eq!(max_instant.epoch_nanoseconds(), max.to_f64().unwrap()); |
||||
assert_eq!(min_instant.epoch_nanoseconds(), min.to_f64().unwrap()); |
||||
|
||||
let max_plus_one = BigInt::from(NS_MAX_INSTANT + 1); |
||||
let min_minus_one = BigInt::from(NS_MIN_INSTANT - 1); |
||||
|
||||
assert!(Instant::new(max_plus_one).is_err()); |
||||
assert!(Instant::new(min_minus_one).is_err()); |
||||
} |
||||
} |
@ -0,0 +1,116 @@
|
||||
//! This module implements the Temporal `TimeZone` and components.
|
||||
|
||||
use std::any::Any; |
||||
|
||||
use num_bigint::BigInt; |
||||
use num_traits::ToPrimitive; |
||||
|
||||
use crate::{ |
||||
calendar::CalendarSlot, datetime::DateTime, instant::Instant, TemporalError, TemporalResult, |
||||
}; |
||||
|
||||
/// Any object that implements the `TzProtocol` must implement the below methods/properties.
|
||||
pub const TIME_ZONE_PROPERTIES: [&str; 3] = |
||||
["getOffsetNanosecondsFor", "getPossibleInstantsFor", "id"]; |
||||
|
||||
/// A clonable `TzProtocol`
|
||||
pub trait TzProtocolClone { |
||||
/// Clones the current `TimeZoneProtocol`.
|
||||
fn clone_box(&self) -> Box<dyn TzProtocol>; |
||||
} |
||||
|
||||
impl<P> TzProtocolClone for P |
||||
where |
||||
P: 'static + TzProtocol + Clone, |
||||
{ |
||||
fn clone_box(&self) -> Box<dyn TzProtocol> { |
||||
Box::new(self.clone()) |
||||
} |
||||
} |
||||
|
||||
/// The Time Zone Protocol that must be implemented for time zones.
|
||||
pub trait TzProtocol: TzProtocolClone { |
||||
/// Get the Offset nanoseconds for this `TimeZone`
|
||||
fn get_offset_nanos_for(&self, context: &mut dyn Any) -> TemporalResult<BigInt>; |
||||
/// Get the possible Instant for this `TimeZone`
|
||||
fn get_possible_instant_for(&self, context: &mut dyn Any) -> TemporalResult<Vec<Instant>>; // TODO: Implement Instant
|
||||
/// Get the `TimeZone`'s identifier.
|
||||
fn id(&self, context: &mut dyn Any) -> String; |
||||
} |
||||
|
||||
/// A Temporal `TimeZone`.
|
||||
#[derive(Debug, Clone)] |
||||
#[allow(unused)] |
||||
pub struct TimeZone { |
||||
pub(crate) iana: Option<String>, // TODO: ICU4X IANA TimeZone support.
|
||||
pub(crate) offset: Option<i16>, |
||||
} |
||||
|
||||
/// The `TimeZoneSlot` represents a `[[TimeZone]]` internal slot value.
|
||||
pub enum TimeZoneSlot { |
||||
/// A native `TimeZone` representation.
|
||||
Tz(TimeZone), |
||||
/// A Custom `TimeZone` that implements the `TzProtocol`.
|
||||
Protocol(Box<dyn TzProtocol>), |
||||
} |
||||
|
||||
impl core::fmt::Debug for TimeZoneSlot { |
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
match self { |
||||
Self::Tz(tz) => write!(f, "{tz:?}"), |
||||
Self::Protocol(_) => write!(f, "TzProtocol"), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Clone for TimeZoneSlot { |
||||
fn clone(&self) -> Self { |
||||
match self { |
||||
Self::Tz(tz) => Self::Tz(tz.clone()), |
||||
Self::Protocol(p) => Self::Protocol(p.clone_box()), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl TimeZoneSlot { |
||||
pub(crate) fn get_datetime_for( |
||||
&self, |
||||
instant: &Instant, |
||||
calendar: &CalendarSlot, |
||||
context: &mut dyn Any, |
||||
) -> TemporalResult<DateTime> { |
||||
let nanos = self.get_offset_nanos_for(context)?; |
||||
DateTime::from_instant(instant, nanos.to_f64().unwrap_or(0.0), calendar.clone()) |
||||
} |
||||
} |
||||
|
||||
impl TzProtocol for TimeZoneSlot { |
||||
fn get_offset_nanos_for(&self, context: &mut dyn Any) -> TemporalResult<BigInt> { |
||||
// 1. Let timeZone be the this value.
|
||||
// 2. Perform ? RequireInternalSlot(timeZone, [[InitializedTemporalTimeZone]]).
|
||||
// 3. Set instant to ? ToTemporalInstant(instant).
|
||||
match self { |
||||
Self::Tz(tz) => { |
||||
// 4. If timeZone.[[OffsetMinutes]] is not empty, return 𝔽(timeZone.[[OffsetMinutes]] × (60 × 10^9)).
|
||||
if let Some(offset) = &tz.offset { |
||||
return Ok(BigInt::from(i64::from(*offset) * 60_000_000_000i64)); |
||||
} |
||||
// 5. Return 𝔽(GetNamedTimeZoneOffsetNanoseconds(timeZone.[[Identifier]], instant.[[Nanoseconds]])).
|
||||
Err(TemporalError::range().with_message("IANA TimeZone names not yet implemented.")) |
||||
} |
||||
// Call any custom implemented TimeZone.
|
||||
Self::Protocol(p) => p.get_offset_nanos_for(context), |
||||
} |
||||
} |
||||
|
||||
fn get_possible_instant_for(&self, _context: &mut dyn Any) -> TemporalResult<Vec<Instant>> { |
||||
Err(TemporalError::general("Not yet implemented.")) |
||||
} |
||||
|
||||
fn id(&self, context: &mut dyn Any) -> String { |
||||
match self { |
||||
Self::Tz(_) => todo!("implement tz.to_string"), |
||||
Self::Protocol(tz) => tz.id(context), |
||||
} |
||||
} |
||||
} |
@ -1,7 +1,267 @@
|
||||
//! The `ZonedDateTime` module.
|
||||
|
||||
// NOTE: Mostly serves as a placeholder currently
|
||||
// until the rest can be implemented.
|
||||
/// `TemporalZoneDateTime`
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct ZonedDateTime; |
||||
use num_bigint::BigInt; |
||||
use tinystr::TinyStr4; |
||||
|
||||
use crate::{calendar::CalendarSlot, instant::Instant, tz::TimeZoneSlot, TemporalResult}; |
||||
|
||||
use core::any::Any; |
||||
|
||||
/// Temporal's `ZonedDateTime` object.
|
||||
#[derive(Debug, Clone)] |
||||
pub struct ZonedDateTime { |
||||
instant: Instant, |
||||
calendar: CalendarSlot, |
||||
tz: TimeZoneSlot, |
||||
} |
||||
|
||||
// ==== Private API ====
|
||||
|
||||
impl ZonedDateTime { |
||||
/// Creates a `ZonedDateTime` without validating the input.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub(crate) fn new_unchecked( |
||||
instant: Instant, |
||||
calendar: CalendarSlot, |
||||
tz: TimeZoneSlot, |
||||
) -> Self { |
||||
Self { |
||||
instant, |
||||
calendar, |
||||
tz, |
||||
} |
||||
} |
||||
} |
||||
|
||||
// ==== Public API ====
|
||||
|
||||
impl ZonedDateTime { |
||||
/// Creates a new valid `ZonedDateTime`.
|
||||
#[inline] |
||||
pub fn new(nanos: BigInt, calendar: CalendarSlot, tz: TimeZoneSlot) -> TemporalResult<Self> { |
||||
let instant = Instant::new(nanos)?; |
||||
Ok(Self::new_unchecked(instant, calendar, tz)) |
||||
} |
||||
|
||||
/// Returns the `ZonedDateTime`'s Calendar identifier.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn calendar_id(&self) -> String { |
||||
// TODO: Implement Identifier method on `CalendarSlot`
|
||||
String::from("Not yet implemented.") |
||||
} |
||||
|
||||
/// Returns the `epochSeconds` value of this `ZonedDateTime`.
|
||||
#[must_use] |
||||
pub fn epoch_seconds(&self) -> f64 { |
||||
self.instant.epoch_seconds() |
||||
} |
||||
|
||||
/// Returns the `epochMilliseconds` value of this `ZonedDateTime`.
|
||||
#[must_use] |
||||
pub fn epoch_milliseconds(&self) -> f64 { |
||||
self.instant.epoch_milliseconds() |
||||
} |
||||
|
||||
/// Returns the `epochMicroseconds` value of this `ZonedDateTime`.
|
||||
#[must_use] |
||||
pub fn epoch_microseconds(&self) -> f64 { |
||||
self.instant.epoch_microseconds() |
||||
} |
||||
|
||||
/// Returns the `epochNanoseconds` value of this `ZonedDateTime`.
|
||||
#[must_use] |
||||
pub fn epoch_nanoseconds(&self) -> f64 { |
||||
self.instant.epoch_nanoseconds() |
||||
} |
||||
} |
||||
|
||||
// ==== Context based API ====
|
||||
|
||||
impl ZonedDateTime { |
||||
/// Returns the `year` value for this `ZonedDateTime`.
|
||||
#[inline] |
||||
pub fn contextual_year(&self, context: &mut dyn Any) -> TemporalResult<i32> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
self.calendar |
||||
.year(&crate::calendar::CalendarDateLike::DateTime(dt), context) |
||||
} |
||||
|
||||
/// Returns the `year` value for this `ZonedDateTime`.
|
||||
#[inline] |
||||
pub fn year(&self) -> TemporalResult<i32> { |
||||
self.contextual_year(&mut ()) |
||||
} |
||||
|
||||
/// Returns the `month` value for this `ZonedDateTime`.
|
||||
pub fn contextual_month(&self, context: &mut dyn Any) -> TemporalResult<u8> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
self.calendar |
||||
.month(&crate::calendar::CalendarDateLike::DateTime(dt), context) |
||||
} |
||||
|
||||
/// Returns the `month` value for this `ZonedDateTime`.
|
||||
#[inline] |
||||
pub fn month(&self) -> TemporalResult<u8> { |
||||
self.contextual_month(&mut ()) |
||||
} |
||||
|
||||
/// Returns the `monthCode` value for this `ZonedDateTime`.
|
||||
pub fn contextual_month_code(&self, context: &mut dyn Any) -> TemporalResult<TinyStr4> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
self.calendar |
||||
.month_code(&crate::calendar::CalendarDateLike::DateTime(dt), context) |
||||
} |
||||
|
||||
/// Returns the `monthCode` value for this `ZonedDateTime`.
|
||||
#[inline] |
||||
pub fn month_code(&self) -> TemporalResult<TinyStr4> { |
||||
self.contextual_month_code(&mut ()) |
||||
} |
||||
|
||||
/// Returns the `day` value for this `ZonedDateTime`.
|
||||
pub fn contextual_day(&self, context: &mut dyn Any) -> TemporalResult<u8> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
self.calendar |
||||
.day(&crate::calendar::CalendarDateLike::DateTime(dt), context) |
||||
} |
||||
|
||||
/// Returns the `day` value for this `ZonedDateTime`.
|
||||
pub fn day(&self) -> TemporalResult<u8> { |
||||
self.contextual_day(&mut ()) |
||||
} |
||||
|
||||
/// Returns the `hour` value for this `ZonedDateTime`.
|
||||
pub fn contextual_hour(&self, context: &mut dyn Any) -> TemporalResult<u8> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
Ok(dt.hours()) |
||||
} |
||||
|
||||
/// Returns the `hour` value for this `ZonedDateTime`.
|
||||
pub fn hour(&self) -> TemporalResult<u8> { |
||||
self.contextual_hour(&mut ()) |
||||
} |
||||
|
||||
/// Returns the `minute` value for this `ZonedDateTime`.
|
||||
pub fn contextual_minute(&self, context: &mut dyn Any) -> TemporalResult<u8> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
Ok(dt.minutes()) |
||||
} |
||||
|
||||
/// Returns the `minute` value for this `ZonedDateTime`.
|
||||
pub fn minute(&self) -> TemporalResult<u8> { |
||||
self.contextual_minute(&mut ()) |
||||
} |
||||
|
||||
/// Returns the `second` value for this `ZonedDateTime`.
|
||||
pub fn contextual_second(&self, context: &mut dyn Any) -> TemporalResult<u8> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
Ok(dt.seconds()) |
||||
} |
||||
|
||||
/// Returns the `second` value for this `ZonedDateTime`.
|
||||
pub fn second(&self) -> TemporalResult<u8> { |
||||
self.contextual_second(&mut ()) |
||||
} |
||||
|
||||
/// Returns the `millisecond` value for this `ZonedDateTime`.
|
||||
pub fn contextual_millisecond(&self, context: &mut dyn Any) -> TemporalResult<u16> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
Ok(dt.milliseconds()) |
||||
} |
||||
|
||||
/// Returns the `millisecond` value for this `ZonedDateTime`.
|
||||
pub fn millisecond(&self) -> TemporalResult<u16> { |
||||
self.contextual_millisecond(&mut ()) |
||||
} |
||||
|
||||
/// Returns the `microsecond` value for this `ZonedDateTime`.
|
||||
pub fn contextual_microsecond(&self, context: &mut dyn Any) -> TemporalResult<u16> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
Ok(dt.milliseconds()) |
||||
} |
||||
|
||||
/// Returns the `microsecond` value for this `ZonedDateTime`.
|
||||
pub fn microsecond(&self) -> TemporalResult<u16> { |
||||
self.contextual_microsecond(&mut ()) |
||||
} |
||||
|
||||
/// Returns the `nanosecond` value for this `ZonedDateTime`.
|
||||
pub fn contextual_nanosecond(&self, context: &mut dyn Any) -> TemporalResult<u16> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
Ok(dt.nanoseconds()) |
||||
} |
||||
|
||||
/// Returns the `nanosecond` value for this `ZonedDateTime`.
|
||||
pub fn nanosecond(&self) -> TemporalResult<u16> { |
||||
self.contextual_nanosecond(&mut ()) |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use crate::tz::TimeZone; |
||||
use num_bigint::BigInt; |
||||
|
||||
use super::{CalendarSlot, TimeZoneSlot, ZonedDateTime}; |
||||
|
||||
#[test] |
||||
fn basic_zdt_test() { |
||||
let nov_30_2023_utc = BigInt::from(1_701_308_952_000_000_000i64); |
||||
|
||||
let zdt = ZonedDateTime::new( |
||||
nov_30_2023_utc.clone(), |
||||
CalendarSlot::Identifier("iso8601".to_owned()), |
||||
TimeZoneSlot::Tz(TimeZone { |
||||
iana: None, |
||||
offset: Some(0), |
||||
}), |
||||
) |
||||
.unwrap(); |
||||
|
||||
assert_eq!(zdt.year().unwrap(), 2023); |
||||
assert_eq!(zdt.month().unwrap(), 11); |
||||
assert_eq!(zdt.day().unwrap(), 30); |
||||
assert_eq!(zdt.hour().unwrap(), 1); |
||||
assert_eq!(zdt.minute().unwrap(), 49); |
||||
assert_eq!(zdt.second().unwrap(), 12); |
||||
|
||||
let zdt_minus_five = ZonedDateTime::new( |
||||
nov_30_2023_utc, |
||||
CalendarSlot::Identifier("iso8601".to_owned()), |
||||
TimeZoneSlot::Tz(TimeZone { |
||||
iana: None, |
||||
offset: Some(-300), |
||||
}), |
||||
) |
||||
.unwrap(); |
||||
|
||||
assert_eq!(zdt_minus_five.year().unwrap(), 2023); |
||||
assert_eq!(zdt_minus_five.month().unwrap(), 11); |
||||
assert_eq!(zdt_minus_five.day().unwrap(), 29); |
||||
assert_eq!(zdt_minus_five.hour().unwrap(), 20); |
||||
assert_eq!(zdt_minus_five.minute().unwrap(), 49); |
||||
assert_eq!(zdt_minus_five.second().unwrap(), 12); |
||||
} |
||||
} |
||||
|
Loading…
Reference in new issue