mirror of https://github.com/boa-dev/boa.git
Kevin
9 months ago
committed by
GitHub
50 changed files with 136 additions and 9650 deletions
@ -1,33 +0,0 @@
|
||||
# About Boa |
||||
|
||||
Boa is an open-source, experimental ECMAScript Engine written in Rust for |
||||
lexing, parsing and executing ECMAScript/JavaScript. Currently, Boa supports some |
||||
of the [language][boa-conformance]. More information can be viewed at [Boa's |
||||
website][boa-web]. |
||||
|
||||
Try out the most recent release with Boa's live demo |
||||
[playground][boa-playground]. |
||||
|
||||
# Boa Crates |
||||
|
||||
- [**`boa_ast`**][ast] - Boa's ECMAScript Abstract Syntax Tree. |
||||
- [**`boa_engine`**][engine] - Boa's implementation of ECMAScript builtin objects and |
||||
execution. |
||||
- [**`boa_gc`**][gc] - Boa's garbage collector. |
||||
- [**`boa_interner`**][interner] - Boa's string interner. |
||||
- [**`boa_parser`**][parser] - Boa's lexer and parser. |
||||
- [**`boa_profiler`**][profiler] - Boa's code profiler. |
||||
- [**`boa_icu_provider`**][icu] - Boa's ICU4X data provider. |
||||
- [**`boa_runtime`**][runtime] - Boa's WebAPI features. |
||||
|
||||
[boa-conformance]: https://boajs.dev/boa/test262/ |
||||
[boa-web]: https://boajs.dev/ |
||||
[boa-playground]: https://boajs.dev/boa/playground/ |
||||
[ast]: https://boajs.dev/boa/doc/boa_ast/index.html |
||||
[engine]: https://boajs.dev/boa/doc/boa_engine/index.html |
||||
[gc]: https://boajs.dev/boa/doc/boa_gc/index.html |
||||
[interner]: https://boajs.dev/boa/doc/boa_interner/index.html |
||||
[parser]: https://boajs.dev/boa/doc/boa_parser/index.html |
||||
[profiler]: https://boajs.dev/boa/doc/boa_profiler/index.html |
||||
[icu]: https://boajs.dev/boa/doc/boa_icu_provider/index.html |
||||
[runtime]: https://boajs.dev/boa/doc/boa_runtime/index.html |
@ -1,23 +0,0 @@
|
||||
[package] |
||||
name = "boa_temporal" |
||||
keywords = ["javascript", "js", "compiler", "temporal", "calendar", "date", "time"] |
||||
categories = ["date", "time", "calendars"] |
||||
readme = "./README.md" |
||||
description.workspace = true |
||||
version.workspace = true |
||||
edition.workspace = true |
||||
authors.workspace = true |
||||
license.workspace = true |
||||
repository.workspace = true |
||||
rust-version.workspace = true |
||||
|
||||
[dependencies] |
||||
tinystr = "0.7.4" |
||||
icu_calendar = { workspace = true, default-features = true } |
||||
rustc-hash = { workspace = true, features = ["std"] } |
||||
num-bigint = { workspace = true, features = ["serde"] } |
||||
bitflags.workspace = true |
||||
num-traits.workspace = true |
||||
|
||||
[lints] |
||||
workspace = true |
@ -1,11 +0,0 @@
|
||||
# Temporal in Rust |
||||
|
||||
Provides a standard API for working with dates and time. |
||||
|
||||
IMPORTANT NOTE: The Temporal Proposal is still in Stage 3. As such, this crate should be viewed |
||||
as highly experimental until the proposal has been completely standardized and released. |
||||
|
||||
## Goal |
||||
|
||||
The intended goal of this crate is to provide an engine agnostic |
||||
implementation of `ECMAScript`'s Temporal algorithms. |
File diff suppressed because it is too large
Load Diff
@ -1,433 +0,0 @@
|
||||
//! This module implements `Date` and any directly related algorithms.
|
||||
|
||||
use tinystr::TinyAsciiStr; |
||||
|
||||
use crate::{ |
||||
components::{ |
||||
calendar::{CalendarProtocol, CalendarSlot}, |
||||
duration::DateDuration, |
||||
DateTime, Duration, |
||||
}, |
||||
iso::{IsoDate, IsoDateSlots}, |
||||
options::{ArithmeticOverflow, TemporalUnit}, |
||||
parser::parse_date_time, |
||||
TemporalError, TemporalResult, |
||||
}; |
||||
use std::str::FromStr; |
||||
|
||||
use super::{ |
||||
calendar::{CalendarDateLike, GetCalendarSlot}, |
||||
duration::TimeDuration, |
||||
}; |
||||
|
||||
/// The native Rust implementation of `Temporal.PlainDate`.
|
||||
#[derive(Debug, Default, Clone)] |
||||
pub struct Date<C: CalendarProtocol> { |
||||
iso: IsoDate, |
||||
calendar: CalendarSlot<C>, |
||||
} |
||||
|
||||
// ==== Private API ====
|
||||
|
||||
impl<C: CalendarProtocol> Date<C> { |
||||
/// Create a new `Date` with the date values and calendar slot.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub(crate) fn new_unchecked(iso: IsoDate, calendar: CalendarSlot<C>) -> Self { |
||||
Self { iso, calendar } |
||||
} |
||||
|
||||
#[inline] |
||||
/// Returns a new moved date and the days associated with that adjustment
|
||||
pub(crate) fn move_relative_date( |
||||
&self, |
||||
duration: &Duration, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<(Self, f64)> { |
||||
let new_date = |
||||
self.contextual_add_date(duration, ArithmeticOverflow::Constrain, context)?; |
||||
let days = f64::from(self.days_until(&new_date)); |
||||
Ok((new_date, days)) |
||||
} |
||||
} |
||||
|
||||
// ==== Public API ====
|
||||
|
||||
impl<C: CalendarProtocol> Date<C> { |
||||
/// Creates a new `Date` while checking for validity.
|
||||
pub fn new( |
||||
year: i32, |
||||
month: i32, |
||||
day: i32, |
||||
calendar: CalendarSlot<C>, |
||||
overflow: ArithmeticOverflow, |
||||
) -> TemporalResult<Self> { |
||||
let iso = IsoDate::new(year, month, day, overflow)?; |
||||
Ok(Self::new_unchecked(iso, calendar)) |
||||
} |
||||
|
||||
#[must_use] |
||||
/// Creates a `Date` from a `DateTime`.
|
||||
pub fn from_datetime(dt: &DateTime<C>) -> Self { |
||||
Self { |
||||
iso: dt.iso_date(), |
||||
calendar: dt.calendar().clone(), |
||||
} |
||||
} |
||||
|
||||
#[inline] |
||||
#[must_use] |
||||
/// Returns this `Date`'s ISO year value.
|
||||
pub const fn iso_year(&self) -> i32 { |
||||
self.iso.year |
||||
} |
||||
|
||||
#[inline] |
||||
#[must_use] |
||||
/// Returns this `Date`'s ISO month value.
|
||||
pub const fn iso_month(&self) -> u8 { |
||||
self.iso.month |
||||
} |
||||
|
||||
#[inline] |
||||
#[must_use] |
||||
/// Returns this `Date`'s ISO day value.
|
||||
pub const fn iso_day(&self) -> u8 { |
||||
self.iso.day |
||||
} |
||||
|
||||
#[inline] |
||||
#[must_use] |
||||
/// Returns the `Date`'s inner `IsoDate` record.
|
||||
pub const fn iso(&self) -> IsoDate { |
||||
self.iso |
||||
} |
||||
|
||||
#[inline] |
||||
#[must_use] |
||||
/// Returns a reference to this `Date`'s calendar slot.
|
||||
pub fn calendar(&self) -> &CalendarSlot<C> { |
||||
&self.calendar |
||||
} |
||||
|
||||
/// 3.5.7 `IsValidISODate`
|
||||
///
|
||||
/// Checks if the current date is a valid `ISODate`.
|
||||
#[must_use] |
||||
pub fn is_valid(&self) -> bool { |
||||
self.iso.is_valid() |
||||
} |
||||
|
||||
/// `DaysUntil`
|
||||
///
|
||||
/// Calculates the epoch days between two `Date`s
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn days_until(&self, other: &Self) -> i32 { |
||||
other.iso.to_epoch_days() - self.iso.to_epoch_days() |
||||
} |
||||
} |
||||
|
||||
// ==== Calendar-derived Public API ====
|
||||
|
||||
impl Date<()> { |
||||
/// Returns the calendar year value.
|
||||
pub fn year(&self) -> TemporalResult<i32> { |
||||
self.calendar |
||||
.year(&CalendarDateLike::Date(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar month value.
|
||||
pub fn month(&self) -> TemporalResult<u8> { |
||||
self.calendar |
||||
.month(&CalendarDateLike::Date(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar month code value.
|
||||
pub fn month_code(&self) -> TemporalResult<TinyAsciiStr<4>> { |
||||
self.calendar |
||||
.month_code(&CalendarDateLike::Date(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar day value.
|
||||
pub fn day(&self) -> TemporalResult<u8> { |
||||
self.calendar |
||||
.day(&CalendarDateLike::Date(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar day of week value.
|
||||
pub fn day_of_week(&self) -> TemporalResult<u16> { |
||||
self.calendar |
||||
.day_of_week(&CalendarDateLike::Date(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar day of year value.
|
||||
pub fn day_of_year(&self) -> TemporalResult<u16> { |
||||
self.calendar |
||||
.day_of_year(&CalendarDateLike::Date(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar week of year value.
|
||||
pub fn week_of_year(&self) -> TemporalResult<u16> { |
||||
self.calendar |
||||
.week_of_year(&CalendarDateLike::Date(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar year of week value.
|
||||
pub fn year_of_week(&self) -> TemporalResult<i32> { |
||||
self.calendar |
||||
.year_of_week(&CalendarDateLike::Date(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar days in week value.
|
||||
pub fn days_in_week(&self) -> TemporalResult<u16> { |
||||
self.calendar |
||||
.days_in_week(&CalendarDateLike::Date(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar days in month value.
|
||||
pub fn days_in_month(&self) -> TemporalResult<u16> { |
||||
self.calendar |
||||
.days_in_month(&CalendarDateLike::Date(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar days in year value.
|
||||
pub fn days_in_year(&self) -> TemporalResult<u16> { |
||||
self.calendar |
||||
.days_in_year(&CalendarDateLike::Date(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar months in year value.
|
||||
pub fn months_in_year(&self) -> TemporalResult<u16> { |
||||
self.calendar |
||||
.months_in_year(&CalendarDateLike::Date(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns returns whether the date in a leap year for the given calendar.
|
||||
pub fn in_leap_year(&self) -> TemporalResult<bool> { |
||||
self.calendar |
||||
.in_leap_year(&CalendarDateLike::Date(self.clone()), &mut ()) |
||||
} |
||||
} |
||||
|
||||
// NOTE(nekevss): The clone below should ideally not change the memory address, but that may
|
||||
// not be true across all cases. I.e., it should be fine as long as the clone is simply a
|
||||
// reference count increment. Need to test.
|
||||
impl<C: CalendarProtocol> Date<C> { |
||||
/// Returns the calendar year value with provided context.
|
||||
pub fn contextual_year(this: &C::Date, context: &mut C::Context) -> TemporalResult<i32> { |
||||
this.get_calendar() |
||||
.year(&CalendarDateLike::CustomDate(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar month value with provided context.
|
||||
pub fn contextual_month(this: &C::Date, context: &mut C::Context) -> TemporalResult<u8> { |
||||
this.get_calendar() |
||||
.month(&CalendarDateLike::CustomDate(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar month code value with provided context.
|
||||
pub fn contextual_month_code( |
||||
this: &C::Date, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<TinyAsciiStr<4>> { |
||||
this.get_calendar() |
||||
.month_code(&CalendarDateLike::CustomDate(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar day value with provided context.
|
||||
pub fn contextual_day(this: &C::Date, context: &mut C::Context) -> TemporalResult<u8> { |
||||
this.get_calendar() |
||||
.day(&CalendarDateLike::CustomDate(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar day of week value with provided context.
|
||||
pub fn contextual_day_of_week(this: &C::Date, context: &mut C::Context) -> TemporalResult<u16> { |
||||
this.get_calendar() |
||||
.day_of_week(&CalendarDateLike::CustomDate(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar day of year value with provided context.
|
||||
pub fn contextual_day_of_year(this: &C::Date, context: &mut C::Context) -> TemporalResult<u16> { |
||||
this.get_calendar() |
||||
.day_of_year(&CalendarDateLike::CustomDate(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar week of year value with provided context.
|
||||
pub fn contextual_week_of_year( |
||||
this: &C::Date, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<u16> { |
||||
this.get_calendar() |
||||
.week_of_year(&CalendarDateLike::CustomDate(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar year of week value with provided context.
|
||||
pub fn contextual_year_of_week( |
||||
this: &C::Date, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<i32> { |
||||
this.get_calendar() |
||||
.year_of_week(&CalendarDateLike::CustomDate(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar days in week value with provided context.
|
||||
pub fn contextual_days_in_week( |
||||
this: &C::Date, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<u16> { |
||||
this.get_calendar() |
||||
.days_in_week(&CalendarDateLike::CustomDate(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar days in month value with provided context.
|
||||
pub fn contextual_days_in_month( |
||||
this: &C::Date, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<u16> { |
||||
this.get_calendar() |
||||
.days_in_month(&CalendarDateLike::CustomDate(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar days in year value with provided context.
|
||||
pub fn contextual_days_in_year( |
||||
this: &C::Date, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<u16> { |
||||
this.get_calendar() |
||||
.days_in_year(&CalendarDateLike::CustomDate(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar months in year value with provided context.
|
||||
pub fn contextual_months_in_year( |
||||
this: &C::Date, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<u16> { |
||||
this.get_calendar() |
||||
.months_in_year(&CalendarDateLike::CustomDate(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns whether the date is in a leap year for the given calendar with provided context.
|
||||
pub fn contextual_in_leap_year( |
||||
this: &C::Date, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<bool> { |
||||
this.get_calendar() |
||||
.in_leap_year(&CalendarDateLike::CustomDate(this.clone()), context) |
||||
} |
||||
} |
||||
|
||||
impl<C: CalendarProtocol> GetCalendarSlot<C> for Date<C> { |
||||
fn get_calendar(&self) -> CalendarSlot<C> { |
||||
self.calendar.clone() |
||||
} |
||||
} |
||||
|
||||
impl<C: CalendarProtocol> IsoDateSlots for Date<C> { |
||||
/// Returns the structs `IsoDate`
|
||||
fn iso_date(&self) -> IsoDate { |
||||
self.iso |
||||
} |
||||
} |
||||
|
||||
// ==== Context based API ====
|
||||
|
||||
impl<C: CalendarProtocol> Date<C> { |
||||
/// Returns the date after adding the given duration to date with a provided context.
|
||||
///
|
||||
/// Temporal Equivalent: 3.5.13 `AddDate ( calendar, plainDate, duration [ , options [ , dateAdd ] ] )`
|
||||
#[inline] |
||||
pub fn contextual_add_date( |
||||
&self, |
||||
duration: &Duration, |
||||
overflow: ArithmeticOverflow, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<Self> { |
||||
// 1. If options is not present, set options to undefined.
|
||||
// 2. If duration.[[Years]] ≠ 0, or duration.[[Months]] ≠ 0, or duration.[[Weeks]] ≠ 0, then
|
||||
if duration.date().years() != 0.0 |
||||
|| duration.date().months() != 0.0 |
||||
|| duration.date().weeks() != 0.0 |
||||
{ |
||||
// a. If dateAdd is not present, then
|
||||
// i. Set dateAdd to unused.
|
||||
// ii. If calendar is an Object, set dateAdd to ? GetMethod(calendar, "dateAdd").
|
||||
// b. Return ? CalendarDateAdd(calendar, plainDate, duration, options, dateAdd).
|
||||
return self.calendar().date_add(self, duration, overflow, context); |
||||
} |
||||
|
||||
// 3. Let overflow be ? ToTemporalOverflow(options).
|
||||
// 4. Let days be ? BalanceTimeDuration(duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]], "day").[[Days]].
|
||||
let (days, _) = TimeDuration::new_unchecked( |
||||
duration.hours(), |
||||
duration.minutes(), |
||||
duration.seconds(), |
||||
duration.milliseconds(), |
||||
duration.microseconds(), |
||||
duration.nanoseconds(), |
||||
) |
||||
.balance(duration.days(), TemporalUnit::Day)?; |
||||
|
||||
// 5. Let result be ? AddISODate(plainDate.[[ISOYear]], plainDate.[[ISOMonth]], plainDate.[[ISODay]], 0, 0, 0, days, overflow).
|
||||
let result = self |
||||
.iso |
||||
.add_iso_date(&DateDuration::new(0f64, 0f64, 0f64, days)?, overflow)?; |
||||
|
||||
Ok(Self::new_unchecked(result, self.calendar().clone())) |
||||
} |
||||
|
||||
/// Returns a duration representing the difference between the dates one and two with a provided context.
|
||||
///
|
||||
/// Temporal Equivalent: 3.5.6 `DifferenceDate ( calendar, one, two, options )`
|
||||
#[inline] |
||||
pub fn contextual_difference_date( |
||||
&self, |
||||
other: &Self, |
||||
largest_unit: TemporalUnit, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<Duration> { |
||||
if self.iso.year == other.iso.year |
||||
&& self.iso.month == other.iso.month |
||||
&& self.iso.day == other.iso.day |
||||
{ |
||||
return Ok(Duration::default()); |
||||
} |
||||
|
||||
if largest_unit == TemporalUnit::Day { |
||||
let days = self.days_until(other); |
||||
return Ok(Duration::from_date_duration(DateDuration::new( |
||||
0f64, |
||||
0f64, |
||||
0f64, |
||||
f64::from(days), |
||||
)?)); |
||||
} |
||||
|
||||
self.calendar() |
||||
.date_until(self, other, largest_unit, context) |
||||
} |
||||
} |
||||
|
||||
// ==== Trait impls ====
|
||||
|
||||
impl<C: CalendarProtocol> FromStr for Date<C> { |
||||
type Err = TemporalError; |
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
let parse_record = parse_date_time(s)?; |
||||
|
||||
let calendar = parse_record.calendar.unwrap_or("iso8601".to_owned()); |
||||
|
||||
let date = IsoDate::new( |
||||
parse_record.date.year, |
||||
parse_record.date.month, |
||||
parse_record.date.day, |
||||
ArithmeticOverflow::Reject, |
||||
)?; |
||||
|
||||
Ok(Self::new_unchecked( |
||||
date, |
||||
CalendarSlot::from_str(&calendar)?, |
||||
)) |
||||
} |
||||
} |
@ -1,447 +0,0 @@
|
||||
//! This module implements `DateTime` any directly related algorithms.
|
||||
|
||||
use crate::{ |
||||
components::{ |
||||
calendar::{CalendarProtocol, CalendarSlot}, |
||||
Instant, |
||||
}, |
||||
iso::{IsoDate, IsoDateSlots, IsoDateTime, IsoTime}, |
||||
options::ArithmeticOverflow, |
||||
parser::parse_date_time, |
||||
TemporalError, TemporalResult, |
||||
}; |
||||
|
||||
use std::str::FromStr; |
||||
use tinystr::TinyAsciiStr; |
||||
|
||||
use super::calendar::{CalendarDateLike, GetCalendarSlot}; |
||||
|
||||
/// The native Rust implementation of `Temporal.PlainDateTime`
|
||||
#[derive(Debug, Default, Clone)] |
||||
pub struct DateTime<C: CalendarProtocol> { |
||||
iso: IsoDateTime, |
||||
calendar: CalendarSlot<C>, |
||||
} |
||||
|
||||
// ==== Private DateTime API ====
|
||||
|
||||
impl<C: CalendarProtocol> DateTime<C> { |
||||
/// Creates a new unchecked `DateTime`.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub(crate) fn new_unchecked(iso: IsoDateTime, calendar: CalendarSlot<C>) -> Self { |
||||
Self { iso, calendar } |
||||
} |
||||
|
||||
#[inline] |
||||
#[must_use] |
||||
/// Utility function for validating `IsoDate`s
|
||||
fn validate_iso(iso: IsoDate) -> bool { |
||||
IsoDateTime::new_unchecked(iso, IsoTime::noon()).is_within_limits() |
||||
} |
||||
|
||||
/// Create a new `DateTime` from an `Instant`.
|
||||
#[inline] |
||||
pub(crate) fn from_instant( |
||||
instant: &Instant, |
||||
offset: f64, |
||||
calendar: CalendarSlot<C>, |
||||
) -> TemporalResult<Self> { |
||||
let iso = IsoDateTime::from_epoch_nanos(&instant.nanos, offset)?; |
||||
Ok(Self { iso, calendar }) |
||||
} |
||||
} |
||||
|
||||
// ==== Public DateTime API ====
|
||||
|
||||
impl<C: CalendarProtocol> DateTime<C> { |
||||
/// Creates a new validated `DateTime`.
|
||||
#[inline] |
||||
#[allow(clippy::too_many_arguments)] |
||||
pub fn new( |
||||
year: i32, |
||||
month: i32, |
||||
day: i32, |
||||
hour: i32, |
||||
minute: i32, |
||||
second: i32, |
||||
millisecond: i32, |
||||
microsecond: i32, |
||||
nanosecond: i32, |
||||
calendar: CalendarSlot<C>, |
||||
) -> TemporalResult<Self> { |
||||
let iso_date = IsoDate::new(year, month, day, ArithmeticOverflow::Reject)?; |
||||
let iso_time = IsoTime::new( |
||||
hour, |
||||
minute, |
||||
second, |
||||
millisecond, |
||||
microsecond, |
||||
nanosecond, |
||||
ArithmeticOverflow::Reject, |
||||
)?; |
||||
Ok(Self::new_unchecked( |
||||
IsoDateTime::new(iso_date, iso_time)?, |
||||
calendar, |
||||
)) |
||||
} |
||||
|
||||
/// Validates whether ISO date slots are within iso limits at noon.
|
||||
#[inline] |
||||
pub fn validate<T: IsoDateSlots>(target: &T) -> bool { |
||||
Self::validate_iso(target.iso_date()) |
||||
} |
||||
|
||||
/// Returns this `Date`'s ISO year value.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub const fn iso_year(&self) -> i32 { |
||||
self.iso.date().year |
||||
} |
||||
|
||||
/// Returns this `Date`'s ISO month value.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub const fn iso_month(&self) -> u8 { |
||||
self.iso.date().month |
||||
} |
||||
|
||||
/// Returns this `Date`'s ISO day value.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub const fn iso_day(&self) -> u8 { |
||||
self.iso.date().day |
||||
} |
||||
|
||||
/// Returns the hour value
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn hour(&self) -> u8 { |
||||
self.iso.time().hour |
||||
} |
||||
|
||||
/// Returns the minute value
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn minute(&self) -> u8 { |
||||
self.iso.time().minute |
||||
} |
||||
|
||||
/// Returns the second value
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn second(&self) -> u8 { |
||||
self.iso.time().second |
||||
} |
||||
|
||||
/// Returns the `millisecond` value
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn millisecond(&self) -> u16 { |
||||
self.iso.time().millisecond |
||||
} |
||||
|
||||
/// Returns the `microsecond` value
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn microsecond(&self) -> u16 { |
||||
self.iso.time().microsecond |
||||
} |
||||
|
||||
/// Returns the `nanosecond` value
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn nanosecond(&self) -> u16 { |
||||
self.iso.time().nanosecond |
||||
} |
||||
|
||||
/// Returns the Calendar value.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn calendar(&self) -> &CalendarSlot<C> { |
||||
&self.calendar |
||||
} |
||||
} |
||||
|
||||
// ==== Calendar-derived public API ====
|
||||
|
||||
// TODO: Revert to `DateTime<C>`.
|
||||
impl DateTime<()> { |
||||
/// Returns the calendar year value.
|
||||
pub fn year(&self) -> TemporalResult<i32> { |
||||
self.calendar |
||||
.year(&CalendarDateLike::DateTime(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar month value.
|
||||
pub fn month(&self) -> TemporalResult<u8> { |
||||
self.calendar |
||||
.month(&CalendarDateLike::DateTime(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar month code value.
|
||||
pub fn month_code(&self) -> TemporalResult<TinyAsciiStr<4>> { |
||||
self.calendar |
||||
.month_code(&CalendarDateLike::DateTime(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar day value.
|
||||
pub fn day(&self) -> TemporalResult<u8> { |
||||
self.calendar |
||||
.day(&CalendarDateLike::DateTime(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar day of week value.
|
||||
pub fn day_of_week(&self) -> TemporalResult<u16> { |
||||
self.calendar |
||||
.day_of_week(&CalendarDateLike::DateTime(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar day of year value.
|
||||
pub fn day_of_year(&self) -> TemporalResult<u16> { |
||||
self.calendar |
||||
.day_of_year(&CalendarDateLike::DateTime(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar week of year value.
|
||||
pub fn week_of_year(&self) -> TemporalResult<u16> { |
||||
self.calendar |
||||
.week_of_year(&CalendarDateLike::DateTime(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar year of week value.
|
||||
pub fn year_of_week(&self) -> TemporalResult<i32> { |
||||
self.calendar |
||||
.year_of_week(&CalendarDateLike::DateTime(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar days in week value.
|
||||
pub fn days_in_week(&self) -> TemporalResult<u16> { |
||||
self.calendar |
||||
.days_in_week(&CalendarDateLike::DateTime(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar days in month value.
|
||||
pub fn days_in_month(&self) -> TemporalResult<u16> { |
||||
self.calendar |
||||
.days_in_month(&CalendarDateLike::DateTime(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar days in year value.
|
||||
pub fn days_in_year(&self) -> TemporalResult<u16> { |
||||
self.calendar |
||||
.days_in_year(&CalendarDateLike::DateTime(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns the calendar months in year value.
|
||||
pub fn months_in_year(&self) -> TemporalResult<u16> { |
||||
self.calendar |
||||
.months_in_year(&CalendarDateLike::DateTime(self.clone()), &mut ()) |
||||
} |
||||
|
||||
/// Returns returns whether the date in a leap year for the given calendar.
|
||||
pub fn in_leap_year(&self) -> TemporalResult<bool> { |
||||
self.calendar |
||||
.in_leap_year(&CalendarDateLike::DateTime(self.clone()), &mut ()) |
||||
} |
||||
} |
||||
|
||||
impl<C: CalendarProtocol> DateTime<C> { |
||||
/// Returns the calendar year value with provided context.
|
||||
pub fn contextual_year(this: &C::DateTime, context: &mut C::Context) -> TemporalResult<i32> { |
||||
this.get_calendar() |
||||
.year(&CalendarDateLike::CustomDateTime(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar month value with provided context.
|
||||
pub fn contextual_month(this: &C::DateTime, context: &mut C::Context) -> TemporalResult<u8> { |
||||
this.get_calendar() |
||||
.month(&CalendarDateLike::CustomDateTime(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar month code value with provided context.
|
||||
pub fn contextual_month_code( |
||||
this: &C::DateTime, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<TinyAsciiStr<4>> { |
||||
this.get_calendar() |
||||
.month_code(&CalendarDateLike::CustomDateTime(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar day value with provided context.
|
||||
pub fn contextual_day(this: &C::DateTime, context: &mut C::Context) -> TemporalResult<u8> { |
||||
this.get_calendar() |
||||
.day(&CalendarDateLike::CustomDateTime(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar day of week value with provided context.
|
||||
pub fn contextual_day_of_week( |
||||
this: &C::DateTime, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<u16> { |
||||
this.get_calendar() |
||||
.day_of_week(&CalendarDateLike::CustomDateTime(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar day of year value with provided context.
|
||||
pub fn contextual_day_of_year( |
||||
this: &C::DateTime, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<u16> { |
||||
this.get_calendar() |
||||
.day_of_year(&CalendarDateLike::CustomDateTime(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar week of year value with provided context.
|
||||
pub fn contextual_week_of_year( |
||||
this: &C::DateTime, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<u16> { |
||||
this.get_calendar() |
||||
.week_of_year(&CalendarDateLike::CustomDateTime(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar year of week value with provided context.
|
||||
pub fn contextual_year_of_week( |
||||
this: &C::DateTime, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<i32> { |
||||
this.get_calendar() |
||||
.year_of_week(&CalendarDateLike::CustomDateTime(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar days in week value with provided context.
|
||||
pub fn contextual_days_in_week( |
||||
this: &C::DateTime, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<u16> { |
||||
this.get_calendar() |
||||
.days_in_week(&CalendarDateLike::CustomDateTime(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar days in month value with provided context.
|
||||
pub fn contextual_days_in_month( |
||||
this: &C::DateTime, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<u16> { |
||||
this.get_calendar() |
||||
.days_in_month(&CalendarDateLike::CustomDateTime(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar days in year value with provided context.
|
||||
pub fn contextual_days_in_year( |
||||
this: &C::DateTime, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<u16> { |
||||
this.get_calendar() |
||||
.days_in_year(&CalendarDateLike::CustomDateTime(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns the calendar months in year value with provided context.
|
||||
pub fn contextual_months_in_year( |
||||
this: &C::DateTime, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<u16> { |
||||
this.get_calendar() |
||||
.months_in_year(&CalendarDateLike::CustomDateTime(this.clone()), context) |
||||
} |
||||
|
||||
/// Returns whether the date is in a leap year for the given calendar with provided context.
|
||||
pub fn contextual_in_leap_year( |
||||
this: &C::DateTime, |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<bool> { |
||||
this.get_calendar() |
||||
.in_leap_year(&CalendarDateLike::CustomDateTime(this.clone()), context) |
||||
} |
||||
} |
||||
|
||||
// ==== Trait impls ====
|
||||
|
||||
impl<C: CalendarProtocol> GetCalendarSlot<C> for DateTime<C> { |
||||
fn get_calendar(&self) -> CalendarSlot<C> { |
||||
self.calendar.clone() |
||||
} |
||||
} |
||||
|
||||
impl<C: CalendarProtocol> IsoDateSlots for DateTime<C> { |
||||
fn iso_date(&self) -> IsoDate { |
||||
*self.iso.date() |
||||
} |
||||
} |
||||
|
||||
impl<C: CalendarProtocol> FromStr for DateTime<C> { |
||||
type Err = TemporalError; |
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
let parse_record = parse_date_time(s)?; |
||||
|
||||
let calendar = parse_record.calendar.unwrap_or("iso8601".to_owned()); |
||||
|
||||
let time = if let Some(time) = parse_record.time { |
||||
IsoTime::from_components( |
||||
i32::from(time.hour), |
||||
i32::from(time.minute), |
||||
i32::from(time.second), |
||||
time.fraction, |
||||
)? |
||||
} else { |
||||
IsoTime::default() |
||||
}; |
||||
|
||||
let date = IsoDate::new( |
||||
parse_record.date.year, |
||||
parse_record.date.month, |
||||
parse_record.date.day, |
||||
ArithmeticOverflow::Reject, |
||||
)?; |
||||
|
||||
Ok(Self::new_unchecked( |
||||
IsoDateTime::new(date, time)?, |
||||
CalendarSlot::from_str(&calendar)?, |
||||
)) |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use std::str::FromStr; |
||||
|
||||
use crate::components::calendar::CalendarSlot; |
||||
|
||||
use super::DateTime; |
||||
|
||||
#[test] |
||||
#[allow(clippy::float_cmp)] |
||||
fn plain_date_time_limits() { |
||||
// This test is primarily to assert that the `expect` in the epoch methods is
|
||||
// valid, i.e., a valid instant is within the range of an f64.
|
||||
let negative_limit = DateTime::<()>::new( |
||||
-271_821, |
||||
4, |
||||
19, |
||||
0, |
||||
0, |
||||
0, |
||||
0, |
||||
0, |
||||
0, |
||||
CalendarSlot::from_str("iso8601").unwrap(), |
||||
); |
||||
let positive_limit = DateTime::<()>::new( |
||||
275_760, |
||||
9, |
||||
14, |
||||
0, |
||||
0, |
||||
0, |
||||
0, |
||||
0, |
||||
0, |
||||
CalendarSlot::from_str("iso8601").unwrap(), |
||||
); |
||||
|
||||
assert!(negative_limit.is_err()); |
||||
assert!(positive_limit.is_err()); |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -1,486 +0,0 @@
|
||||
//! Implementation of a `DateDuration`
|
||||
|
||||
use crate::{ |
||||
components::{ |
||||
calendar::CalendarProtocol, duration::TimeDuration, tz::TzProtocol, Date, DateTime, |
||||
Duration, ZonedDateTime, |
||||
}, |
||||
options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit}, |
||||
utils, TemporalError, TemporalResult, NS_PER_DAY, |
||||
}; |
||||
|
||||
/// `DateDuration` represents the [date duration record][spec] of the `Duration.`
|
||||
///
|
||||
/// These fields are laid out in the [Temporal Proposal][field spec] as 64-bit floating point numbers.
|
||||
///
|
||||
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-date-duration-records
|
||||
/// [field spec]: https://tc39.es/proposal-temporal/#sec-properties-of-temporal-duration-instances
|
||||
#[derive(Debug, Default, Clone, Copy)] |
||||
pub struct DateDuration { |
||||
pub(crate) years: f64, |
||||
pub(crate) months: f64, |
||||
pub(crate) weeks: f64, |
||||
pub(crate) days: f64, |
||||
} |
||||
|
||||
impl DateDuration { |
||||
/// Creates a new, non-validated `DateDuration`.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub(crate) const fn new_unchecked(years: f64, months: f64, weeks: f64, days: f64) -> Self { |
||||
Self { |
||||
years, |
||||
months, |
||||
weeks, |
||||
days, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl DateDuration { |
||||
/// Creates a new `DateDuration` with provided values.
|
||||
pub fn new(years: f64, months: f64, weeks: f64, days: f64) -> TemporalResult<Self> { |
||||
let result = Self::new_unchecked(years, months, weeks, days); |
||||
if !super::is_valid_duration(&result.into_iter().collect()) { |
||||
return Err(TemporalError::range().with_message("Invalid DateDuration.")); |
||||
} |
||||
Ok(result) |
||||
} |
||||
|
||||
/// Returns a `PartialDateDuration` with all fields set to `NaN`.
|
||||
#[must_use] |
||||
pub const fn partial() -> Self { |
||||
Self { |
||||
years: f64::NAN, |
||||
months: f64::NAN, |
||||
weeks: f64::NAN, |
||||
days: f64::NAN, |
||||
} |
||||
} |
||||
|
||||
/// Creates a `DateDuration` from a provided partial `DateDuration`.
|
||||
#[must_use] |
||||
pub fn from_partial(partial: &DateDuration) -> Self { |
||||
Self { |
||||
years: if partial.years.is_nan() { |
||||
0.0 |
||||
} else { |
||||
partial.years |
||||
}, |
||||
months: if partial.months.is_nan() { |
||||
0.0 |
||||
} else { |
||||
partial.months |
||||
}, |
||||
weeks: if partial.weeks.is_nan() { |
||||
0.0 |
||||
} else { |
||||
partial.weeks |
||||
}, |
||||
days: if partial.days.is_nan() { |
||||
0.0 |
||||
} else { |
||||
partial.days |
||||
}, |
||||
} |
||||
} |
||||
|
||||
/// Returns a new `DateDuration` representing the absolute value of the current.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn abs(&self) -> Self { |
||||
Self { |
||||
years: self.years.abs(), |
||||
months: self.months.abs(), |
||||
weeks: self.weeks.abs(), |
||||
days: self.days.abs(), |
||||
} |
||||
} |
||||
|
||||
/// Returns the `[[years]]` value.
|
||||
#[must_use] |
||||
pub const fn years(&self) -> f64 { |
||||
self.years |
||||
} |
||||
|
||||
/// Returns the `[[months]]` value.
|
||||
#[must_use] |
||||
pub const fn months(&self) -> f64 { |
||||
self.months |
||||
} |
||||
|
||||
/// Returns the `[[weeks]]` value.
|
||||
#[must_use] |
||||
pub const fn weeks(&self) -> f64 { |
||||
self.weeks |
||||
} |
||||
|
||||
/// Returns the `[[days]]` value.
|
||||
#[must_use] |
||||
pub const fn days(&self) -> f64 { |
||||
self.days |
||||
} |
||||
|
||||
/// Returns the iterator for `DateDuration`
|
||||
#[must_use] |
||||
pub fn iter(&self) -> DateIter<'_> { |
||||
<&Self as IntoIterator>::into_iter(self) |
||||
} |
||||
} |
||||
|
||||
// ==== DateDuration Operations ====
|
||||
|
||||
impl DateDuration { |
||||
/// Rounds the current `DateDuration` returning a tuple of the rounded `DateDuration` and
|
||||
/// the `total` value of the smallest unit prior to rounding.
|
||||
#[allow(clippy::type_complexity, clippy::let_and_return)] |
||||
pub fn round<C: CalendarProtocol, Z: TzProtocol>( |
||||
&self, |
||||
additional_time: Option<TimeDuration>, |
||||
increment: f64, |
||||
unit: TemporalUnit, |
||||
rounding_mode: TemporalRoundingMode, |
||||
relative_targets: ( |
||||
Option<&Date<C>>, |
||||
Option<&ZonedDateTime<C, Z>>, |
||||
Option<&DateTime<C>>, |
||||
), |
||||
context: &mut C::Context, |
||||
) -> TemporalResult<(Self, f64)> { |
||||
// 1. If plainRelativeTo is not present, set plainRelativeTo to undefined.
|
||||
let plain_relative_to = relative_targets.0; |
||||
// 2. If zonedRelativeTo is not present, set zonedRelativeTo to undefined.
|
||||
let zoned_relative_to = relative_targets.1; |
||||
// 3. If precalculatedPlainDateTime is not present, set precalculatedPlainDateTime to undefined.
|
||||
let _ = relative_targets.2; |
||||
|
||||
let mut fractional_days = match unit { |
||||
// 4. If unit is "year", "month", or "week", and plainRelativeTo is undefined, then
|
||||
TemporalUnit::Year | TemporalUnit::Month | TemporalUnit::Week |
||||
if plain_relative_to.is_none() => |
||||
{ |
||||
// a. Throw a RangeError exception.
|
||||
return Err(TemporalError::range() |
||||
.with_message("plainRelativeTo canot be undefined with given TemporalUnit")); |
||||
} |
||||
// 5. If unit is one of "year", "month", "week", or "day", then
|
||||
TemporalUnit::Year | TemporalUnit::Month | TemporalUnit::Week | TemporalUnit::Day => { |
||||
// a. Let nanoseconds be TotalDurationNanoseconds(hours, minutes, seconds, milliseconds, microseconds, nanoseconds).
|
||||
let nanoseconds = additional_time.unwrap_or_default().as_nanos(); |
||||
|
||||
// b. If zonedRelativeTo is not undefined, then
|
||||
// i. Let intermediate be ? MoveRelativeZonedDateTime(zonedRelativeTo, years, months, weeks, days, precalculatedPlainDateTime).
|
||||
// ii. Let result be ? NanosecondsToDays(nanoseconds, intermediate).
|
||||
// iii. Let fractionalDays be days + result.[[Days]] + result.[[Nanoseconds]] / result.[[DayLength]].
|
||||
// c. Else,
|
||||
// i. Let fractionalDays be days + nanoseconds / nsPerDay.
|
||||
// d. Set days, hours, minutes, seconds, milliseconds, microseconds, and nanoseconds to 0.
|
||||
// e. Assert: fractionalSeconds is not used below.
|
||||
if zoned_relative_to.is_none() { |
||||
self.days + nanoseconds / NS_PER_DAY as f64 |
||||
} else { |
||||
// implementation of b: i-iii needed.
|
||||
return Err(TemporalError::range().with_message("Not yet implemented.")); |
||||
} |
||||
} |
||||
_ => { |
||||
return Err(TemporalError::range() |
||||
.with_message("Invalid TemporalUnit provided to DateDuration.round")) |
||||
} |
||||
}; |
||||
|
||||
// 7. let total be unset.
|
||||
// We begin matching against unit and return the remainder value.
|
||||
match unit { |
||||
// 8. If unit is "year", then
|
||||
TemporalUnit::Year => { |
||||
let plain_relative_to = plain_relative_to.expect("this must exist."); |
||||
// a. Let calendar be plainRelativeTo.[[Calendar]].
|
||||
let calendar = plain_relative_to.calendar(); |
||||
|
||||
// b. Let yearsDuration be ! CreateTemporalDuration(years, 0, 0, 0, 0, 0, 0, 0, 0, 0).
|
||||
let years = DateDuration::new_unchecked(self.years, 0.0, 0.0, 0.0); |
||||
let years_duration = Duration::new_unchecked(years, TimeDuration::default()); |
||||
|
||||
// c. If calendar is an Object, then
|
||||
// i. Let dateAdd be ? GetMethod(calendar, "dateAdd").
|
||||
// d. Else,
|
||||
// i. Let dateAdd be unused.
|
||||
|
||||
// e. Let yearsLater be ? AddDate(calendar, plainRelativeTo, yearsDuration, undefined, dateAdd).
|
||||
let years_later = plain_relative_to.contextual_add_date( |
||||
&years_duration, |
||||
ArithmeticOverflow::Constrain, |
||||
context, |
||||
)?; |
||||
|
||||
// f. Let yearsMonthsWeeks be ! CreateTemporalDuration(years, months, weeks, 0, 0, 0, 0, 0, 0, 0).
|
||||
let years_months_weeks = Duration::new_unchecked( |
||||
Self::new_unchecked(self.years, self.months, self.weeks, 0.0), |
||||
TimeDuration::default(), |
||||
); |
||||
|
||||
// g. Let yearsMonthsWeeksLater be ? AddDate(calendar, plainRelativeTo, yearsMonthsWeeks, undefined, dateAdd).
|
||||
let years_months_weeks_later = plain_relative_to.contextual_add_date( |
||||
&years_months_weeks, |
||||
ArithmeticOverflow::Constrain, |
||||
context, |
||||
)?; |
||||
|
||||
// h. Let monthsWeeksInDays be DaysUntil(yearsLater, yearsMonthsWeeksLater).
|
||||
let months_weeks_in_days = years_later.days_until(&years_months_weeks_later); |
||||
|
||||
// i. Set plainRelativeTo to yearsLater.
|
||||
let plain_relative_to = years_later; |
||||
|
||||
// j. Set fractionalDays to fractionalDays + monthsWeeksInDays.
|
||||
fractional_days += f64::from(months_weeks_in_days); |
||||
|
||||
// k. Let isoResult be ! AddISODate(plainRelativeTo.[[ISOYear]]. plainRelativeTo.[[ISOMonth]], plainRelativeTo.[[ISODay]], 0, 0, 0, truncate(fractionalDays), "constrain").
|
||||
let iso_result = plain_relative_to.iso().add_iso_date( |
||||
&DateDuration::new_unchecked(0.0, 0.0, 0.0, fractional_days.trunc()), |
||||
ArithmeticOverflow::Constrain, |
||||
)?; |
||||
|
||||
// l. Let wholeDaysLater be ? CreateDate(isoResult.[[Year]], isoResult.[[Month]], isoResult.[[Day]], calendar).
|
||||
let whole_days_later = Date::new_unchecked(iso_result, calendar.clone()); |
||||
|
||||
// m. Let untilOptions be OrdinaryObjectCreate(null).
|
||||
// n. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", "year").
|
||||
// o. Let timePassed be ? DifferenceDate(calendar, plainRelativeTo, wholeDaysLater, untilOptions).
|
||||
let time_passed = plain_relative_to.contextual_difference_date( |
||||
&whole_days_later, |
||||
TemporalUnit::Year, |
||||
context, |
||||
)?; |
||||
|
||||
// p. Let yearsPassed be timePassed.[[Years]].
|
||||
let years_passed = time_passed.date.years(); |
||||
|
||||
// q. Set years to years + yearsPassed.
|
||||
let years = self.years() + years_passed; |
||||
|
||||
// r. Let yearsDuration be ! CreateTemporalDuration(yearsPassed, 0, 0, 0, 0, 0, 0, 0, 0, 0).
|
||||
let years_duration = Duration::one_year(years_passed); |
||||
|
||||
// s. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, yearsDuration, dateAdd).
|
||||
// t. Set plainRelativeTo to moveResult.[[RelativeTo]].
|
||||
// u. Let daysPassed be moveResult.[[Days]].
|
||||
let (plain_relative_to, days_passed) = |
||||
plain_relative_to.move_relative_date(&years_duration, context)?; |
||||
|
||||
// v. Set fractionalDays to fractionalDays - daysPassed.
|
||||
fractional_days -= days_passed; |
||||
|
||||
// w. If fractionalDays < 0, let sign be -1; else, let sign be 1.
|
||||
let sign = if fractional_days < 0.0 { -1 } else { 1 }; |
||||
|
||||
// x. Let oneYear be ! CreateTemporalDuration(sign, 0, 0, 0, 0, 0, 0, 0, 0, 0).
|
||||
let one_year = Duration::one_year(f64::from(sign)); |
||||
|
||||
// y. Set moveResult to ? MoveRelativeDate(calendar, plainRelativeTo, oneYear, dateAdd).
|
||||
// z. Let oneYearDays be moveResult.[[Days]].
|
||||
let (_, one_year_days) = |
||||
plain_relative_to.move_relative_date(&one_year, context)?; |
||||
|
||||
// aa. Let fractionalYears be years + fractionalDays / abs(oneYearDays).
|
||||
let frac_years = years + (fractional_days / one_year_days.abs()); |
||||
|
||||
// ab. Set years to RoundNumberToIncrement(fractionalYears, increment, roundingMode).
|
||||
let rounded_years = |
||||
utils::round_number_to_increment(frac_years, increment, rounding_mode); |
||||
|
||||
// ac. Set total to fractionalYears.
|
||||
// ad. Set months and weeks to 0.
|
||||
let result = Self::new(rounded_years, 0f64, 0f64, 0f64)?; |
||||
Ok((result, frac_years)) |
||||
} |
||||
// 9. Else if unit is "month", then
|
||||
TemporalUnit::Month => { |
||||
// a. Let calendar be plainRelativeTo.[[Calendar]].
|
||||
let plain_relative_to = plain_relative_to.expect("this must exist."); |
||||
|
||||
// b. Let yearsMonths be ! CreateTemporalDuration(years, months, 0, 0, 0, 0, 0, 0, 0, 0).
|
||||
let years_months = Duration::from_date_duration(DateDuration::new_unchecked( |
||||
self.years(), |
||||
self.months(), |
||||
0.0, |
||||
0.0, |
||||
)); |
||||
|
||||
// c. If calendar is an Object, then
|
||||
// i. Let dateAdd be ? GetMethod(calendar, "dateAdd").
|
||||
// d. Else,
|
||||
// i. Let dateAdd be unused.
|
||||
|
||||
// e. Let yearsMonthsLater be ? AddDate(calendar, plainRelativeTo, yearsMonths, undefined, dateAdd).
|
||||
let years_months_later = plain_relative_to.contextual_add_date( |
||||
&years_months, |
||||
ArithmeticOverflow::Constrain, |
||||
context, |
||||
)?; |
||||
|
||||
// f. Let yearsMonthsWeeks be ! CreateTemporalDuration(years, months, weeks, 0, 0, 0, 0, 0, 0, 0).
|
||||
let years_months_weeks = Duration::from_date_duration(DateDuration::new_unchecked( |
||||
self.years(), |
||||
self.months(), |
||||
self.weeks(), |
||||
0.0, |
||||
)); |
||||
|
||||
// g. Let yearsMonthsWeeksLater be ? AddDate(calendar, plainRelativeTo, yearsMonthsWeeks, undefined, dateAdd).
|
||||
let years_months_weeks_later = plain_relative_to.contextual_add_date( |
||||
&years_months_weeks, |
||||
ArithmeticOverflow::Constrain, |
||||
context, |
||||
)?; |
||||
|
||||
// h. Let weeksInDays be DaysUntil(yearsMonthsLater, yearsMonthsWeeksLater).
|
||||
let weeks_in_days = years_months_later.days_until(&years_months_weeks_later); |
||||
|
||||
// i. Set plainRelativeTo to yearsMonthsLater.
|
||||
let plain_relative_to = years_months_later; |
||||
|
||||
// j. Set fractionalDays to fractionalDays + weeksInDays.
|
||||
fractional_days += f64::from(weeks_in_days); |
||||
|
||||
// k. If fractionalDays < 0, let sign be -1; else, let sign be 1.
|
||||
let sign = if fractional_days < 0.0 { -1f64 } else { 1f64 }; |
||||
|
||||
// l. Let oneMonth be ! CreateTemporalDuration(0, sign, 0, 0, 0, 0, 0, 0, 0, 0).
|
||||
let one_month = Duration::one_month(sign); |
||||
|
||||
// m. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, oneMonth, dateAdd).
|
||||
// n. Set plainRelativeTo to moveResult.[[RelativeTo]].
|
||||
// o. Let oneMonthDays be moveResult.[[Days]].
|
||||
let (mut plain_relative_to, mut one_month_days) = |
||||
plain_relative_to.move_relative_date(&one_month, context)?; |
||||
|
||||
let mut months = self.months; |
||||
// p. Repeat, while abs(fractionalDays) ≥ abs(oneMonthDays),
|
||||
while fractional_days.abs() >= one_month_days.abs() { |
||||
// i. Set months to months + sign.
|
||||
months += sign; |
||||
|
||||
// ii. Set fractionalDays to fractionalDays - oneMonthDays.
|
||||
fractional_days -= one_month_days; |
||||
|
||||
// iii. Set moveResult to ? MoveRelativeDate(calendar, plainRelativeTo, oneMonth, dateAdd).
|
||||
let move_result = plain_relative_to.move_relative_date(&one_month, context)?; |
||||
|
||||
// iv. Set plainRelativeTo to moveResult.[[RelativeTo]].
|
||||
plain_relative_to = move_result.0; |
||||
// v. Set oneMonthDays to moveResult.[[Days]].
|
||||
one_month_days = move_result.1; |
||||
} |
||||
|
||||
// q. Let fractionalMonths be months + fractionalDays / abs(oneMonthDays).
|
||||
let frac_months = months + fractional_days / one_month_days.abs(); |
||||
|
||||
// r. Set months to RoundNumberToIncrement(fractionalMonths, increment, roundingMode).
|
||||
let rounded_months = |
||||
utils::round_number_to_increment(frac_months, increment, rounding_mode); |
||||
|
||||
// s. Set total to fractionalMonths.
|
||||
// t. Set weeks to 0.
|
||||
let result = Self::new(self.years, rounded_months, 0f64, 0f64)?; |
||||
Ok((result, frac_months)) |
||||
} |
||||
// 10. Else if unit is "week", then
|
||||
TemporalUnit::Week => { |
||||
// a. Let calendar be plainRelativeTo.[[Calendar]].
|
||||
let plain_relative_to = plain_relative_to.expect("date must exist given Week"); |
||||
|
||||
// b. If fractionalDays < 0, let sign be -1; else, let sign be 1.
|
||||
let sign = if fractional_days < 0.0 { -1f64 } else { 1f64 }; |
||||
|
||||
// c. Let oneWeek be ! CreateTemporalDuration(0, 0, sign, 0, 0, 0, 0, 0, 0, 0).
|
||||
let one_week = Duration::one_week(sign); |
||||
|
||||
// d. If calendar is an Object, then
|
||||
// i. Let dateAdd be ? GetMethod(calendar, "dateAdd").
|
||||
// e. Else,
|
||||
// i. Let dateAdd be unused.
|
||||
|
||||
// f. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, oneWeek, dateAdd).
|
||||
// g. Set plainRelativeTo to moveResult.[[RelativeTo]].
|
||||
// h. Let oneWeekDays be moveResult.[[Days]].
|
||||
let (mut plain_relative_to, mut one_week_days) = |
||||
plain_relative_to.move_relative_date(&one_week, context)?; |
||||
|
||||
let mut weeks = self.weeks; |
||||
// i. Repeat, while abs(fractionalDays) ≥ abs(oneWeekDays),
|
||||
while fractional_days.abs() >= one_week_days.abs() { |
||||
// i. Set weeks to weeks + sign.
|
||||
weeks += sign; |
||||
|
||||
// ii. Set fractionalDays to fractionalDays - oneWeekDays.
|
||||
fractional_days -= one_week_days; |
||||
|
||||
// iii. Set moveResult to ? MoveRelativeDate(calendar, plainRelativeTo, oneWeek, dateAdd).
|
||||
let move_result = plain_relative_to.move_relative_date(&one_week, context)?; |
||||
|
||||
// iv. Set plainRelativeTo to moveResult.[[RelativeTo]].
|
||||
plain_relative_to = move_result.0; |
||||
// v. Set oneWeekDays to moveResult.[[Days]].
|
||||
one_week_days = move_result.1; |
||||
} |
||||
|
||||
// j. Let fractionalWeeks be weeks + fractionalDays / abs(oneWeekDays).
|
||||
let frac_weeks = weeks + fractional_days / one_week_days.abs(); |
||||
|
||||
// k. Set weeks to RoundNumberToIncrement(fractionalWeeks, increment, roundingMode).
|
||||
let rounded_weeks = |
||||
utils::round_number_to_increment(frac_weeks, increment, rounding_mode); |
||||
// l. Set total to fractionalWeeks.
|
||||
let result = Self::new(self.years, self.months, rounded_weeks, 0f64)?; |
||||
Ok((result, frac_weeks)) |
||||
} |
||||
// 11. Else if unit is "day", then
|
||||
TemporalUnit::Day => { |
||||
// a. Set days to RoundNumberToIncrement(fractionalDays, increment, roundingMode).
|
||||
let rounded_days = |
||||
utils::round_number_to_increment(fractional_days, increment, rounding_mode); |
||||
// b. Set total to fractionalDays.
|
||||
let result = Self::new(self.years, self.months, self.weeks, rounded_days)?; |
||||
Ok((result, fractional_days)) |
||||
} |
||||
_ => unreachable!("All other TemporalUnits were returned early as invalid."), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl<'a> IntoIterator for &'a DateDuration { |
||||
type Item = f64; |
||||
type IntoIter = DateIter<'a>; |
||||
|
||||
fn into_iter(self) -> Self::IntoIter { |
||||
DateIter { |
||||
date: self, |
||||
index: 0, |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// An iterator over the `DateDuration`
|
||||
#[derive(Debug)] |
||||
pub struct DateIter<'a> { |
||||
date: &'a DateDuration, |
||||
index: usize, |
||||
} |
||||
|
||||
impl Iterator for DateIter<'_> { |
||||
type Item = f64; |
||||
|
||||
fn next(&mut self) -> Option<Self::Item> { |
||||
let result = match self.index { |
||||
0 => Some(self.date.years), |
||||
1 => Some(self.date.months), |
||||
2 => Some(self.date.weeks), |
||||
3 => Some(self.date.days), |
||||
_ => None, |
||||
}; |
||||
self.index += 1; |
||||
result |
||||
} |
||||
} |
@ -1,596 +0,0 @@
|
||||
//! An implementation of `TimeDuration` and it's methods.
|
||||
|
||||
use crate::{ |
||||
options::{TemporalRoundingMode, TemporalUnit}, |
||||
utils, TemporalError, TemporalResult, |
||||
}; |
||||
|
||||
use super::is_valid_duration; |
||||
|
||||
/// `TimeDuration` represents the [Time Duration record][spec] of the `Duration.`
|
||||
///
|
||||
/// These fields are laid out in the [Temporal Proposal][field spec] as 64-bit floating point numbers.
|
||||
///
|
||||
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-time-duration-records
|
||||
/// [field spec]: https://tc39.es/proposal-temporal/#sec-properties-of-temporal-duration-instances
|
||||
#[derive(Debug, Default, Clone, Copy)] |
||||
pub struct TimeDuration { |
||||
pub(crate) hours: f64, |
||||
pub(crate) minutes: f64, |
||||
pub(crate) seconds: f64, |
||||
pub(crate) milliseconds: f64, |
||||
pub(crate) microseconds: f64, |
||||
pub(crate) nanoseconds: f64, |
||||
} |
||||
// ==== TimeDuration Private API ====
|
||||
|
||||
impl TimeDuration { |
||||
/// Creates a new `TimeDuration`.
|
||||
#[must_use] |
||||
pub(crate) const fn new_unchecked( |
||||
hours: f64, |
||||
minutes: f64, |
||||
seconds: f64, |
||||
milliseconds: f64, |
||||
microseconds: f64, |
||||
nanoseconds: f64, |
||||
) -> Self { |
||||
Self { |
||||
hours, |
||||
minutes, |
||||
seconds, |
||||
milliseconds, |
||||
microseconds, |
||||
nanoseconds, |
||||
} |
||||
} |
||||
|
||||
/// Returns the current `TimeDuration` as nanoseconds.
|
||||
#[inline] |
||||
pub(crate) fn as_nanos(&self) -> f64 { |
||||
self.hours |
||||
.mul_add(60_f64, self.minutes) |
||||
.mul_add(60_f64, self.seconds) |
||||
.mul_add(1_000_f64, self.milliseconds) |
||||
.mul_add(1_000_f64, self.microseconds) |
||||
.mul_add(1_000_f64, self.nanoseconds) |
||||
} |
||||
|
||||
/// Abstract Operation 7.5.18 `BalancePossiblyInfiniteDuration ( days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit )`
|
||||
///
|
||||
/// This function will balance the current `TimeDuration`. It returns the balanced `day` and `TimeDuration` value.
|
||||
#[allow(clippy::too_many_arguments)] |
||||
pub(crate) fn balance_possibly_infinite_time_duration( |
||||
days: f64, |
||||
hours: f64, |
||||
minutes: f64, |
||||
seconds: f64, |
||||
milliseconds: f64, |
||||
microseconds: f64, |
||||
nanoseconds: f64, |
||||
largest_unit: TemporalUnit, |
||||
) -> TemporalResult<(f64, Option<Self>)> { |
||||
// 1. Set hours to hours + days × 24.
|
||||
let hours = hours + (days * 24f64); |
||||
|
||||
// 2. Set nanoseconds to TotalDurationNanoseconds(hours, minutes, seconds, milliseconds, microseconds, nanoseconds).
|
||||
let mut nanoseconds = Self::new_unchecked( |
||||
hours, |
||||
minutes, |
||||
seconds, |
||||
milliseconds, |
||||
microseconds, |
||||
nanoseconds, |
||||
) |
||||
.as_nanos(); |
||||
|
||||
// 3. Set days, hours, minutes, seconds, milliseconds, and microseconds to 0.
|
||||
let mut days = 0f64; |
||||
let mut hours = 0f64; |
||||
let mut minutes = 0f64; |
||||
let mut seconds = 0f64; |
||||
let mut milliseconds = 0f64; |
||||
let mut microseconds = 0f64; |
||||
|
||||
// 4. If nanoseconds < 0, let sign be -1; else, let sign be 1.
|
||||
let sign = if nanoseconds < 0f64 { -1 } else { 1 }; |
||||
// 5. Set nanoseconds to abs(nanoseconds).
|
||||
nanoseconds = nanoseconds.abs(); |
||||
|
||||
match largest_unit { |
||||
// 9. If largestUnit is "year", "month", "week", "day", or "hour", then
|
||||
TemporalUnit::Year |
||||
| TemporalUnit::Month |
||||
| TemporalUnit::Week |
||||
| TemporalUnit::Day |
||||
| TemporalUnit::Hour => { |
||||
// a. Set microseconds to floor(nanoseconds / 1000).
|
||||
microseconds = (nanoseconds / 1000f64).floor(); |
||||
// b. Set nanoseconds to nanoseconds modulo 1000.
|
||||
nanoseconds %= 1000f64; |
||||
|
||||
// c. Set milliseconds to floor(microseconds / 1000).
|
||||
milliseconds = (microseconds / 1000f64).floor(); |
||||
// d. Set microseconds to microseconds modulo 1000.
|
||||
microseconds %= 1000f64; |
||||
|
||||
// e. Set seconds to floor(milliseconds / 1000).
|
||||
seconds = (milliseconds / 1000f64).floor(); |
||||
// f. Set milliseconds to milliseconds modulo 1000.
|
||||
milliseconds %= 1000f64; |
||||
|
||||
// g. Set minutes to floor(seconds / 60).
|
||||
minutes = (seconds / 60f64).floor(); |
||||
// h. Set seconds to seconds modulo 60.
|
||||
seconds %= 60f64; |
||||
|
||||
// i. Set hours to floor(minutes / 60).
|
||||
hours = (minutes / 60f64).floor(); |
||||
// j. Set minutes to minutes modulo 60.
|
||||
minutes %= 60f64; |
||||
|
||||
// k. Set days to floor(hours / 24).
|
||||
days = (hours / 24f64).floor(); |
||||
// l. Set hours to hours modulo 24.
|
||||
hours %= 24f64; |
||||
} |
||||
// 10. Else if largestUnit is "minute", then
|
||||
TemporalUnit::Minute => { |
||||
// a. Set microseconds to floor(nanoseconds / 1000).
|
||||
// b. Set nanoseconds to nanoseconds modulo 1000.
|
||||
microseconds = (nanoseconds / 1000f64).floor(); |
||||
nanoseconds %= 1000f64; |
||||
|
||||
// c. Set milliseconds to floor(microseconds / 1000).
|
||||
// d. Set microseconds to microseconds modulo 1000.
|
||||
milliseconds = (microseconds / 1000f64).floor(); |
||||
microseconds %= 1000f64; |
||||
|
||||
// e. Set seconds to floor(milliseconds / 1000).
|
||||
// f. Set milliseconds to milliseconds modulo 1000.
|
||||
seconds = (milliseconds / 1000f64).floor(); |
||||
milliseconds %= 1000f64; |
||||
|
||||
// g. Set minutes to floor(seconds / 60).
|
||||
// h. Set seconds to seconds modulo 60.
|
||||
minutes = (seconds / 60f64).floor(); |
||||
seconds %= 60f64; |
||||
} |
||||
// 11. Else if largestUnit is "second", then
|
||||
TemporalUnit::Second => { |
||||
// a. Set microseconds to floor(nanoseconds / 1000).
|
||||
// b. Set nanoseconds to nanoseconds modulo 1000.
|
||||
microseconds = (nanoseconds / 1000f64).floor(); |
||||
nanoseconds %= 1000f64; |
||||
|
||||
// c. Set milliseconds to floor(microseconds / 1000).
|
||||
// d. Set microseconds to microseconds modulo 1000.
|
||||
milliseconds = (microseconds / 1000f64).floor(); |
||||
microseconds %= 1000f64; |
||||
|
||||
// e. Set seconds to floor(milliseconds / 1000).
|
||||
// f. Set milliseconds to milliseconds modulo 1000.
|
||||
seconds = (milliseconds / 1000f64).floor(); |
||||
milliseconds %= 1000f64; |
||||
} |
||||
// 12. Else if largestUnit is "millisecond", then
|
||||
TemporalUnit::Millisecond => { |
||||
// a. Set microseconds to floor(nanoseconds / 1000).
|
||||
// b. Set nanoseconds to nanoseconds modulo 1000.
|
||||
microseconds = (nanoseconds / 1000f64).floor(); |
||||
nanoseconds %= 1000f64; |
||||
|
||||
// c. Set milliseconds to floor(microseconds / 1000).
|
||||
// d. Set microseconds to microseconds modulo 1000.
|
||||
milliseconds = (microseconds / 1000f64).floor(); |
||||
microseconds %= 1000f64; |
||||
} |
||||
// 13. Else if largestUnit is "microsecond", then
|
||||
TemporalUnit::Microsecond => { |
||||
// a. Set microseconds to floor(nanoseconds / 1000).
|
||||
// b. Set nanoseconds to nanoseconds modulo 1000.
|
||||
microseconds = (nanoseconds / 1000f64).floor(); |
||||
nanoseconds %= 1000f64; |
||||
} |
||||
// 14. Else,
|
||||
// a. Assert: largestUnit is "nanosecond".
|
||||
_ => debug_assert!(largest_unit == TemporalUnit::Nanosecond), |
||||
} |
||||
|
||||
let result_values = Vec::from(&[ |
||||
days, |
||||
hours, |
||||
minutes, |
||||
seconds, |
||||
milliseconds, |
||||
microseconds, |
||||
nanoseconds, |
||||
]); |
||||
// 15. For each value v of « days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds », do
|
||||
for value in result_values { |
||||
// a. If 𝔽(v) is not finite, then
|
||||
if !value.is_finite() { |
||||
// i. If sign = 1, then
|
||||
if sign == 1 { |
||||
// 1. Return positive overflow.
|
||||
return Ok((f64::INFINITY, None)); |
||||
} |
||||
// ii. Else if sign = -1, then
|
||||
// 1. Return negative overflow.
|
||||
return Ok((f64::NEG_INFINITY, None)); |
||||
} |
||||
} |
||||
|
||||
let sign = f64::from(sign); |
||||
|
||||
// 16. Return ? CreateTimeDurationRecord(days, hours × sign, minutes × sign, seconds × sign, milliseconds × sign, microseconds × sign, nanoseconds × sign).
|
||||
let result = Self::new( |
||||
hours * sign, |
||||
minutes * sign, |
||||
seconds * sign, |
||||
milliseconds * sign, |
||||
microseconds * sign, |
||||
nanoseconds * sign, |
||||
)?; |
||||
|
||||
Ok((days, Some(result))) |
||||
} |
||||
} |
||||
|
||||
// ==== TimeDuration's public API ====
|
||||
|
||||
impl TimeDuration { |
||||
/// Creates a new validated `TimeDuration`.
|
||||
pub fn new( |
||||
hours: f64, |
||||
minutes: f64, |
||||
seconds: f64, |
||||
milliseconds: f64, |
||||
microseconds: f64, |
||||
nanoseconds: f64, |
||||
) -> TemporalResult<Self> { |
||||
let result = Self::new_unchecked( |
||||
hours, |
||||
minutes, |
||||
seconds, |
||||
milliseconds, |
||||
microseconds, |
||||
nanoseconds, |
||||
); |
||||
if !is_valid_duration(&result.into_iter().collect()) { |
||||
return Err( |
||||
TemporalError::range().with_message("Attempted to create an invalid TimeDuration.") |
||||
); |
||||
} |
||||
Ok(result) |
||||
} |
||||
|
||||
/// Creates a partial `TimeDuration` with all values set to `NaN`.
|
||||
#[must_use] |
||||
pub const fn partial() -> Self { |
||||
Self { |
||||
hours: f64::NAN, |
||||
minutes: f64::NAN, |
||||
seconds: f64::NAN, |
||||
milliseconds: f64::NAN, |
||||
microseconds: f64::NAN, |
||||
nanoseconds: f64::NAN, |
||||
} |
||||
} |
||||
|
||||
/// Creates a `TimeDuration` from a provided partial `TimeDuration`.
|
||||
#[must_use] |
||||
pub fn from_partial(partial: &TimeDuration) -> Self { |
||||
Self { |
||||
hours: if partial.hours.is_nan() { |
||||
0.0 |
||||
} else { |
||||
partial.hours |
||||
}, |
||||
minutes: if partial.minutes.is_nan() { |
||||
0.0 |
||||
} else { |
||||
partial.minutes |
||||
}, |
||||
seconds: if partial.seconds.is_nan() { |
||||
0.0 |
||||
} else { |
||||
partial.seconds |
||||
}, |
||||
milliseconds: if partial.milliseconds.is_nan() { |
||||
0.0 |
||||
} else { |
||||
partial.milliseconds |
||||
}, |
||||
microseconds: if partial.microseconds.is_nan() { |
||||
0.0 |
||||
} else { |
||||
partial.microseconds |
||||
}, |
||||
nanoseconds: if partial.nanoseconds.is_nan() { |
||||
0.0 |
||||
} else { |
||||
partial.nanoseconds |
||||
}, |
||||
} |
||||
} |
||||
|
||||
/// Returns a new `TimeDuration` representing the absolute value of the current.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn abs(&self) -> Self { |
||||
Self { |
||||
hours: self.hours.abs(), |
||||
minutes: self.minutes.abs(), |
||||
seconds: self.seconds.abs(), |
||||
milliseconds: self.milliseconds.abs(), |
||||
microseconds: self.microseconds.abs(), |
||||
nanoseconds: self.nanoseconds.abs(), |
||||
} |
||||
} |
||||
|
||||
/// Returns a negated `TimeDuration`.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn neg(&self) -> Self { |
||||
Self { |
||||
hours: self.hours * -1f64, |
||||
minutes: self.minutes * -1f64, |
||||
seconds: self.seconds * -1f64, |
||||
milliseconds: self.milliseconds * -1f64, |
||||
microseconds: self.microseconds * -1f64, |
||||
nanoseconds: self.nanoseconds * -1f64, |
||||
} |
||||
} |
||||
|
||||
/// Balances a `TimeDuration` given a day value and the largest unit. `balance` will return
|
||||
/// the balanced `day` and `TimeDuration`.
|
||||
///
|
||||
/// # Errors:
|
||||
/// - Will error if provided duration is invalid
|
||||
pub fn balance(&self, days: f64, largest_unit: TemporalUnit) -> TemporalResult<(f64, Self)> { |
||||
let result = Self::balance_possibly_infinite_time_duration( |
||||
days, |
||||
self.hours, |
||||
self.minutes, |
||||
self.seconds, |
||||
self.milliseconds, |
||||
self.microseconds, |
||||
self.nanoseconds, |
||||
largest_unit, |
||||
)?; |
||||
let Some(time_duration) = result.1 else { |
||||
return Err(TemporalError::range().with_message("Invalid balance TimeDuration.")); |
||||
}; |
||||
Ok((result.0, time_duration)) |
||||
} |
||||
|
||||
/// Utility function for returning if values in a valid range.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn is_within_range(&self) -> bool { |
||||
self.hours.abs() < 24f64 |
||||
&& self.minutes.abs() < 60f64 |
||||
&& self.seconds.abs() < 60f64 |
||||
&& self.milliseconds.abs() < 1000f64 |
||||
&& self.milliseconds.abs() < 1000f64 |
||||
&& self.milliseconds.abs() < 1000f64 |
||||
} |
||||
|
||||
/// Returns the `[[hours]]` value.
|
||||
#[must_use] |
||||
pub const fn hours(&self) -> f64 { |
||||
self.hours |
||||
} |
||||
|
||||
/// Returns the `[[minutes]]` value.
|
||||
#[must_use] |
||||
pub const fn minutes(&self) -> f64 { |
||||
self.minutes |
||||
} |
||||
|
||||
/// Returns the `[[seconds]]` value.
|
||||
#[must_use] |
||||
pub const fn seconds(&self) -> f64 { |
||||
self.seconds |
||||
} |
||||
|
||||
/// Returns the `[[milliseconds]]` value.
|
||||
#[must_use] |
||||
pub const fn milliseconds(&self) -> f64 { |
||||
self.milliseconds |
||||
} |
||||
|
||||
/// Returns the `[[microseconds]]` value.
|
||||
#[must_use] |
||||
pub const fn microseconds(&self) -> f64 { |
||||
self.microseconds |
||||
} |
||||
|
||||
/// Returns the `[[nanoseconds]]` value.
|
||||
#[must_use] |
||||
pub const fn nanoseconds(&self) -> f64 { |
||||
self.nanoseconds |
||||
} |
||||
|
||||
/// Returns the `TimeDuration`'s iterator.
|
||||
#[must_use] |
||||
pub fn iter(&self) -> TimeIter<'_> { |
||||
<&Self as IntoIterator>::into_iter(self) |
||||
} |
||||
} |
||||
|
||||
// ==== TimeDuration method impls ====
|
||||
|
||||
impl TimeDuration { |
||||
/// Rounds the current `TimeDuration` given a rounding increment, unit and rounding mode. `round` will return a tuple of the rounded `TimeDuration` and
|
||||
/// the `total` value of the smallest unit prior to rounding.
|
||||
#[inline] |
||||
pub fn round( |
||||
&self, |
||||
increment: f64, |
||||
unit: TemporalUnit, |
||||
rounding_mode: TemporalRoundingMode, |
||||
) -> TemporalResult<(Self, f64)> { |
||||
let fraction_seconds = match unit { |
||||
TemporalUnit::Year |
||||
| TemporalUnit::Month |
||||
| TemporalUnit::Week |
||||
| TemporalUnit::Day |
||||
| TemporalUnit::Auto => { |
||||
return Err(TemporalError::r#type() |
||||
.with_message("Invalid unit provided to for TimeDuration to round.")) |
||||
} |
||||
_ => self.nanoseconds().mul_add( |
||||
1_000_000_000f64, |
||||
self.microseconds().mul_add( |
||||
1_000_000f64, |
||||
self.milliseconds().mul_add(1000f64, self.seconds()), |
||||
), |
||||
), |
||||
}; |
||||
|
||||
match unit { |
||||
// 12. Else if unit is "hour", then
|
||||
TemporalUnit::Hour => { |
||||
// a. Let fractionalHours be (fractionalSeconds / 60 + minutes) / 60 + hours.
|
||||
let frac_hours = (fraction_seconds / 60f64 + self.minutes) / 60f64 + self.hours; |
||||
// b. Set hours to RoundNumberToIncrement(fractionalHours, increment, roundingMode).
|
||||
let rounded_hours = |
||||
utils::round_number_to_increment(frac_hours, increment, rounding_mode); |
||||
// c. Set total to fractionalHours.
|
||||
// d. Set minutes, seconds, milliseconds, microseconds, and nanoseconds to 0.
|
||||
let result = Self::new(rounded_hours, 0f64, 0f64, 0f64, 0f64, 0f64)?; |
||||
Ok((result, frac_hours)) |
||||
} |
||||
// 13. Else if unit is "minute", then
|
||||
TemporalUnit::Minute => { |
||||
// a. Let fractionalMinutes be fractionalSeconds / 60 + minutes.
|
||||
let frac_minutes = fraction_seconds / 60f64 + self.minutes; |
||||
// b. Set minutes to RoundNumberToIncrement(fractionalMinutes, increment, roundingMode).
|
||||
let rounded_minutes = |
||||
utils::round_number_to_increment(frac_minutes, increment, rounding_mode); |
||||
// c. Set total to fractionalMinutes.
|
||||
// d. Set seconds, milliseconds, microseconds, and nanoseconds to 0.
|
||||
let result = Self::new(self.hours, rounded_minutes, 0f64, 0f64, 0f64, 0f64)?; |
||||
|
||||
Ok((result, frac_minutes)) |
||||
} |
||||
// 14. Else if unit is "second", then
|
||||
TemporalUnit::Second => { |
||||
// a. Set seconds to RoundNumberToIncrement(fractionalSeconds, increment, roundingMode).
|
||||
let rounded_seconds = |
||||
utils::round_number_to_increment(fraction_seconds, increment, rounding_mode); |
||||
// b. Set total to fractionalSeconds.
|
||||
// c. Set milliseconds, microseconds, and nanoseconds to 0.
|
||||
let result = |
||||
Self::new(self.hours, self.minutes, rounded_seconds, 0f64, 0f64, 0f64)?; |
||||
|
||||
Ok((result, fraction_seconds)) |
||||
} |
||||
// 15. Else if unit is "millisecond", then
|
||||
TemporalUnit::Millisecond => { |
||||
// a. Let fractionalMilliseconds be nanoseconds × 10-6 + microseconds × 10-3 + milliseconds.
|
||||
let fraction_millis = self.nanoseconds.mul_add( |
||||
1_000_000f64, |
||||
self.microseconds.mul_add(1_000f64, self.milliseconds), |
||||
); |
||||
|
||||
// b. Set milliseconds to RoundNumberToIncrement(fractionalMilliseconds, increment, roundingMode).
|
||||
let rounded_millis = |
||||
utils::round_number_to_increment(fraction_millis, increment, rounding_mode); |
||||
|
||||
// c. Set total to fractionalMilliseconds.
|
||||
// d. Set microseconds and nanoseconds to 0.
|
||||
let result = Self::new( |
||||
self.hours, |
||||
self.minutes, |
||||
self.seconds, |
||||
rounded_millis, |
||||
0f64, |
||||
0f64, |
||||
)?; |
||||
Ok((result, fraction_millis)) |
||||
} |
||||
// 16. Else if unit is "microsecond", then
|
||||
TemporalUnit::Microsecond => { |
||||
// a. Let fractionalMicroseconds be nanoseconds × 10-3 + microseconds.
|
||||
let frac_micros = self.nanoseconds.mul_add(1_000f64, self.microseconds); |
||||
|
||||
// b. Set microseconds to RoundNumberToIncrement(fractionalMicroseconds, increment, roundingMode).
|
||||
let rounded_micros = |
||||
utils::round_number_to_increment(frac_micros, increment, rounding_mode); |
||||
|
||||
// c. Set total to fractionalMicroseconds.
|
||||
// d. Set nanoseconds to 0.
|
||||
let result = Self::new( |
||||
self.hours, |
||||
self.minutes, |
||||
self.seconds, |
||||
self.milliseconds, |
||||
rounded_micros, |
||||
0f64, |
||||
)?; |
||||
Ok((result, frac_micros)) |
||||
} |
||||
// 17. Else,
|
||||
TemporalUnit::Nanosecond => { |
||||
// a. Assert: unit is "nanosecond".
|
||||
// b. Set total to nanoseconds.
|
||||
let total = self.nanoseconds; |
||||
// c. Set nanoseconds to RoundNumberToIncrement(nanoseconds, increment, roundingMode).
|
||||
let rounded_nanos = |
||||
utils::round_number_to_increment(self.nanoseconds, increment, rounding_mode); |
||||
|
||||
let result = Self::new( |
||||
self.hours, |
||||
self.minutes, |
||||
self.seconds, |
||||
self.milliseconds, |
||||
self.microseconds, |
||||
rounded_nanos, |
||||
)?; |
||||
|
||||
Ok((result, total)) |
||||
} |
||||
_ => unreachable!("All other units early return error."), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl<'a> IntoIterator for &'a TimeDuration { |
||||
type Item = f64; |
||||
type IntoIter = TimeIter<'a>; |
||||
|
||||
fn into_iter(self) -> Self::IntoIter { |
||||
TimeIter { |
||||
time: self, |
||||
index: 0, |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// An iterator over a `TimeDuration`.
|
||||
#[derive(Debug, Clone)] |
||||
pub struct TimeIter<'a> { |
||||
time: &'a TimeDuration, |
||||
index: usize, |
||||
} |
||||
|
||||
impl Iterator for TimeIter<'_> { |
||||
type Item = f64; |
||||
|
||||
fn next(&mut self) -> Option<Self::Item> { |
||||
let result = match self.index { |
||||
0 => Some(self.time.hours), |
||||
1 => Some(self.time.minutes), |
||||
2 => Some(self.time.seconds), |
||||
3 => Some(self.time.milliseconds), |
||||
4 => Some(self.time.microseconds), |
||||
5 => Some(self.time.nanoseconds), |
||||
_ => None, |
||||
}; |
||||
self.index += 1; |
||||
result |
||||
} |
||||
} |
@ -1,318 +0,0 @@
|
||||
//! An implementation of the Temporal Instant.
|
||||
|
||||
use crate::{ |
||||
components::{duration::TimeDuration, Duration}, |
||||
options::{TemporalRoundingMode, TemporalUnit}, |
||||
utils, TemporalError, TemporalResult, MS_PER_DAY, NS_PER_DAY, |
||||
}; |
||||
|
||||
use num_bigint::BigInt; |
||||
use num_traits::{FromPrimitive, ToPrimitive}; |
||||
|
||||
const NANOSECONDS_PER_SECOND: f64 = 1e9; |
||||
const NANOSECONDS_PER_MINUTE: f64 = 60f64 * NANOSECONDS_PER_SECOND; |
||||
const NANOSECONDS_PER_HOUR: f64 = 60f64 * NANOSECONDS_PER_MINUTE; |
||||
|
||||
/// The native Rust implementation of `Temporal.Instant`
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] |
||||
pub struct Instant { |
||||
pub(crate) nanos: BigInt, |
||||
} |
||||
|
||||
// ==== Private API ====
|
||||
|
||||
impl Instant { |
||||
/// Adds a `TimeDuration` to the current `Instant`.
|
||||
///
|
||||
/// Temporal-Proposal equivalent: `AddDurationToOrSubtractDurationFrom`.
|
||||
pub(crate) fn add_to_instant(&self, duration: &TimeDuration) -> TemporalResult<Self> { |
||||
let result = self.epoch_nanoseconds() |
||||
+ duration.nanoseconds |
||||
+ (duration.microseconds * 1000f64) |
||||
+ (duration.milliseconds * 1_000_000f64) |
||||
+ (duration.seconds * NANOSECONDS_PER_SECOND) |
||||
+ (duration.minutes * NANOSECONDS_PER_MINUTE) |
||||
+ (duration.hours * NANOSECONDS_PER_HOUR); |
||||
let nanos = BigInt::from_f64(result).ok_or_else(|| { |
||||
TemporalError::range().with_message("Duration added to instant exceeded valid range.") |
||||
})?; |
||||
Self::new(nanos) |
||||
} |
||||
|
||||
// TODO: Add test for `diff_instant`.
|
||||
// NOTE(nekevss): As the below is internal, op will be left as a boolean
|
||||
// with a `since` op being true and `until` being false.
|
||||
/// Internal operation to handle `since` and `until` difference ops.
|
||||
#[allow(unused)] |
||||
pub(crate) fn diff_instant( |
||||
&self, |
||||
op: bool, |
||||
other: &Self, |
||||
rounding_mode: Option<TemporalRoundingMode>, |
||||
rounding_increment: Option<f64>, |
||||
largest_unit: Option<TemporalUnit>, |
||||
smallest_unit: Option<TemporalUnit>, |
||||
) -> TemporalResult<TimeDuration> { |
||||
// diff the instant and determine its component values.
|
||||
let diff = self.to_f64() - other.to_f64(); |
||||
let nanos = diff.rem_euclid(1000f64); |
||||
let micros = (diff / 1000f64).trunc().rem_euclid(1000f64); |
||||
let millis = (diff / 1_000_000f64).trunc().rem_euclid(1000f64); |
||||
let secs = (diff / NANOSECONDS_PER_SECOND).trunc(); |
||||
|
||||
// Handle the settings provided to `diff_instant`
|
||||
let rounding_increment = rounding_increment.unwrap_or(1.0); |
||||
let rounding_mode = if op { |
||||
rounding_mode |
||||
.unwrap_or(TemporalRoundingMode::Trunc) |
||||
.negate() |
||||
} else { |
||||
rounding_mode.unwrap_or(TemporalRoundingMode::Trunc) |
||||
}; |
||||
let smallest_unit = smallest_unit.unwrap_or(TemporalUnit::Nanosecond); |
||||
// Use the defaultlargestunit which is max smallestlargestdefault and smallestunit
|
||||
let largest_unit = largest_unit.unwrap_or(smallest_unit.max(TemporalUnit::Second)); |
||||
|
||||
// TODO: validate roundingincrement
|
||||
// Steps 11-13 of 13.47 GetDifferenceSettings
|
||||
|
||||
if smallest_unit == TemporalUnit::Nanosecond { |
||||
let (_, result) = TimeDuration::new_unchecked(0f64, 0f64, secs, millis, micros, nanos) |
||||
.balance(0f64, largest_unit)?; |
||||
return Ok(result); |
||||
} |
||||
|
||||
let (round_result, _) = TimeDuration::new(0f64, 0f64, secs, millis, micros, nanos)?.round( |
||||
rounding_increment, |
||||
smallest_unit, |
||||
rounding_mode, |
||||
)?; |
||||
let (_, result) = round_result.balance(0f64, largest_unit)?; |
||||
Ok(result) |
||||
} |
||||
|
||||
/// Rounds a current `Instant` given the resolved options, returning a `BigInt` result.
|
||||
pub(crate) fn round_instant( |
||||
&self, |
||||
increment: f64, |
||||
unit: TemporalUnit, |
||||
rounding_mode: TemporalRoundingMode, |
||||
) -> TemporalResult<BigInt> { |
||||
let increment_nanos = match unit { |
||||
TemporalUnit::Hour => increment * NANOSECONDS_PER_HOUR, |
||||
TemporalUnit::Minute => increment * NANOSECONDS_PER_MINUTE, |
||||
TemporalUnit::Second => increment * NANOSECONDS_PER_SECOND, |
||||
TemporalUnit::Millisecond => increment * 1_000_000f64, |
||||
TemporalUnit::Microsecond => increment * 1_000f64, |
||||
TemporalUnit::Nanosecond => increment, |
||||
_ => { |
||||
return Err(TemporalError::range() |
||||
.with_message("Invalid unit provided for Instant::round.")) |
||||
} |
||||
}; |
||||
|
||||
let rounded = utils::round_number_to_increment_as_if_positive( |
||||
self.to_f64(), |
||||
increment_nanos, |
||||
rounding_mode, |
||||
); |
||||
|
||||
BigInt::from_f64(rounded) |
||||
.ok_or_else(|| TemporalError::range().with_message("Invalid rounded Instant value.")) |
||||
} |
||||
|
||||
/// Utility for converting `Instant` to f64.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function will panic if called on an invalid `Instant`.
|
||||
pub(crate) fn to_f64(&self) -> f64 { |
||||
self.nanos |
||||
.to_f64() |
||||
.expect("A valid instant is representable by f64.") |
||||
} |
||||
} |
||||
|
||||
// ==== 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 }) |
||||
} |
||||
|
||||
/// Adds a `Duration` to the current `Instant`, returning an error if the `Duration`
|
||||
/// contains a `DateDuration`.
|
||||
#[inline] |
||||
pub fn add(&self, duration: Duration) -> TemporalResult<Self> { |
||||
if !duration.is_time_duration() { |
||||
return Err(TemporalError::range() |
||||
.with_message("DateDuration values cannot be added to instant.")); |
||||
} |
||||
self.add_time_duration(duration.time()) |
||||
} |
||||
|
||||
/// Adds a `TimeDuration` to `Instant`.
|
||||
#[inline] |
||||
pub fn add_time_duration(&self, duration: &TimeDuration) -> TemporalResult<Self> { |
||||
self.add_to_instant(duration) |
||||
} |
||||
|
||||
/// Subtract a `Duration` to the current `Instant`, returning an error if the `Duration`
|
||||
/// contains a `DateDuration`.
|
||||
#[inline] |
||||
pub fn subtract(&self, duration: Duration) -> TemporalResult<Self> { |
||||
if !duration.is_time_duration() { |
||||
return Err(TemporalError::range() |
||||
.with_message("DateDuration values cannot be added to instant.")); |
||||
} |
||||
self.subtract_time_duration(duration.time()) |
||||
} |
||||
|
||||
/// Subtracts a `TimeDuration` to `Instant`.
|
||||
#[inline] |
||||
pub fn subtract_time_duration(&self, duration: &TimeDuration) -> TemporalResult<Self> { |
||||
self.add_to_instant(&duration.neg()) |
||||
} |
||||
|
||||
/// Returns a `TimeDuration` representing the duration since provided `Instant`
|
||||
#[inline] |
||||
pub fn since( |
||||
&self, |
||||
other: &Self, |
||||
rounding_mode: Option<TemporalRoundingMode>, |
||||
rounding_increment: Option<f64>, |
||||
largest_unit: Option<TemporalUnit>, |
||||
smallest_unit: Option<TemporalUnit>, |
||||
) -> TemporalResult<TimeDuration> { |
||||
self.diff_instant( |
||||
true, |
||||
other, |
||||
rounding_mode, |
||||
rounding_increment, |
||||
largest_unit, |
||||
smallest_unit, |
||||
) |
||||
} |
||||
|
||||
/// Returns a `TimeDuration` representing the duration until provided `Instant`
|
||||
#[inline] |
||||
pub fn until( |
||||
&self, |
||||
other: &Self, |
||||
rounding_mode: Option<TemporalRoundingMode>, |
||||
rounding_increment: Option<f64>, |
||||
largest_unit: Option<TemporalUnit>, |
||||
smallest_unit: Option<TemporalUnit>, |
||||
) -> TemporalResult<TimeDuration> { |
||||
self.diff_instant( |
||||
false, |
||||
other, |
||||
rounding_mode, |
||||
rounding_increment, |
||||
largest_unit, |
||||
smallest_unit, |
||||
) |
||||
} |
||||
|
||||
/// Returns an `Instant` by rounding the current `Instant` according to the provided settings.
|
||||
pub fn round( |
||||
&self, |
||||
increment: Option<f64>, |
||||
unit: TemporalUnit, // smallestUnit is required on Instant::round
|
||||
rounding_mode: Option<TemporalRoundingMode>, |
||||
) -> TemporalResult<Self> { |
||||
let increment = utils::to_rounding_increment(increment)?; |
||||
let mode = rounding_mode.unwrap_or(TemporalRoundingMode::HalfExpand); |
||||
let maximum = match unit { |
||||
TemporalUnit::Hour => 24u64, |
||||
TemporalUnit::Minute => 24 * 60, |
||||
TemporalUnit::Second => 24 * 3600, |
||||
TemporalUnit::Millisecond => MS_PER_DAY as u64, |
||||
TemporalUnit::Microsecond => MS_PER_DAY as u64 * 1000, |
||||
TemporalUnit::Nanosecond => NS_PER_DAY as u64, |
||||
_ => return Err(TemporalError::range().with_message("Invalid roundTo unit provided.")), |
||||
}; |
||||
// NOTE: to_rounding_increment returns an f64 within a u32 range.
|
||||
utils::validate_temporal_rounding_increment(increment as u32, maximum, true)?; |
||||
|
||||
let round_result = self.round_instant(increment, unit, mode)?; |
||||
Self::new(round_result) |
||||
} |
||||
|
||||
/// 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.to_f64() |
||||
} |
||||
} |
||||
|
||||
// ==== Utility Functions ====
|
||||
|
||||
/// 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) |
||||
} |
||||
|
||||
// ==== Instant Tests ====
|
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use crate::{components::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, i.e., a valid instant is within the range of an f64.
|
||||
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()); |
||||
} |
||||
} |
@ -1,46 +0,0 @@
|
||||
//! The primary date-time components provided by Temporal.
|
||||
//!
|
||||
//! The below components are the main primitives of the `Temporal` specification:
|
||||
//! - `Date` -> `PlainDate`
|
||||
//! - `DateTime` -> `PlainDateTime`
|
||||
//! - `Time` -> `PlainTime`
|
||||
//! - `Duration` -> `Duration`
|
||||
//! - `Instant` -> `Instant`
|
||||
//! - `MonthDay` -> `PlainMonthDay`
|
||||
//! - `YearMonth` -> `PlainYearMonth`
|
||||
//! - `ZonedDateTime` -> `ZonedDateTime`
|
||||
//!
|
||||
//! The Temporal specification, along with this implementation aims to provide
|
||||
//! full support for time zones and non-gregorian calendars that are compliant
|
||||
//! with standards like ISO 8601, RFC 3339, and RFC 5545.
|
||||
|
||||
// TODO: Expand upon above introduction.
|
||||
|
||||
pub mod calendar; |
||||
pub mod duration; |
||||
pub mod tz; |
||||
|
||||
mod date; |
||||
mod datetime; |
||||
mod instant; |
||||
mod month_day; |
||||
mod time; |
||||
mod year_month; |
||||
mod zoneddatetime; |
||||
|
||||
#[doc(inline)] |
||||
pub use date::Date; |
||||
#[doc(inline)] |
||||
pub use datetime::DateTime; |
||||
#[doc(inline)] |
||||
pub use duration::Duration; |
||||
#[doc(inline)] |
||||
pub use instant::Instant; |
||||
#[doc(inline)] |
||||
pub use month_day::MonthDay; |
||||
#[doc(inline)] |
||||
pub use time::Time; |
||||
#[doc(inline)] |
||||
pub use year_month::YearMonth; |
||||
#[doc(inline)] |
||||
pub use zoneddatetime::ZonedDateTime; |
@ -1,92 +0,0 @@
|
||||
//! This module implements `MonthDay` and any directly related algorithms.
|
||||
|
||||
use std::str::FromStr; |
||||
|
||||
use crate::{ |
||||
components::calendar::CalendarSlot, |
||||
iso::{IsoDate, IsoDateSlots}, |
||||
options::ArithmeticOverflow, |
||||
TemporalError, TemporalResult, |
||||
}; |
||||
|
||||
use super::calendar::{CalendarProtocol, GetCalendarSlot}; |
||||
|
||||
/// The native Rust implementation of `Temporal.PlainMonthDay`
|
||||
#[derive(Debug, Default, Clone)] |
||||
pub struct MonthDay<C: CalendarProtocol> { |
||||
iso: IsoDate, |
||||
calendar: CalendarSlot<C>, |
||||
} |
||||
|
||||
impl<C: CalendarProtocol> MonthDay<C> { |
||||
/// Creates a new unchecked `MonthDay`
|
||||
#[inline] |
||||
#[must_use] |
||||
pub(crate) fn new_unchecked(iso: IsoDate, calendar: CalendarSlot<C>) -> Self { |
||||
Self { iso, calendar } |
||||
} |
||||
|
||||
/// Creates a new valid `MonthDay`.
|
||||
#[inline] |
||||
pub fn new( |
||||
month: i32, |
||||
day: i32, |
||||
calendar: CalendarSlot<C>, |
||||
overflow: ArithmeticOverflow, |
||||
) -> TemporalResult<Self> { |
||||
let iso = IsoDate::new(1972, month, day, overflow)?; |
||||
Ok(Self::new_unchecked(iso, calendar)) |
||||
} |
||||
|
||||
/// Returns the `month` value of `MonthDay`.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn month(&self) -> u8 { |
||||
self.iso.month |
||||
} |
||||
|
||||
/// Returns the `day` value of `MonthDay`.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn day(&self) -> u8 { |
||||
self.iso.day |
||||
} |
||||
|
||||
/// Returns a reference to `MonthDay`'s `CalendarSlot`
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn calendar(&self) -> &CalendarSlot<C> { |
||||
&self.calendar |
||||
} |
||||
} |
||||
|
||||
impl<C: CalendarProtocol> GetCalendarSlot<C> for MonthDay<C> { |
||||
fn get_calendar(&self) -> CalendarSlot<C> { |
||||
self.calendar.clone() |
||||
} |
||||
} |
||||
|
||||
impl<C: CalendarProtocol> IsoDateSlots for MonthDay<C> { |
||||
#[inline] |
||||
/// Returns this structs `IsoDate`.
|
||||
fn iso_date(&self) -> IsoDate { |
||||
self.iso |
||||
} |
||||
} |
||||
|
||||
impl<C: CalendarProtocol> FromStr for MonthDay<C> { |
||||
type Err = TemporalError; |
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
let record = crate::parser::parse_month_day(s)?; |
||||
|
||||
let calendar = record.calendar.unwrap_or("iso8601".into()); |
||||
|
||||
Self::new( |
||||
record.date.month, |
||||
record.date.day, |
||||
CalendarSlot::from_str(&calendar)?, |
||||
ArithmeticOverflow::Reject, |
||||
) |
||||
} |
||||
} |
@ -1,274 +0,0 @@
|
||||
//! This module implements `Time` and any directly related algorithms.
|
||||
|
||||
use crate::{ |
||||
components::{duration::TimeDuration, Duration}, |
||||
iso::IsoTime, |
||||
options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit}, |
||||
utils, TemporalError, TemporalResult, |
||||
}; |
||||
|
||||
/// The native Rust implementation of `Temporal.PlainTime`.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] |
||||
pub struct Time { |
||||
iso: IsoTime, |
||||
} |
||||
|
||||
// ==== Private API ====
|
||||
|
||||
impl Time { |
||||
#[inline] |
||||
#[must_use] |
||||
pub(crate) fn new_unchecked(iso: IsoTime) -> Self { |
||||
Self { iso } |
||||
} |
||||
|
||||
/// Returns true if a valid `Time`.
|
||||
#[allow(dead_code)] |
||||
pub(crate) fn is_valid(&self) -> bool { |
||||
self.iso.is_valid() |
||||
} |
||||
|
||||
/// Adds a `TimeDuration` to the current `Time`.
|
||||
///
|
||||
/// Spec Equivalent: `AddDurationToOrSubtractDurationFromPlainTime` AND `AddTime`.
|
||||
pub(crate) fn add_to_time(&self, duration: &TimeDuration) -> Self { |
||||
let (_, result) = IsoTime::balance( |
||||
f64::from(self.hour()) + duration.hours(), |
||||
f64::from(self.minute()) + duration.minutes(), |
||||
f64::from(self.second()) + duration.seconds(), |
||||
f64::from(self.millisecond()) + duration.milliseconds(), |
||||
f64::from(self.microsecond()) + duration.microseconds(), |
||||
f64::from(self.nanosecond()) + duration.nanoseconds(), |
||||
); |
||||
|
||||
// NOTE (nekevss): IsoTime::balance should never return an invalid `IsoTime`
|
||||
|
||||
Self::new_unchecked(result) |
||||
} |
||||
} |
||||
|
||||
// ==== Public API ====
|
||||
|
||||
impl Time { |
||||
/// Creates a new `IsoTime` value.
|
||||
pub fn new( |
||||
hour: i32, |
||||
minute: i32, |
||||
second: i32, |
||||
millisecond: i32, |
||||
microsecond: i32, |
||||
nanosecond: i32, |
||||
overflow: ArithmeticOverflow, |
||||
) -> TemporalResult<Self> { |
||||
let time = IsoTime::new( |
||||
hour, |
||||
minute, |
||||
second, |
||||
millisecond, |
||||
microsecond, |
||||
nanosecond, |
||||
overflow, |
||||
)?; |
||||
Ok(Self::new_unchecked(time)) |
||||
} |
||||
|
||||
/// Returns the internal `hour` field.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub const fn hour(&self) -> u8 { |
||||
self.iso.hour |
||||
} |
||||
|
||||
/// Returns the internal `minute` field.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub const fn minute(&self) -> u8 { |
||||
self.iso.minute |
||||
} |
||||
|
||||
/// Returns the internal `second` field.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub const fn second(&self) -> u8 { |
||||
self.iso.second |
||||
} |
||||
|
||||
/// Returns the internal `millisecond` field.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub const fn millisecond(&self) -> u16 { |
||||
self.iso.millisecond |
||||
} |
||||
|
||||
/// Returns the internal `microsecond` field.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub const fn microsecond(&self) -> u16 { |
||||
self.iso.microsecond |
||||
} |
||||
|
||||
/// Returns the internal `nanosecond` field.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub const fn nanosecond(&self) -> u16 { |
||||
self.iso.nanosecond |
||||
} |
||||
|
||||
/// Add a `Duration` to the current `Time`.
|
||||
pub fn add(&self, duration: &Duration) -> TemporalResult<Self> { |
||||
if !duration.is_time_duration() { |
||||
return Err(TemporalError::range() |
||||
.with_message("DateDuration values cannot be added to `Time`.")); |
||||
} |
||||
Ok(self.add_time_duration(duration.time())) |
||||
} |
||||
|
||||
/// Adds a `TimeDuration` to the current `Time`.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn add_time_duration(&self, duration: &TimeDuration) -> Self { |
||||
self.add_to_time(duration) |
||||
} |
||||
|
||||
/// Subtract a `Duration` to the current `Time`.
|
||||
pub fn subtract(&self, duration: &Duration) -> TemporalResult<Self> { |
||||
if !duration.is_time_duration() { |
||||
return Err(TemporalError::range() |
||||
.with_message("DateDuration values cannot be added to `Time` component.")); |
||||
} |
||||
Ok(self.add_time_duration(duration.time())) |
||||
} |
||||
|
||||
/// Adds a `TimeDuration` to the current `Time`.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn subtract_time_duration(&self, duration: &TimeDuration) -> Self { |
||||
self.add_to_time(&duration.neg()) |
||||
} |
||||
|
||||
// TODO (nekevss): optimize and test rounding_increment type (f64 vs. u64).
|
||||
/// Rounds the current `Time` according to provided options.
|
||||
pub fn round( |
||||
&self, |
||||
smallest_unit: TemporalUnit, |
||||
rounding_increment: Option<f64>, |
||||
rounding_mode: Option<TemporalRoundingMode>, |
||||
) -> TemporalResult<Self> { |
||||
let increment = utils::to_rounding_increment(rounding_increment)?; |
||||
let mode = rounding_mode.unwrap_or(TemporalRoundingMode::HalfExpand); |
||||
|
||||
let max = smallest_unit |
||||
.to_maximum_rounding_increment() |
||||
.ok_or_else(|| { |
||||
TemporalError::range().with_message("smallestUnit must be a time value.") |
||||
})?; |
||||
|
||||
// Safety (nekevss): to_rounding_increment returns a value in the range of a u32.
|
||||
utils::validate_temporal_rounding_increment(increment as u32, u64::from(max), false)?; |
||||
|
||||
let (_, result) = self.iso.round(increment, smallest_unit, mode, None)?; |
||||
|
||||
Ok(Self::new_unchecked(result)) |
||||
} |
||||
} |
||||
|
||||
// ==== Test land ====
|
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use crate::{components::Duration, iso::IsoTime, options::TemporalUnit}; |
||||
|
||||
use super::Time; |
||||
|
||||
fn assert_time(result: Time, values: (u8, u8, u8, u16, u16, u16)) { |
||||
assert!(result.hour() == values.0); |
||||
assert!(result.minute() == values.1); |
||||
assert!(result.second() == values.2); |
||||
assert!(result.millisecond() == values.3); |
||||
assert!(result.microsecond() == values.4); |
||||
assert!(result.nanosecond() == values.5); |
||||
} |
||||
|
||||
#[test] |
||||
fn time_round_millisecond() { |
||||
let base = Time::new_unchecked(IsoTime::new_unchecked(3, 34, 56, 987, 654, 321)); |
||||
|
||||
let result_1 = base |
||||
.round(TemporalUnit::Millisecond, Some(1.0), None) |
||||
.unwrap(); |
||||
assert_time(result_1, (3, 34, 56, 988, 0, 0)); |
||||
|
||||
let result_2 = base |
||||
.round(TemporalUnit::Millisecond, Some(2.0), None) |
||||
.unwrap(); |
||||
assert_time(result_2, (3, 34, 56, 988, 0, 0)); |
||||
|
||||
let result_3 = base |
||||
.round(TemporalUnit::Millisecond, Some(4.0), None) |
||||
.unwrap(); |
||||
assert_time(result_3, (3, 34, 56, 988, 0, 0)); |
||||
|
||||
let result_4 = base |
||||
.round(TemporalUnit::Millisecond, Some(5.0), None) |
||||
.unwrap(); |
||||
assert_time(result_4, (3, 34, 56, 990, 0, 0)); |
||||
} |
||||
|
||||
#[test] |
||||
fn time_round_microsecond() { |
||||
let base = Time::new_unchecked(IsoTime::new_unchecked(3, 34, 56, 987, 654, 321)); |
||||
|
||||
let result_1 = base |
||||
.round(TemporalUnit::Microsecond, Some(1.0), None) |
||||
.unwrap(); |
||||
assert_time(result_1, (3, 34, 56, 987, 654, 0)); |
||||
|
||||
let result_2 = base |
||||
.round(TemporalUnit::Microsecond, Some(2.0), None) |
||||
.unwrap(); |
||||
assert_time(result_2, (3, 34, 56, 987, 654, 0)); |
||||
|
||||
let result_3 = base |
||||
.round(TemporalUnit::Microsecond, Some(4.0), None) |
||||
.unwrap(); |
||||
assert_time(result_3, (3, 34, 56, 987, 656, 0)); |
||||
|
||||
let result_4 = base |
||||
.round(TemporalUnit::Microsecond, Some(5.0), None) |
||||
.unwrap(); |
||||
assert_time(result_4, (3, 34, 56, 987, 655, 0)); |
||||
} |
||||
|
||||
#[test] |
||||
fn time_round_nanoseconds() { |
||||
let base = Time::new_unchecked(IsoTime::new_unchecked(3, 34, 56, 987, 654, 321)); |
||||
|
||||
let result_1 = base |
||||
.round(TemporalUnit::Nanosecond, Some(1.0), None) |
||||
.unwrap(); |
||||
assert_time(result_1, (3, 34, 56, 987, 654, 321)); |
||||
|
||||
let result_2 = base |
||||
.round(TemporalUnit::Nanosecond, Some(2.0), None) |
||||
.unwrap(); |
||||
assert_time(result_2, (3, 34, 56, 987, 654, 322)); |
||||
|
||||
let result_3 = base |
||||
.round(TemporalUnit::Nanosecond, Some(4.0), None) |
||||
.unwrap(); |
||||
assert_time(result_3, (3, 34, 56, 987, 654, 320)); |
||||
|
||||
let result_4 = base |
||||
.round(TemporalUnit::Nanosecond, Some(5.0), None) |
||||
.unwrap(); |
||||
assert_time(result_4, (3, 34, 56, 987, 654, 320)); |
||||
} |
||||
|
||||
#[test] |
||||
fn add_duration_basic() { |
||||
let base = Time::new_unchecked(IsoTime::new_unchecked(15, 23, 30, 123, 456, 789)); |
||||
let result = base.add(&"PT16H".parse::<Duration>().unwrap()).unwrap(); |
||||
|
||||
assert_time(result, (7, 23, 30, 123, 456, 789)); |
||||
} |
||||
} |
@ -1,118 +0,0 @@
|
||||
//! This module implements the Temporal `TimeZone` and components.
|
||||
|
||||
use num_bigint::BigInt; |
||||
use num_traits::ToPrimitive; |
||||
|
||||
use crate::{ |
||||
components::{calendar::CalendarSlot, DateTime, Instant}, |
||||
TemporalError, TemporalResult, |
||||
}; |
||||
|
||||
use super::calendar::CalendarProtocol; |
||||
|
||||
/// Any object that implements the `TzProtocol` must implement the below methods/properties.
|
||||
pub const TIME_ZONE_PROPERTIES: [&str; 3] = |
||||
["getOffsetNanosecondsFor", "getPossibleInstantsFor", "id"]; |
||||
|
||||
/// The Time Zone Protocol that must be implemented for time zones.
|
||||
pub trait TzProtocol: Clone { |
||||
/// The context passed to every method of the `TzProtocol`.
|
||||
type Context; |
||||
/// Get the Offset nanoseconds for this `TimeZone`
|
||||
fn get_offset_nanos_for(&self, context: &mut Self::Context) -> TemporalResult<BigInt>; |
||||
/// Get the possible Instant for this `TimeZone`
|
||||
fn get_possible_instant_for(&self, context: &mut Self::Context) |
||||
-> TemporalResult<Vec<Instant>>; // TODO: Implement Instant
|
||||
/// Get the `TimeZone`'s identifier.
|
||||
fn id(&self, context: &mut Self::Context) -> TemporalResult<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.
|
||||
#[derive(Clone)] |
||||
pub enum TimeZoneSlot<Z: TzProtocol> { |
||||
/// A native `TimeZone` representation.
|
||||
Tz(TimeZone), |
||||
/// A Custom `TimeZone` that implements the `TzProtocol`.
|
||||
Protocol(Z), |
||||
} |
||||
|
||||
impl<Z: TzProtocol> core::fmt::Debug for TimeZoneSlot<Z> { |
||||
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<Z: TzProtocol> TimeZoneSlot<Z> { |
||||
pub(crate) fn get_datetime_for<C: CalendarProtocol>( |
||||
&self, |
||||
instant: &Instant, |
||||
calendar: &CalendarSlot<C>, |
||||
context: &mut Z::Context, |
||||
) -> TemporalResult<DateTime<C>> { |
||||
let nanos = self.get_offset_nanos_for(context)?; |
||||
DateTime::from_instant(instant, nanos.to_f64().unwrap_or(0.0), calendar.clone()) |
||||
} |
||||
} |
||||
|
||||
impl<Z: TzProtocol> TimeZoneSlot<Z> { |
||||
/// Get the offset for this current `TimeZoneSlot`.
|
||||
pub fn get_offset_nanos_for(&self, context: &mut Z::Context) -> 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), |
||||
} |
||||
} |
||||
|
||||
/// Get the possible `Instant`s for this `TimeZoneSlot`.
|
||||
pub fn get_possible_instant_for( |
||||
&self, |
||||
_context: &mut Z::Context, |
||||
) -> TemporalResult<Vec<Instant>> { |
||||
Err(TemporalError::general("Not yet implemented.")) |
||||
} |
||||
|
||||
/// Returns the current `TimeZoneSlot`'s identifier.
|
||||
pub fn id(&self, context: &mut Z::Context) -> TemporalResult<String> { |
||||
match self { |
||||
Self::Tz(_) => Err(TemporalError::range().with_message("Not yet implemented.")), // TODO: Implement Display for Time Zone.
|
||||
Self::Protocol(tz) => tz.id(context), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl TzProtocol for () { |
||||
type Context = (); |
||||
fn get_offset_nanos_for(&self, (): &mut ()) -> TemporalResult<BigInt> { |
||||
unreachable!() |
||||
} |
||||
|
||||
fn get_possible_instant_for(&self, (): &mut ()) -> TemporalResult<Vec<Instant>> { |
||||
unreachable!() |
||||
} |
||||
|
||||
fn id(&self, (): &mut ()) -> TemporalResult<String> { |
||||
Ok("() TimeZone".to_owned()) |
||||
} |
||||
} |
@ -1,96 +0,0 @@
|
||||
//! This module implements `YearMonth` and any directly related algorithms.
|
||||
|
||||
use std::str::FromStr; |
||||
|
||||
use crate::{ |
||||
components::calendar::CalendarSlot, |
||||
iso::{IsoDate, IsoDateSlots}, |
||||
options::ArithmeticOverflow, |
||||
TemporalError, TemporalResult, |
||||
}; |
||||
|
||||
use super::calendar::{CalendarProtocol, GetCalendarSlot}; |
||||
|
||||
/// The native Rust implementation of `Temporal.YearMonth`.
|
||||
#[derive(Debug, Default, Clone)] |
||||
pub struct YearMonth<C: CalendarProtocol> { |
||||
iso: IsoDate, |
||||
calendar: CalendarSlot<C>, |
||||
} |
||||
|
||||
impl<C: CalendarProtocol> YearMonth<C> { |
||||
/// Creates an unvalidated `YearMonth`.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub(crate) fn new_unchecked(iso: IsoDate, calendar: CalendarSlot<C>) -> Self { |
||||
Self { iso, calendar } |
||||
} |
||||
|
||||
/// Creates a new valid `YearMonth`.
|
||||
#[inline] |
||||
pub fn new( |
||||
year: i32, |
||||
month: i32, |
||||
reference_day: Option<i32>, |
||||
calendar: CalendarSlot<C>, |
||||
overflow: ArithmeticOverflow, |
||||
) -> TemporalResult<Self> { |
||||
let day = reference_day.unwrap_or(1); |
||||
let iso = IsoDate::new(year, month, day, overflow)?; |
||||
Ok(Self::new_unchecked(iso, calendar)) |
||||
} |
||||
|
||||
/// Returns the `year` value for this `YearMonth`.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn year(&self) -> i32 { |
||||
self.iso.year |
||||
} |
||||
|
||||
/// Returns the `month` value for this `YearMonth`.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn month(&self) -> u8 { |
||||
self.iso.month |
||||
} |
||||
|
||||
/// Returns the Calendar value.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn calendar(&self) -> &CalendarSlot<C> { |
||||
&self.calendar |
||||
} |
||||
} |
||||
|
||||
impl<C: CalendarProtocol> GetCalendarSlot<C> for YearMonth<C> { |
||||
/// Returns a reference to `YearMonth`'s `CalendarSlot`
|
||||
fn get_calendar(&self) -> CalendarSlot<C> { |
||||
self.calendar.clone() |
||||
} |
||||
} |
||||
|
||||
impl<C: CalendarProtocol> IsoDateSlots for YearMonth<C> { |
||||
#[inline] |
||||
/// Returns this `YearMonth`'s `IsoDate`
|
||||
fn iso_date(&self) -> IsoDate { |
||||
self.iso |
||||
} |
||||
} |
||||
|
||||
impl<C: CalendarProtocol> FromStr for YearMonth<C> { |
||||
type Err = TemporalError; |
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
let record = crate::parser::parse_year_month(s)?; |
||||
|
||||
let calendar = record.calendar.unwrap_or("iso8601".into()); |
||||
|
||||
Self::new( |
||||
record.date.year, |
||||
record.date.month, |
||||
None, |
||||
CalendarSlot::from_str(&calendar)?, |
||||
ArithmeticOverflow::Reject, |
||||
) |
||||
} |
||||
} |
@ -1,234 +0,0 @@
|
||||
//! This module implements `ZonedDateTime` and any directly related algorithms.
|
||||
|
||||
use num_bigint::BigInt; |
||||
use tinystr::TinyStr4; |
||||
|
||||
use crate::{ |
||||
components::{ |
||||
calendar::{CalendarDateLike, CalendarProtocol, CalendarSlot}, |
||||
tz::TimeZoneSlot, |
||||
Instant, |
||||
}, |
||||
TemporalResult, |
||||
}; |
||||
|
||||
use super::tz::TzProtocol; |
||||
|
||||
/// The native Rust implementation of `Temporal.ZonedDateTime`.
|
||||
#[derive(Debug, Clone)] |
||||
pub struct ZonedDateTime<C: CalendarProtocol, Z: TzProtocol> { |
||||
instant: Instant, |
||||
calendar: CalendarSlot<C>, |
||||
tz: TimeZoneSlot<Z>, |
||||
} |
||||
|
||||
// ==== Private API ====
|
||||
|
||||
impl<C: CalendarProtocol, Z: TzProtocol> ZonedDateTime<C, Z> { |
||||
/// Creates a `ZonedDateTime` without validating the input.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub(crate) fn new_unchecked( |
||||
instant: Instant, |
||||
calendar: CalendarSlot<C>, |
||||
tz: TimeZoneSlot<Z>, |
||||
) -> Self { |
||||
Self { |
||||
instant, |
||||
calendar, |
||||
tz, |
||||
} |
||||
} |
||||
} |
||||
|
||||
// ==== Public API ====
|
||||
|
||||
impl<C: CalendarProtocol, Z: TzProtocol> ZonedDateTime<C, Z> { |
||||
/// Creates a new valid `ZonedDateTime`.
|
||||
#[inline] |
||||
pub fn new( |
||||
nanos: BigInt, |
||||
calendar: CalendarSlot<C>, |
||||
tz: TimeZoneSlot<Z>, |
||||
) -> TemporalResult<Self> { |
||||
let instant = Instant::new(nanos)?; |
||||
Ok(Self::new_unchecked(instant, calendar, tz)) |
||||
} |
||||
|
||||
/// Returns `ZonedDateTime`'s Calendar.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn calendar(&self) -> &CalendarSlot<C> { |
||||
&self.calendar |
||||
} |
||||
|
||||
/// Returns `ZonedDateTime`'s `TimeZone` slot.
|
||||
#[inline] |
||||
#[must_use] |
||||
pub fn tz(&self) -> &TimeZoneSlot<Z> { |
||||
&self.tz |
||||
} |
||||
|
||||
/// 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<C, Z: TzProtocol> ZonedDateTime<C, Z> |
||||
where |
||||
C: CalendarProtocol<Context = Z::Context>, |
||||
{ |
||||
/// Returns the `year` value for this `ZonedDateTime`.
|
||||
#[inline] |
||||
pub fn contextual_year(&self, context: &mut C::Context) -> TemporalResult<i32> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
self.calendar.year(&CalendarDateLike::DateTime(dt), context) |
||||
} |
||||
|
||||
/// Returns the `month` value for this `ZonedDateTime`.
|
||||
pub fn contextual_month(&self, context: &mut C::Context) -> TemporalResult<u8> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
self.calendar |
||||
.month(&CalendarDateLike::DateTime(dt), context) |
||||
} |
||||
|
||||
/// Returns the `monthCode` value for this `ZonedDateTime`.
|
||||
pub fn contextual_month_code(&self, context: &mut C::Context) -> TemporalResult<TinyStr4> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
self.calendar |
||||
.month_code(&CalendarDateLike::DateTime(dt), context) |
||||
} |
||||
|
||||
/// Returns the `day` value for this `ZonedDateTime`.
|
||||
pub fn contextual_day(&self, context: &mut C::Context) -> TemporalResult<u8> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
self.calendar.day(&CalendarDateLike::DateTime(dt), context) |
||||
} |
||||
|
||||
/// Returns the `hour` value for this `ZonedDateTime`.
|
||||
pub fn contextual_hour(&self, context: &mut C::Context) -> TemporalResult<u8> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
Ok(dt.hour()) |
||||
} |
||||
|
||||
/// Returns the `minute` value for this `ZonedDateTime`.
|
||||
pub fn contextual_minute(&self, context: &mut C::Context) -> TemporalResult<u8> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
Ok(dt.minute()) |
||||
} |
||||
|
||||
/// Returns the `second` value for this `ZonedDateTime`.
|
||||
pub fn contextual_second(&self, context: &mut C::Context) -> TemporalResult<u8> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
Ok(dt.second()) |
||||
} |
||||
|
||||
/// Returns the `millisecond` value for this `ZonedDateTime`.
|
||||
pub fn contextual_millisecond(&self, context: &mut C::Context) -> TemporalResult<u16> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
Ok(dt.millisecond()) |
||||
} |
||||
|
||||
/// Returns the `microsecond` value for this `ZonedDateTime`.
|
||||
pub fn contextual_microsecond(&self, context: &mut C::Context) -> TemporalResult<u16> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
Ok(dt.millisecond()) |
||||
} |
||||
|
||||
/// Returns the `nanosecond` value for this `ZonedDateTime`.
|
||||
pub fn contextual_nanosecond(&self, context: &mut C::Context) -> TemporalResult<u16> { |
||||
let dt = self |
||||
.tz |
||||
.get_datetime_for(&self.instant, &self.calendar, context)?; |
||||
Ok(dt.nanosecond()) |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use std::str::FromStr; |
||||
|
||||
use crate::components::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::from_str("iso8601").unwrap(), |
||||
TimeZoneSlot::Tz(TimeZone { |
||||
iana: None, |
||||
offset: Some(0), |
||||
}), |
||||
) |
||||
.unwrap(); |
||||
|
||||
assert_eq!(zdt.contextual_year(&mut ()).unwrap(), 2023); |
||||
assert_eq!(zdt.contextual_month(&mut ()).unwrap(), 11); |
||||
assert_eq!(zdt.contextual_day(&mut ()).unwrap(), 30); |
||||
assert_eq!(zdt.contextual_hour(&mut ()).unwrap(), 1); |
||||
assert_eq!(zdt.contextual_minute(&mut ()).unwrap(), 49); |
||||
assert_eq!(zdt.contextual_second(&mut ()).unwrap(), 12); |
||||
|
||||
let zdt_minus_five = ZonedDateTime::<(), ()>::new( |
||||
nov_30_2023_utc, |
||||
CalendarSlot::from_str("iso8601").unwrap(), |
||||
TimeZoneSlot::Tz(TimeZone { |
||||
iana: None, |
||||
offset: Some(-300), |
||||
}), |
||||
) |
||||
.unwrap(); |
||||
|
||||
assert_eq!(zdt_minus_five.contextual_year(&mut ()).unwrap(), 2023); |
||||
assert_eq!(zdt_minus_five.contextual_month(&mut ()).unwrap(), 11); |
||||
assert_eq!(zdt_minus_five.contextual_day(&mut ()).unwrap(), 29); |
||||
assert_eq!(zdt_minus_five.contextual_hour(&mut ()).unwrap(), 20); |
||||
assert_eq!(zdt_minus_five.contextual_minute(&mut ()).unwrap(), 49); |
||||
assert_eq!(zdt_minus_five.contextual_second(&mut ()).unwrap(), 12); |
||||
} |
||||
} |
@ -1,121 +0,0 @@
|
||||
//! This module implements `TemporalError`.
|
||||
|
||||
use core::fmt; |
||||
|
||||
use icu_calendar::CalendarError; |
||||
|
||||
/// `TemporalError`'s error type.
|
||||
#[derive(Debug, Default, Clone, Copy)] |
||||
pub enum ErrorKind { |
||||
/// Error.
|
||||
#[default] |
||||
Generic, |
||||
/// TypeError
|
||||
Type, |
||||
/// RangeError
|
||||
Range, |
||||
/// SyntaxError
|
||||
Syntax, |
||||
} |
||||
|
||||
impl fmt::Display for ErrorKind { |
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
||||
match self { |
||||
Self::Generic => "Error", |
||||
Self::Type => "TypeError", |
||||
Self::Range => "RangeError", |
||||
Self::Syntax => "SyntaxError", |
||||
} |
||||
.fmt(f) |
||||
} |
||||
} |
||||
|
||||
/// The error type for `boa_temporal`.
|
||||
#[derive(Debug, Clone)] |
||||
pub struct TemporalError { |
||||
kind: ErrorKind, |
||||
msg: Box<str>, |
||||
} |
||||
|
||||
impl TemporalError { |
||||
fn new(kind: ErrorKind) -> Self { |
||||
Self { |
||||
kind, |
||||
msg: Box::default(), |
||||
} |
||||
} |
||||
|
||||
/// Create a generic error
|
||||
#[must_use] |
||||
pub fn general<S>(msg: S) -> Self |
||||
where |
||||
S: Into<Box<str>>, |
||||
{ |
||||
Self::new(ErrorKind::Generic).with_message(msg) |
||||
} |
||||
|
||||
/// Create a range error.
|
||||
#[must_use] |
||||
pub fn range() -> Self { |
||||
Self::new(ErrorKind::Range) |
||||
} |
||||
|
||||
/// Create a type error.
|
||||
#[must_use] |
||||
pub fn r#type() -> Self { |
||||
Self::new(ErrorKind::Type) |
||||
} |
||||
|
||||
/// Create a syntax error.
|
||||
#[must_use] |
||||
pub fn syntax() -> Self { |
||||
Self::new(ErrorKind::Syntax) |
||||
} |
||||
|
||||
/// Create an abrupt end error.
|
||||
#[must_use] |
||||
pub fn abrupt_end() -> Self { |
||||
Self::syntax().with_message("Abrupt end to parsing target.") |
||||
} |
||||
|
||||
/// Add a message to the error.
|
||||
#[must_use] |
||||
pub fn with_message<S>(mut self, msg: S) -> Self |
||||
where |
||||
S: Into<Box<str>>, |
||||
{ |
||||
self.msg = msg.into(); |
||||
self |
||||
} |
||||
|
||||
/// Returns this error's kind.
|
||||
#[must_use] |
||||
pub fn kind(&self) -> ErrorKind { |
||||
self.kind |
||||
} |
||||
|
||||
/// Returns the error message.
|
||||
#[must_use] |
||||
pub fn message(&self) -> &str { |
||||
&self.msg |
||||
} |
||||
} |
||||
|
||||
impl fmt::Display for TemporalError { |
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
||||
write!(f, "{}", self.kind)?; |
||||
|
||||
let msg = self.msg.trim(); |
||||
if !msg.is_empty() { |
||||
write!(f, ": {msg}")?; |
||||
} |
||||
|
||||
Ok(()) |
||||
} |
||||
} |
||||
|
||||
impl From<CalendarError> for TemporalError { |
||||
fn from(value: CalendarError) -> Self { |
||||
TemporalError::general(value.to_string()) |
||||
} |
||||
} |
@ -1,640 +0,0 @@
|
||||
//! This module implements a native Rust `TemporalField` and components.
|
||||
|
||||
use std::{fmt, str::FromStr}; |
||||
|
||||
use crate::{ |
||||
components::calendar::{CalendarProtocol, CalendarSlot}, |
||||
error::TemporalError, |
||||
TemporalResult, |
||||
}; |
||||
|
||||
use bitflags::bitflags; |
||||
// use rustc_hash::FxHashSet;
|
||||
use tinystr::{TinyAsciiStr, TinyStr16, TinyStr4}; |
||||
|
||||
bitflags! { |
||||
/// FieldMap maps the currently active fields on the `TemporalField`
|
||||
#[derive(Debug, PartialEq, Eq)] |
||||
pub struct FieldMap: u16 { |
||||
/// Represents an active `year` field
|
||||
const YEAR = 0b0000_0000_0000_0001; |
||||
/// Represents an active `month` field
|
||||
const MONTH = 0b0000_0000_0000_0010; |
||||
/// Represents an active `monthCode` field
|
||||
const MONTH_CODE = 0b0000_0000_0000_0100; |
||||
/// Represents an active `day` field
|
||||
const DAY = 0b0000_0000_0000_1000; |
||||
/// Represents an active `hour` field
|
||||
const HOUR = 0b0000_0000_0001_0000; |
||||
/// Represents an active `minute` field
|
||||
const MINUTE = 0b0000_0000_0010_0000; |
||||
/// Represents an active `second` field
|
||||
const SECOND = 0b0000_0000_0100_0000; |
||||
/// Represents an active `millisecond` field
|
||||
const MILLISECOND = 0b0000_0000_1000_0000; |
||||
/// Represents an active `microsecond` field
|
||||
const MICROSECOND = 0b0000_0001_0000_0000; |
||||
/// Represents an active `nanosecond` field
|
||||
const NANOSECOND = 0b0000_0010_0000_0000; |
||||
/// Represents an active `offset` field
|
||||
const OFFSET = 0b0000_0100_0000_0000; |
||||
/// Represents an active `era` field
|
||||
const ERA = 0b0000_1000_0000_0000; |
||||
/// Represents an active `eraYear` field
|
||||
const ERA_YEAR = 0b0001_0000_0000_0000; |
||||
/// Represents an active `timeZone` field
|
||||
const TIME_ZONE = 0b0010_0000_0000_0000; |
||||
// NOTE(nekevss): Two bits preserved if needed.
|
||||
} |
||||
} |
||||
|
||||
/// The post conversion field value.
|
||||
#[derive(Debug)] |
||||
#[allow(variant_size_differences)] |
||||
pub enum FieldValue { |
||||
/// Designates the values as an integer.
|
||||
Integer(i32), |
||||
/// Designates that the value is undefined.
|
||||
Undefined, |
||||
/// Designates the value as a string.
|
||||
String(String), |
||||
} |
||||
|
||||
impl From<i32> for FieldValue { |
||||
fn from(value: i32) -> Self { |
||||
FieldValue::Integer(value) |
||||
} |
||||
} |
||||
|
||||
/// The Conversion type of a field.
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub enum FieldConversion { |
||||
/// Designates the Conversion type is `ToIntegerWithTruncation`
|
||||
ToIntegerWithTruncation, |
||||
/// Designates the Conversion type is `ToPositiveIntegerWithTruncation`
|
||||
ToPositiveIntegerWithTruncation, |
||||
/// Designates the Conversion type is `ToPrimitiveRequireString`
|
||||
ToPrimativeAndRequireString, |
||||
/// Designates the Conversion type is nothing
|
||||
None, |
||||
} |
||||
|
||||
impl FromStr for FieldConversion { |
||||
type Err = TemporalError; |
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
match s { |
||||
"year" | "hour" | "minute" | "second" | "millisecond" | "microsecond" |
||||
| "nanosecond" => Ok(Self::ToIntegerWithTruncation), |
||||
"month" | "day" => Ok(Self::ToPositiveIntegerWithTruncation), |
||||
"monthCode" | "offset" | "eraYear" => Ok(Self::ToPrimativeAndRequireString), |
||||
_ => Err(TemporalError::range() |
||||
.with_message(format!("{s} is not a valid TemporalField Property"))), |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// `TemporalFields` acts as a native Rust implementation of the `fields` object
|
||||
///
|
||||
/// The temporal fields are laid out in the Temporal proposal under section 13.46 `PrepareTemporalFields`
|
||||
/// with conversion and defaults laid out by Table 17 (displayed below).
|
||||
///
|
||||
/// ## Table 17: Temporal field requirements
|
||||
///
|
||||
/// | Property | Conversion | Default |
|
||||
/// | -------------|-----------------------------------|------------|
|
||||
/// | "year" | `ToIntegerWithTruncation` | undefined |
|
||||
/// | "month" | `ToPositiveIntegerWithTruncation` | undefined |
|
||||
/// | "monthCode" | `ToPrimitiveAndRequireString` | undefined |
|
||||
/// | "day" | `ToPositiveIntegerWithTruncation` | undefined |
|
||||
/// | "hour" | `ToIntegerWithTruncation` | +0𝔽 |
|
||||
/// | "minute" | `ToIntegerWithTruncation` | +0𝔽 |
|
||||
/// | "second" | `ToIntegerWithTruncation` | +0𝔽 |
|
||||
/// | "millisecond"| `ToIntegerWithTruncation` | +0𝔽 |
|
||||
/// | "microsecond"| `ToIntegerWithTruncation` | +0𝔽 |
|
||||
/// | "nanosecond" | `ToIntegerWithTruncation` | +0𝔽 |
|
||||
/// | "offset" | `ToPrimitiveAndRequireString` | undefined |
|
||||
/// | "era" | `ToPrimitiveAndRequireString` | undefined |
|
||||
/// | "eraYear" | `ToIntegerWithTruncation` | undefined |
|
||||
/// | "timeZone" | `None` | undefined |
|
||||
#[derive(Debug)] |
||||
pub struct TemporalFields { |
||||
bit_map: FieldMap, |
||||
year: Option<i32>, |
||||
month: Option<i32>, |
||||
month_code: Option<TinyStr4>, // TODO: Switch to icu compatible value.
|
||||
day: Option<i32>, |
||||
hour: i32, |
||||
minute: i32, |
||||
second: i32, |
||||
millisecond: i32, |
||||
microsecond: i32, |
||||
nanosecond: i32, |
||||
offset: Option<String>, // TODO: Switch to tinystr?
|
||||
era: Option<TinyStr16>, // TODO: switch to icu compatible value.
|
||||
era_year: Option<i32>, // TODO: switch to icu compatible value.
|
||||
time_zone: Option<String>, // TODO: figure out the identifier for TimeZone.
|
||||
} |
||||
|
||||
impl Default for TemporalFields { |
||||
fn default() -> Self { |
||||
Self { |
||||
bit_map: FieldMap::empty(), |
||||
year: None, |
||||
month: None, |
||||
month_code: None, |
||||
day: None, |
||||
hour: 0, |
||||
minute: 0, |
||||
second: 0, |
||||
millisecond: 0, |
||||
microsecond: 0, |
||||
nanosecond: 0, |
||||
offset: None, |
||||
era: None, |
||||
era_year: None, |
||||
time_zone: None, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl TemporalFields { |
||||
pub(crate) fn era(&self) -> TinyAsciiStr<16> { |
||||
self.era.unwrap_or("default".parse().expect("less than 8")) |
||||
} |
||||
|
||||
pub(crate) const fn year(&self) -> Option<i32> { |
||||
self.year |
||||
} |
||||
|
||||
pub(crate) const fn month(&self) -> Option<i32> { |
||||
self.month |
||||
} |
||||
|
||||
pub(crate) fn month_code(&self) -> TinyAsciiStr<4> { |
||||
// Passing along an invalid MonthCode to ICU...might be better to figure out a different approach...TBD.
|
||||
self.month_code |
||||
.unwrap_or("M00".parse().expect("less than 4")) |
||||
} |
||||
|
||||
pub(crate) const fn day(&self) -> Option<i32> { |
||||
self.day |
||||
} |
||||
} |
||||
|
||||
// TODO: Update the below.
|
||||
impl TemporalFields { |
||||
/// Flags a field as being required.
|
||||
#[inline] |
||||
pub fn require_field(&mut self, field: &str) { |
||||
match field { |
||||
"year" => self.bit_map.set(FieldMap::YEAR, true), |
||||
"month" => self.bit_map.set(FieldMap::MONTH, true), |
||||
"monthCode" => self.bit_map.set(FieldMap::MONTH_CODE, true), |
||||
"day" => self.bit_map.set(FieldMap::DAY, true), |
||||
"hour" => self.bit_map.set(FieldMap::HOUR, true), |
||||
"minute" => self.bit_map.set(FieldMap::MINUTE, true), |
||||
"second" => self.bit_map.set(FieldMap::SECOND, true), |
||||
"millisecond" => self.bit_map.set(FieldMap::MILLISECOND, true), |
||||
"microsecond" => self.bit_map.set(FieldMap::MICROSECOND, true), |
||||
"nanosecond" => self.bit_map.set(FieldMap::NANOSECOND, true), |
||||
"offset" => self.bit_map.set(FieldMap::OFFSET, true), |
||||
"era" => self.bit_map.set(FieldMap::ERA, true), |
||||
"eraYear" => self.bit_map.set(FieldMap::ERA_YEAR, true), |
||||
"timeZone" => self.bit_map.set(FieldMap::TIME_ZONE, true), |
||||
_ => {} |
||||
} |
||||
} |
||||
|
||||
#[inline] |
||||
/// A generic field setter for `TemporalFields`
|
||||
///
|
||||
/// This method will not run any `JsValue` conversion. `FieldValue` is
|
||||
/// expected to contain a preconverted value.
|
||||
pub fn set_field_value(&mut self, field: &str, value: &FieldValue) -> TemporalResult<()> { |
||||
match field { |
||||
"year" => self.set_year(value)?, |
||||
"month" => self.set_month(value)?, |
||||
"monthCode" => self.set_month_code(value)?, |
||||
"day" => self.set_day(value)?, |
||||
"hour" => self.set_hour(value)?, |
||||
"minute" => self.set_minute(value)?, |
||||
"second" => self.set_second(value)?, |
||||
"millisecond" => self.set_milli(value)?, |
||||
"microsecond" => self.set_micro(value)?, |
||||
"nanosecond" => self.set_nano(value)?, |
||||
"offset" => self.set_offset(value)?, |
||||
"era" => self.set_era(value)?, |
||||
"eraYear" => self.set_era_year(value)?, |
||||
"timeZone" => self.set_time_zone(value)?, |
||||
_ => unreachable!(), |
||||
} |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
/// Retrieves a field value if set, else None.
|
||||
pub fn get(&self, field: &str) -> Option<FieldValue> { |
||||
if !self.is_set_field(field) { |
||||
return None; |
||||
} |
||||
match field { |
||||
"year" => self.year.map(FieldValue::Integer), |
||||
"month" => self.month.map(FieldValue::Integer), |
||||
"monthCode" => self.month_code.map(|s| FieldValue::String(s.to_string())), |
||||
"day" => self.day.map(FieldValue::from), |
||||
"hour" => Some(FieldValue::Integer(self.hour)), |
||||
"minute" => Some(FieldValue::Integer(self.minute)), |
||||
"second" => Some(FieldValue::Integer(self.second)), |
||||
"millisecond" => Some(FieldValue::Integer(self.millisecond)), |
||||
"microsecond" => Some(FieldValue::Integer(self.microsecond)), |
||||
"nanosecond" => Some(FieldValue::Integer(self.nanosecond)), |
||||
"offset" => self.offset.as_ref().map(|s| FieldValue::String(s.clone())), |
||||
"era" => self.era.map(|s| FieldValue::String(s.to_string())), |
||||
"eraYear" => self.era_year.map(FieldValue::Integer), |
||||
"timeZone" => self |
||||
.time_zone |
||||
.as_ref() |
||||
.map(|s| FieldValue::String(s.clone())), |
||||
_ => unreachable!(), |
||||
} |
||||
} |
||||
|
||||
fn is_set_field(&self, field: &str) -> bool { |
||||
match field { |
||||
"year" => self.bit_map.contains(FieldMap::YEAR), |
||||
"month" => self.bit_map.contains(FieldMap::MONTH), |
||||
"monthCode" => self.bit_map.contains(FieldMap::MONTH_CODE), |
||||
"day" => self.bit_map.contains(FieldMap::DAY), |
||||
"hour" => self.bit_map.contains(FieldMap::HOUR), |
||||
"minute" => self.bit_map.contains(FieldMap::MINUTE), |
||||
"second" => self.bit_map.contains(FieldMap::SECOND), |
||||
"millisecond" => self.bit_map.contains(FieldMap::MILLISECOND), |
||||
"microsecond" => self.bit_map.contains(FieldMap::MICROSECOND), |
||||
"nanosecond" => self.bit_map.contains(FieldMap::NANOSECOND), |
||||
"offset" => self.bit_map.contains(FieldMap::OFFSET), |
||||
"era" => self.bit_map.contains(FieldMap::ERA), |
||||
"eraYear" => self.bit_map.contains(FieldMap::ERA_YEAR), |
||||
"timeZone" => self.bit_map.contains(FieldMap::TIME_ZONE), |
||||
_ => unreachable!(), |
||||
} |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_year(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||
let FieldValue::Integer(y) = value else { |
||||
return Err(TemporalError::r#type().with_message("Year must be an integer.")); |
||||
}; |
||||
self.year = Some(*y); |
||||
self.bit_map.set(FieldMap::YEAR, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_month(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||
let FieldValue::Integer(mo) = value else { |
||||
return Err(TemporalError::r#type().with_message("Month must be an integer.")); |
||||
}; |
||||
self.year = Some(*mo); |
||||
self.bit_map.set(FieldMap::MONTH, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_month_code(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||
let FieldValue::String(mc) = value else { |
||||
return Err(TemporalError::r#type().with_message("monthCode must be string.")); |
||||
}; |
||||
self.month_code = |
||||
Some(TinyStr4::from_bytes(mc.as_bytes()).expect("monthCode must be less than 4 chars")); |
||||
self.bit_map.set(FieldMap::MONTH_CODE, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_day(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||
let FieldValue::Integer(d) = value else { |
||||
return Err(TemporalError::r#type().with_message("day must be an integer.")); |
||||
}; |
||||
self.day = Some(*d); |
||||
self.bit_map.set(FieldMap::DAY, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_hour(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||
let FieldValue::Integer(h) = value else { |
||||
return Err(TemporalError::r#type().with_message("hour must be an integer.")); |
||||
}; |
||||
self.hour = *h; |
||||
self.bit_map.set(FieldMap::HOUR, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_minute(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||
let FieldValue::Integer(min) = value else { |
||||
return Err(TemporalError::r#type().with_message("minute must be an integer.")); |
||||
}; |
||||
self.minute = *min; |
||||
self.bit_map.set(FieldMap::MINUTE, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_second(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||
let FieldValue::Integer(sec) = value else { |
||||
return Err(TemporalError::r#type().with_message("Second must be an integer.")); |
||||
}; |
||||
self.second = *sec; |
||||
self.bit_map.set(FieldMap::SECOND, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_milli(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||
let FieldValue::Integer(milli) = value else { |
||||
return Err(TemporalError::r#type().with_message("Second must be an integer.")); |
||||
}; |
||||
self.millisecond = *milli; |
||||
self.bit_map.set(FieldMap::MILLISECOND, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_micro(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||
let FieldValue::Integer(micro) = value else { |
||||
return Err(TemporalError::r#type().with_message("microsecond must be an integer.")); |
||||
}; |
||||
self.microsecond = *micro; |
||||
self.bit_map.set(FieldMap::MICROSECOND, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_nano(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||
let FieldValue::Integer(nano) = value else { |
||||
return Err(TemporalError::r#type().with_message("nanosecond must be an integer.")); |
||||
}; |
||||
self.nanosecond = *nano; |
||||
self.bit_map.set(FieldMap::NANOSECOND, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_offset(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||
let FieldValue::String(offset) = value else { |
||||
return Err(TemporalError::r#type().with_message("offset must be string.")); |
||||
}; |
||||
self.offset = Some(offset.to_string()); |
||||
self.bit_map.set(FieldMap::OFFSET, true); |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_era(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||
let FieldValue::String(era) = value else { |
||||
return Err(TemporalError::r#type().with_message("era must be string.")); |
||||
}; |
||||
self.era = |
||||
Some(TinyStr16::from_bytes(era.as_bytes()).expect("era should not exceed 16 bytes.")); |
||||
self.bit_map.set(FieldMap::ERA, true); |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_era_year(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||
let FieldValue::Integer(era_year) = value else { |
||||
return Err(TemporalError::r#type().with_message("eraYear must be an integer.")); |
||||
}; |
||||
self.era_year = Some(*era_year); |
||||
self.bit_map.set(FieldMap::ERA_YEAR, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_time_zone(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||
let FieldValue::String(tz) = value else { |
||||
return Err(TemporalError::r#type().with_message("tz must be string.")); |
||||
}; |
||||
self.time_zone = Some(tz.to_string()); |
||||
self.bit_map.set(FieldMap::TIME_ZONE, true); |
||||
Ok(()) |
||||
} |
||||
} |
||||
|
||||
impl TemporalFields { |
||||
/// Returns a vector filled with the key-value pairs marked as active.
|
||||
#[must_use] |
||||
pub fn active_kvs(&self) -> Vec<(String, FieldValue)> { |
||||
self.keys().zip(self.values()).collect() |
||||
} |
||||
|
||||
/// Returns an iterator over the current keys.
|
||||
#[must_use] |
||||
pub fn keys(&self) -> Keys { |
||||
Keys { |
||||
iter: self.bit_map.iter(), |
||||
} |
||||
} |
||||
|
||||
/// Returns an iterator over the current values.
|
||||
#[must_use] |
||||
pub fn values(&self) -> Values<'_> { |
||||
Values { |
||||
fields: self, |
||||
iter: self.bit_map.iter(), |
||||
} |
||||
} |
||||
|
||||
/// Resolve `TemporalFields` month and monthCode fields.
|
||||
pub(crate) fn iso_resolve_month(&mut self) -> TemporalResult<()> { |
||||
if self.month_code.is_none() { |
||||
if self.month.is_some() { |
||||
return Ok(()); |
||||
} |
||||
|
||||
return Err(TemporalError::range() |
||||
.with_message("month and MonthCode values cannot both be undefined.")); |
||||
} |
||||
|
||||
let unresolved_month_code = self |
||||
.month_code |
||||
.as_ref() |
||||
.expect("monthCode must exist at this point."); |
||||
|
||||
let month_code_integer = month_code_to_integer(*unresolved_month_code)?; |
||||
|
||||
let new_month = match self.month { |
||||
Some(month) if month != month_code_integer => { |
||||
return Err( |
||||
TemporalError::range().with_message("month and monthCode cannot be resolved.") |
||||
) |
||||
} |
||||
_ => month_code_integer, |
||||
}; |
||||
|
||||
self.month = Some(new_month); |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
/// Merges two `TemporalFields` values given a specific `CalendarSlot`.
|
||||
pub fn merge_fields<C: CalendarProtocol>( |
||||
&self, |
||||
other: &Self, |
||||
calendar: &CalendarSlot<C>, |
||||
) -> TemporalResult<Self> { |
||||
let add_keys = other.keys().collect::<Vec<_>>(); |
||||
let overridden_keys = calendar.field_keys_to_ignore(&add_keys)?; |
||||
|
||||
let mut result = Self::default(); |
||||
|
||||
for key in self.keys() { |
||||
let value = if overridden_keys.contains(&key) { |
||||
other.get(&key) |
||||
} else { |
||||
self.get(&key) |
||||
}; |
||||
|
||||
if let Some(value) = value { |
||||
result.set_field_value(&key, &value)?; |
||||
} |
||||
} |
||||
|
||||
Ok(result) |
||||
} |
||||
} |
||||
|
||||
/// Iterator over `TemporalFields` keys.
|
||||
pub struct Keys { |
||||
iter: bitflags::iter::Iter<FieldMap>, |
||||
} |
||||
|
||||
impl fmt::Debug for Keys { |
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
||||
write!(f, "TemporalFields KeyIterator") |
||||
} |
||||
} |
||||
|
||||
impl Iterator for Keys { |
||||
type Item = String; |
||||
|
||||
fn next(&mut self) -> Option<Self::Item> { |
||||
let Some(field) = self.iter.next() else { |
||||
return None; |
||||
}; |
||||
|
||||
match field { |
||||
FieldMap::YEAR => Some("year".to_owned()), |
||||
FieldMap::MONTH => Some("month".to_owned()), |
||||
FieldMap::MONTH_CODE => Some("monthCode".to_owned()), |
||||
FieldMap::DAY => Some("day".to_owned()), |
||||
FieldMap::HOUR => Some("hour".to_owned()), |
||||
FieldMap::MINUTE => Some("minute".to_owned()), |
||||
FieldMap::SECOND => Some("second".to_owned()), |
||||
FieldMap::MILLISECOND => Some("millisecond".to_owned()), |
||||
FieldMap::MICROSECOND => Some("microsecond".to_owned()), |
||||
FieldMap::NANOSECOND => Some("nanosecond".to_owned()), |
||||
FieldMap::OFFSET => Some("offset".to_owned()), |
||||
FieldMap::ERA => Some("era".to_owned()), |
||||
FieldMap::ERA_YEAR => Some("eraYear".to_owned()), |
||||
FieldMap::TIME_ZONE => Some("timeZone".to_owned()), |
||||
_ => None, |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// An iterator over `TemporalFields`'s values.
|
||||
pub struct Values<'a> { |
||||
fields: &'a TemporalFields, |
||||
iter: bitflags::iter::Iter<FieldMap>, |
||||
} |
||||
|
||||
impl fmt::Debug for Values<'_> { |
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
||||
write!(f, "TemporalFields Values Iterator") |
||||
} |
||||
} |
||||
|
||||
impl Iterator for Values<'_> { |
||||
type Item = FieldValue; |
||||
|
||||
fn next(&mut self) -> Option<Self::Item> { |
||||
let Some(field) = self.iter.next() else { |
||||
return None; |
||||
}; |
||||
match field { |
||||
FieldMap::YEAR => Some( |
||||
self.fields |
||||
.year |
||||
.map_or(FieldValue::Undefined, FieldValue::Integer), |
||||
), |
||||
FieldMap::MONTH => Some( |
||||
self.fields |
||||
.month |
||||
.map_or(FieldValue::Undefined, FieldValue::Integer), |
||||
), |
||||
FieldMap::MONTH_CODE => Some( |
||||
self.fields |
||||
.month_code |
||||
.map_or(FieldValue::Undefined, |s| FieldValue::String(s.to_string())), |
||||
), |
||||
FieldMap::DAY => Some( |
||||
self.fields |
||||
.day |
||||
.map_or(FieldValue::Undefined, FieldValue::Integer), |
||||
), |
||||
FieldMap::HOUR => Some(FieldValue::Integer(self.fields.hour)), |
||||
FieldMap::MINUTE => Some(FieldValue::Integer(self.fields.minute)), |
||||
FieldMap::SECOND => Some(FieldValue::Integer(self.fields.second)), |
||||
FieldMap::MILLISECOND => Some(FieldValue::Integer(self.fields.millisecond)), |
||||
FieldMap::MICROSECOND => Some(FieldValue::Integer(self.fields.microsecond)), |
||||
FieldMap::NANOSECOND => Some(FieldValue::Integer(self.fields.nanosecond)), |
||||
FieldMap::OFFSET => Some( |
||||
self.fields |
||||
.offset |
||||
.clone() |
||||
.map_or(FieldValue::Undefined, FieldValue::String), |
||||
), |
||||
FieldMap::ERA => Some( |
||||
self.fields |
||||
.era |
||||
.map_or(FieldValue::Undefined, |s| FieldValue::String(s.to_string())), |
||||
), |
||||
FieldMap::ERA_YEAR => Some( |
||||
self.fields |
||||
.era_year |
||||
.map_or(FieldValue::Undefined, FieldValue::Integer), |
||||
), |
||||
FieldMap::TIME_ZONE => Some( |
||||
self.fields |
||||
.time_zone |
||||
.clone() |
||||
.map_or(FieldValue::Undefined, FieldValue::String), |
||||
), |
||||
_ => None, |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn month_code_to_integer(mc: TinyAsciiStr<4>) -> TemporalResult<i32> { |
||||
match mc.as_str() { |
||||
"M01" => Ok(1), |
||||
"M02" => Ok(2), |
||||
"M03" => Ok(3), |
||||
"M04" => Ok(4), |
||||
"M05" => Ok(5), |
||||
"M06" => Ok(6), |
||||
"M07" => Ok(7), |
||||
"M08" => Ok(8), |
||||
"M09" => Ok(9), |
||||
"M10" => Ok(10), |
||||
"M11" => Ok(11), |
||||
"M12" => Ok(12), |
||||
"M13" => Ok(13), |
||||
_ => Err(TemporalError::range().with_message("monthCode is not within the valid values.")), |
||||
} |
||||
} |
@ -1,654 +0,0 @@
|
||||
//! This module implements the internal ISO field slots.
|
||||
//!
|
||||
//! The three main types of slots are:
|
||||
//! - `IsoDateTime`
|
||||
//! - `IsoDate`
|
||||
//! - `IsoTime`
|
||||
//!
|
||||
//! An `IsoDate` represents the `[[ISOYear]]`, `[[ISOMonth]]`, and `[[ISODay]]` internal slots.
|
||||
//!
|
||||
//! An `IsoTime` represents the `[[ISOHour]]`, `[[ISOMinute]]`, `[[ISOsecond]]`, `[[ISOmillisecond]]`,
|
||||
//! `[[ISOmicrosecond]]`, and `[[ISOnanosecond]]` internal slots.
|
||||
//!
|
||||
//! An `IsoDateTime` has the internal slots of both an `IsoDate` and `IsoTime`.
|
||||
|
||||
use crate::{ |
||||
components::duration::DateDuration, |
||||
error::TemporalError, |
||||
options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit}, |
||||
utils, TemporalResult, NS_PER_DAY, |
||||
}; |
||||
use icu_calendar::{Date as IcuDate, Iso}; |
||||
use num_bigint::BigInt; |
||||
use num_traits::{cast::FromPrimitive, ToPrimitive}; |
||||
|
||||
/// `IsoDateTime` is the record of the `IsoDate` and `IsoTime` internal slots.
|
||||
#[derive(Debug, Default, Clone, Copy)] |
||||
pub struct IsoDateTime { |
||||
date: IsoDate, |
||||
time: IsoTime, |
||||
} |
||||
|
||||
impl IsoDateTime { |
||||
/// Creates a new `IsoDateTime` without any validaiton.
|
||||
pub(crate) fn new_unchecked(date: IsoDate, time: IsoTime) -> Self { |
||||
Self { date, time } |
||||
} |
||||
|
||||
/// Creates a new validated `IsoDateTime` that is within valid limits.
|
||||
pub(crate) fn new(date: IsoDate, time: IsoTime) -> TemporalResult<Self> { |
||||
if !iso_dt_within_valid_limits(date, &time) { |
||||
return Err( |
||||
TemporalError::range().with_message("IsoDateTime not within a valid range.") |
||||
); |
||||
} |
||||
Ok(Self::new_unchecked(date, time)) |
||||
} |
||||
|
||||
// NOTE: The below assumes that nanos is from an `Instant` and thus in a valid range. -> Needs validation.
|
||||
/// Creates an `IsoDateTime` from a `BigInt` of epochNanoseconds.
|
||||
pub(crate) fn from_epoch_nanos(nanos: &BigInt, offset: f64) -> TemporalResult<Self> { |
||||
// Skip the assert as nanos should be validated by Instant.
|
||||
// TODO: Determine whether value needs to be validated as integral.
|
||||
// Get the component ISO parts
|
||||
let mathematical_nanos = nanos.to_f64().ok_or_else(|| { |
||||
TemporalError::range().with_message("nanos was not within a valid range.") |
||||
})?; |
||||
|
||||
// 2. Let remainderNs be epochNanoseconds modulo 10^6.
|
||||
let remainder_nanos = mathematical_nanos % 1_000_000f64; |
||||
|
||||
// 3. Let epochMilliseconds be 𝔽((epochNanoseconds - remainderNs) / 10^6).
|
||||
let epoch_millis = ((mathematical_nanos - remainder_nanos) / 1_000_000f64).floor(); |
||||
|
||||
let year = utils::epoch_time_to_epoch_year(epoch_millis); |
||||
let month = utils::epoch_time_to_month_in_year(epoch_millis) + 1; |
||||
let day = utils::epoch_time_to_date(epoch_millis); |
||||
|
||||
// 7. Let hour be ℝ(! HourFromTime(epochMilliseconds)).
|
||||
let hour = (epoch_millis / 3_600_000f64).floor() % 24f64; |
||||
// 8. Let minute be ℝ(! MinFromTime(epochMilliseconds)).
|
||||
let minute = (epoch_millis / 60_000f64).floor() % 60f64; |
||||
// 9. Let second be ℝ(! SecFromTime(epochMilliseconds)).
|
||||
let second = (epoch_millis / 1000f64).floor() % 60f64; |
||||
// 10. Let millisecond be ℝ(! msFromTime(epochMilliseconds)).
|
||||
let millis = (epoch_millis % 1000f64).floor() % 1000f64; |
||||
|
||||
// 11. Let microsecond be floor(remainderNs / 1000).
|
||||
let micros = (remainder_nanos / 1000f64).floor(); |
||||
// 12. Assert: microsecond < 1000.
|
||||
debug_assert!(micros < 1000f64); |
||||
// 13. Let nanosecond be remainderNs modulo 1000.
|
||||
let nanos = (remainder_nanos % 1000f64).floor(); |
||||
|
||||
Ok(Self::balance( |
||||
year, |
||||
i32::from(month), |
||||
i32::from(day), |
||||
hour, |
||||
minute, |
||||
second, |
||||
millis, |
||||
micros, |
||||
nanos + offset, |
||||
)) |
||||
} |
||||
|
||||
#[allow(clippy::too_many_arguments)] |
||||
fn balance( |
||||
year: i32, |
||||
month: i32, |
||||
day: i32, |
||||
hour: f64, |
||||
minute: f64, |
||||
second: f64, |
||||
millisecond: f64, |
||||
microsecond: f64, |
||||
nanosecond: f64, |
||||
) -> Self { |
||||
let (overflow_day, time) = |
||||
IsoTime::balance(hour, minute, second, millisecond, microsecond, nanosecond); |
||||
let date = IsoDate::balance(year, month, day + overflow_day); |
||||
Self::new_unchecked(date, time) |
||||
} |
||||
|
||||
/// Returns whether the `IsoDateTime` is within valid limits.
|
||||
pub(crate) fn is_within_limits(&self) -> bool { |
||||
iso_dt_within_valid_limits(self.date, &self.time) |
||||
} |
||||
|
||||
pub(crate) const fn date(&self) -> &IsoDate { |
||||
&self.date |
||||
} |
||||
|
||||
pub(crate) const fn time(&self) -> &IsoTime { |
||||
&self.time |
||||
} |
||||
} |
||||
|
||||
// ==== `IsoDate` section ====
|
||||
|
||||
// TODO: Figure out `ICU4X` interop / replacement?
|
||||
|
||||
/// A trait for accessing the `IsoDate` across the various Temporal objects
|
||||
pub trait IsoDateSlots { |
||||
/// Returns the target's internal `IsoDate`.
|
||||
fn iso_date(&self) -> IsoDate; |
||||
} |
||||
|
||||
/// `IsoDate` serves as a record for the `[[ISOYear]]`, `[[ISOMonth]]`,
|
||||
/// and `[[ISODay]]` internal fields.
|
||||
///
|
||||
/// These fields are used for the `Temporal.PlainDate` object, the
|
||||
/// `Temporal.YearMonth` object, and the `Temporal.MonthDay` object.
|
||||
#[derive(Debug, Clone, Copy, Default)] |
||||
pub struct IsoDate { |
||||
pub(crate) year: i32, |
||||
pub(crate) month: u8, |
||||
pub(crate) day: u8, |
||||
} |
||||
|
||||
impl IsoDate { |
||||
/// Creates a new `IsoDate` without determining the validity.
|
||||
pub(crate) const fn new_unchecked(year: i32, month: u8, day: u8) -> Self { |
||||
Self { year, month, day } |
||||
} |
||||
|
||||
pub(crate) fn new( |
||||
year: i32, |
||||
month: i32, |
||||
day: i32, |
||||
overflow: ArithmeticOverflow, |
||||
) -> TemporalResult<Self> { |
||||
match overflow { |
||||
ArithmeticOverflow::Constrain => { |
||||
let month = month.clamp(1, 12); |
||||
let days_in_month = utils::iso_days_in_month(year, month); |
||||
let d = day.clamp(1, days_in_month); |
||||
// NOTE: Values are clamped in a u8 range.
|
||||
Ok(Self::new_unchecked(year, month as u8, d as u8)) |
||||
} |
||||
ArithmeticOverflow::Reject => { |
||||
if !is_valid_date(year, month, day) { |
||||
return Err(TemporalError::range().with_message("not a valid ISO date.")); |
||||
} |
||||
// NOTE: Values have been verified to be in a u8 range.
|
||||
Ok(Self::new_unchecked(year, month as u8, day as u8)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Create a balanced `IsoDate`
|
||||
///
|
||||
/// Equivalent to `BalanceISODate`.
|
||||
fn balance(year: i32, month: i32, day: i32) -> Self { |
||||
let epoch_days = iso_date_to_epoch_days(year, month - 1, day); |
||||
let ms = utils::epoch_days_to_epoch_ms(epoch_days, 0f64); |
||||
Self::new_unchecked( |
||||
utils::epoch_time_to_epoch_year(ms), |
||||
utils::epoch_time_to_month_in_year(ms) + 1, |
||||
utils::epoch_time_to_date(ms), |
||||
) |
||||
} |
||||
|
||||
/// Functionally the same as Date's abstract operation `MakeDay`
|
||||
///
|
||||
/// Equivalent to `IsoDateToEpochDays`
|
||||
pub(crate) fn to_epoch_days(self) -> i32 { |
||||
iso_date_to_epoch_days(self.year, self.month.into(), self.day.into()) |
||||
} |
||||
|
||||
/// Returns if the current `IsoDate` is valid.
|
||||
pub(crate) fn is_valid(self) -> bool { |
||||
is_valid_date(self.year, self.month.into(), self.day.into()) |
||||
} |
||||
|
||||
/// Returns the resulting `IsoDate` from adding a provided `Duration` to this `IsoDate`
|
||||
pub(crate) fn add_iso_date( |
||||
self, |
||||
duration: &DateDuration, |
||||
overflow: ArithmeticOverflow, |
||||
) -> TemporalResult<Self> { |
||||
// 1. Assert: year, month, day, years, months, weeks, and days are integers.
|
||||
// 2. Assert: overflow is either "constrain" or "reject".
|
||||
// 3. Let intermediate be ! BalanceISOYearMonth(year + years, month + months).
|
||||
let mut intermediate_year = self.year + duration.years() as i32; |
||||
let mut intermediate_month = i32::from(self.month) + duration.months() as i32; |
||||
|
||||
intermediate_year += (intermediate_month - 1) / 12; |
||||
intermediate_month = (intermediate_month - 1) % 12 + 1; |
||||
|
||||
// 4. Let intermediate be ? RegulateISODate(intermediate.[[Year]], intermediate.[[Month]], day, overflow).
|
||||
let intermediate = Self::new( |
||||
intermediate_year, |
||||
intermediate_month, |
||||
i32::from(self.day), |
||||
overflow, |
||||
)?; |
||||
|
||||
// 5. Set days to days + 7 × weeks.
|
||||
// 6. Let d be intermediate.[[Day]] + days.
|
||||
let additional_days = duration.days() as i32 + (duration.weeks() as i32 * 7); |
||||
let d = i32::from(intermediate.day) + additional_days; |
||||
|
||||
// 7. Return BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], d).
|
||||
Ok(Self::balance( |
||||
intermediate.year, |
||||
intermediate.month.into(), |
||||
d, |
||||
)) |
||||
} |
||||
} |
||||
|
||||
impl IsoDate { |
||||
/// Creates `[[ISOYear]]`, `[[isoMonth]]`, `[[isoDay]]` fields from `ICU4X`'s `Date<Iso>` struct.
|
||||
pub(crate) fn as_icu4x(self) -> TemporalResult<IcuDate<Iso>> { |
||||
IcuDate::try_new_iso_date(self.year, self.month, self.day) |
||||
.map_err(|e| TemporalError::range().with_message(e.to_string())) |
||||
} |
||||
} |
||||
|
||||
// ==== `IsoTime` section ====
|
||||
|
||||
/// An `IsoTime` record that contains `Temporal`'s
|
||||
/// time slots.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] |
||||
pub struct IsoTime { |
||||
pub(crate) hour: u8, // 0..=23
|
||||
pub(crate) minute: u8, // 0..=59
|
||||
pub(crate) second: u8, // 0..=59
|
||||
pub(crate) millisecond: u16, // 0..=999
|
||||
pub(crate) microsecond: u16, // 0..=999
|
||||
pub(crate) nanosecond: u16, // 0..=999
|
||||
} |
||||
|
||||
impl IsoTime { |
||||
/// Creates a new `IsoTime` without any validation.
|
||||
pub(crate) fn new_unchecked( |
||||
hour: u8, |
||||
minute: u8, |
||||
second: u8, |
||||
millisecond: u16, |
||||
microsecond: u16, |
||||
nanosecond: u16, |
||||
) -> Self { |
||||
Self { |
||||
hour, |
||||
minute, |
||||
second, |
||||
millisecond, |
||||
microsecond, |
||||
nanosecond, |
||||
} |
||||
} |
||||
|
||||
/// Creates a new regulated `IsoTime`.
|
||||
pub fn new( |
||||
hour: i32, |
||||
minute: i32, |
||||
second: i32, |
||||
millisecond: i32, |
||||
microsecond: i32, |
||||
nanosecond: i32, |
||||
overflow: ArithmeticOverflow, |
||||
) -> TemporalResult<IsoTime> { |
||||
match overflow { |
||||
ArithmeticOverflow::Constrain => { |
||||
let h = hour.clamp(0, 23) as u8; |
||||
let min = minute.clamp(0, 59) as u8; |
||||
let sec = second.clamp(0, 59) as u8; |
||||
let milli = millisecond.clamp(0, 999) as u16; |
||||
let micro = microsecond.clamp(0, 999) as u16; |
||||
let nano = nanosecond.clamp(0, 999) as u16; |
||||
Ok(Self::new_unchecked(h, min, sec, milli, micro, nano)) |
||||
} |
||||
ArithmeticOverflow::Reject => { |
||||
if !is_valid_time(hour, minute, second, millisecond, microsecond, nanosecond) { |
||||
return Err(TemporalError::range().with_message("IsoTime is not valid")); |
||||
}; |
||||
Ok(Self::new_unchecked( |
||||
hour as u8, |
||||
minute as u8, |
||||
second as u8, |
||||
millisecond as u16, |
||||
microsecond as u16, |
||||
nanosecond as u16, |
||||
)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Returns an `IsoTime` set to 12:00:00
|
||||
pub(crate) const fn noon() -> Self { |
||||
Self { |
||||
hour: 12, |
||||
minute: 0, |
||||
second: 0, |
||||
millisecond: 0, |
||||
microsecond: 0, |
||||
nanosecond: 0, |
||||
} |
||||
} |
||||
|
||||
/// Returns an `IsoTime` based off parse components.
|
||||
pub(crate) fn from_components( |
||||
hour: i32, |
||||
minute: i32, |
||||
second: i32, |
||||
fraction: f64, |
||||
) -> TemporalResult<Self> { |
||||
let millisecond = fraction * 1000f64; |
||||
let micros = millisecond.rem_euclid(1f64) * 1000f64; |
||||
let nanos = micros.rem_euclid(1f64).mul_add(1000f64, 0.5).floor(); |
||||
|
||||
Self::new( |
||||
hour, |
||||
minute, |
||||
second, |
||||
millisecond as i32, |
||||
micros as i32, |
||||
nanos as i32, |
||||
ArithmeticOverflow::Reject, |
||||
) |
||||
} |
||||
|
||||
// NOTE(nekevss): f64 is needed here as values could exceed i32 when input.
|
||||
/// Balances and creates a new `IsoTime` with `day` overflow from the provided values.
|
||||
pub(crate) fn balance( |
||||
hour: f64, |
||||
minute: f64, |
||||
second: f64, |
||||
millisecond: f64, |
||||
microsecond: f64, |
||||
nanosecond: f64, |
||||
) -> (i32, Self) { |
||||
// 1. Set microsecond to microsecond + floor(nanosecond / 1000).
|
||||
// 2. Set nanosecond to nanosecond modulo 1000.
|
||||
let (quotient, nanosecond) = div_mod(nanosecond, 1000f64); |
||||
let microsecond = microsecond + quotient; |
||||
|
||||
// 3. Set millisecond to millisecond + floor(microsecond / 1000).
|
||||
// 4. Set microsecond to microsecond modulo 1000.
|
||||
let (quotient, microsecond) = div_mod(microsecond, 1000f64); |
||||
let millisecond = millisecond + quotient; |
||||
|
||||
// 5. Set second to second + floor(millisecond / 1000).
|
||||
// 6. Set millisecond to millisecond modulo 1000.
|
||||
let (quotient, millisecond) = div_mod(millisecond, 1000f64); |
||||
let second = second + quotient; |
||||
|
||||
// 7. Set minute to minute + floor(second / 60).
|
||||
// 8. Set second to second modulo 60.
|
||||
let (quotient, second) = div_mod(second, 60f64); |
||||
let minute = minute + quotient; |
||||
|
||||
// 9. Set hour to hour + floor(minute / 60).
|
||||
// 10. Set minute to minute modulo 60.
|
||||
let (quotient, minute) = div_mod(minute, 60f64); |
||||
let hour = hour + quotient; |
||||
|
||||
// 11. Let days be floor(hour / 24).
|
||||
// 12. Set hour to hour modulo 24.
|
||||
let (days, hour) = div_mod(hour, 24f64); |
||||
|
||||
let time = Self::new_unchecked( |
||||
hour as u8, |
||||
minute as u8, |
||||
second as u8, |
||||
millisecond as u16, |
||||
microsecond as u16, |
||||
nanosecond as u16, |
||||
); |
||||
|
||||
(days as i32, time) |
||||
} |
||||
|
||||
// NOTE (nekevss): Specification seemed to be off / not entirely working, so the below was adapted from the
|
||||
// temporal-polyfill
|
||||
// TODO: DayLengthNS can probably be a u64, but keep as is for now and optimize.
|
||||
/// Rounds the current `IsoTime` according to the provided settings.
|
||||
pub(crate) fn round( |
||||
&self, |
||||
increment: f64, |
||||
unit: TemporalUnit, |
||||
mode: TemporalRoundingMode, |
||||
day_length_ns: Option<i64>, |
||||
) -> TemporalResult<(i32, Self)> { |
||||
// 1. Let fractionalSecond be nanosecond × 10-9 + microsecond × 10-6 + millisecond × 10-3 + second.
|
||||
|
||||
let quantity = match unit { |
||||
// 2. If unit is "day", then
|
||||
// a. If dayLengthNs is not present, set dayLengthNs to nsPerDay.
|
||||
// b. Let quantity be (((((hour × 60 + minute) × 60 + second) × 1000 + millisecond) × 1000 + microsecond) × 1000 + nanosecond) / dayLengthNs.
|
||||
// 3. Else if unit is "hour", then
|
||||
// a. Let quantity be (fractionalSecond / 60 + minute) / 60 + hour.
|
||||
TemporalUnit::Hour | TemporalUnit::Day => { |
||||
u64::from(self.nanosecond) |
||||
+ u64::from(self.microsecond) * 1_000 |
||||
+ u64::from(self.millisecond) * 1_000_000 |
||||
+ u64::from(self.second) * 1_000_000_000 |
||||
+ u64::from(self.minute) * 60 * 1_000_000_000 |
||||
+ u64::from(self.hour) * 60 * 60 * 1_000_000_000 |
||||
} |
||||
// 4. Else if unit is "minute", then
|
||||
// a. Let quantity be fractionalSecond / 60 + minute.
|
||||
TemporalUnit::Minute => { |
||||
u64::from(self.nanosecond) |
||||
+ u64::from(self.microsecond) * 1_000 |
||||
+ u64::from(self.millisecond) * 1_000_000 |
||||
+ u64::from(self.second) * 1_000_000_000 |
||||
+ u64::from(self.minute) * 60 |
||||
} |
||||
// 5. Else if unit is "second", then
|
||||
// a. Let quantity be fractionalSecond.
|
||||
TemporalUnit::Second => { |
||||
u64::from(self.nanosecond) |
||||
+ u64::from(self.microsecond) * 1_000 |
||||
+ u64::from(self.millisecond) * 1_000_000 |
||||
+ u64::from(self.second) * 1_000_000_000 |
||||
} |
||||
// 6. Else if unit is "millisecond", then
|
||||
// a. Let quantity be nanosecond × 10-6 + microsecond × 10-3 + millisecond.
|
||||
TemporalUnit::Millisecond => { |
||||
u64::from(self.nanosecond) |
||||
+ u64::from(self.microsecond) * 1_000 |
||||
+ u64::from(self.millisecond) * 1_000_000 |
||||
} |
||||
// 7. Else if unit is "microsecond", then
|
||||
// a. Let quantity be nanosecond × 10-3 + microsecond.
|
||||
TemporalUnit::Microsecond => { |
||||
u64::from(self.nanosecond) + 1_000 * u64::from(self.microsecond) |
||||
} |
||||
// 8. Else,
|
||||
// a. Assert: unit is "nanosecond".
|
||||
// b. Let quantity be nanosecond.
|
||||
TemporalUnit::Nanosecond => u64::from(self.nanosecond), |
||||
_ => { |
||||
return Err(TemporalError::range() |
||||
.with_message("Invalid temporal unit provided to Time.round.")) |
||||
} |
||||
}; |
||||
|
||||
let ns_per_unit = if unit == TemporalUnit::Day { |
||||
day_length_ns.unwrap_or(NS_PER_DAY) as f64 |
||||
} else { |
||||
unit.as_nanoseconds().expect("Only valid time values are ") |
||||
}; |
||||
|
||||
// TODO: Verify validity of cast or handle better.
|
||||
// 9. Let result be RoundNumberToIncrement(quantity, increment, roundingMode).
|
||||
let result = |
||||
utils::round_number_to_increment(quantity as f64, ns_per_unit * increment, mode) |
||||
/ ns_per_unit; |
||||
|
||||
let result = match unit { |
||||
// 10. If unit is "day", then
|
||||
// a. Return the Record { [[Days]]: result, [[Hour]]: 0, [[Minute]]: 0, [[Second]]: 0, [[Millisecond]]: 0, [[Microsecond]]: 0, [[Nanosecond]]: 0 }.
|
||||
TemporalUnit::Day => (result as i32, IsoTime::default()), |
||||
// 11. If unit is "hour", then
|
||||
// a. Return BalanceTime(result, 0, 0, 0, 0, 0).
|
||||
TemporalUnit::Hour => IsoTime::balance(result, 0.0, 0.0, 0.0, 0.0, 0.0), |
||||
// 12. If unit is "minute", then
|
||||
// a. Return BalanceTime(hour, result, 0, 0, 0, 0).
|
||||
TemporalUnit::Minute => { |
||||
IsoTime::balance(f64::from(self.hour), result, 0.0, 0.0, 0.0, 0.0) |
||||
} |
||||
// 13. If unit is "second", then
|
||||
// a. Return BalanceTime(hour, minute, result, 0, 0, 0).
|
||||
TemporalUnit::Second => IsoTime::balance( |
||||
f64::from(self.hour), |
||||
f64::from(self.minute), |
||||
result, |
||||
0.0, |
||||
0.0, |
||||
0.0, |
||||
), |
||||
// 14. If unit is "millisecond", then
|
||||
// a. Return BalanceTime(hour, minute, second, result, 0, 0).
|
||||
TemporalUnit::Millisecond => IsoTime::balance( |
||||
f64::from(self.hour), |
||||
f64::from(self.minute), |
||||
f64::from(self.second), |
||||
result, |
||||
0.0, |
||||
0.0, |
||||
), |
||||
// 15. If unit is "microsecond", then
|
||||
// a. Return BalanceTime(hour, minute, second, millisecond, result, 0).
|
||||
TemporalUnit::Microsecond => IsoTime::balance( |
||||
f64::from(self.hour), |
||||
f64::from(self.minute), |
||||
f64::from(self.second), |
||||
f64::from(self.millisecond), |
||||
result, |
||||
0.0, |
||||
), |
||||
// 16. Assert: unit is "nanosecond".
|
||||
// 17. Return BalanceTime(hour, minute, second, millisecond, microsecond, result).
|
||||
TemporalUnit::Nanosecond => IsoTime::balance( |
||||
f64::from(self.hour), |
||||
f64::from(self.minute), |
||||
f64::from(self.second), |
||||
f64::from(self.millisecond), |
||||
f64::from(self.microsecond), |
||||
result, |
||||
), |
||||
_ => unreachable!("Error is thrown in previous match."), |
||||
}; |
||||
|
||||
Ok(result) |
||||
} |
||||
|
||||
/// Checks if the time is a valid `IsoTime`
|
||||
pub(crate) fn is_valid(&self) -> bool { |
||||
if !(0..=23).contains(&self.hour) { |
||||
return false; |
||||
} |
||||
|
||||
let min_sec = 0..=59; |
||||
if !min_sec.contains(&self.minute) || !min_sec.contains(&self.second) { |
||||
return false; |
||||
} |
||||
|
||||
let sub_second = 0..=999; |
||||
sub_second.contains(&self.millisecond) |
||||
&& sub_second.contains(&self.microsecond) |
||||
&& sub_second.contains(&self.nanosecond) |
||||
} |
||||
|
||||
/// `IsoTimeToEpochMs`
|
||||
///
|
||||
/// Note: This method is library specific and not in spec
|
||||
///
|
||||
/// Functionally the same as Date's `MakeTime`
|
||||
pub(crate) fn to_epoch_ms(self) -> f64 { |
||||
((f64::from(self.hour) * utils::MS_PER_HOUR |
||||
+ f64::from(self.minute) * utils::MS_PER_MINUTE) |
||||
+ f64::from(self.second) * 1000f64) |
||||
+ f64::from(self.millisecond) |
||||
} |
||||
} |
||||
|
||||
// ==== `IsoDateTime` specific utility functions ====
|
||||
|
||||
#[inline] |
||||
/// Utility function to determine if a `DateTime`'s components create a `DateTime` within valid limits
|
||||
fn iso_dt_within_valid_limits(date: IsoDate, time: &IsoTime) -> bool { |
||||
if iso_date_to_epoch_days(date.year, (date.month).into(), date.day.into()).abs() > 100_000_001 { |
||||
return false; |
||||
} |
||||
let Some(ns) = utc_epoch_nanos(date, time, 0.0) else { |
||||
return false; |
||||
}; |
||||
|
||||
let max = BigInt::from(crate::NS_MAX_INSTANT + i128::from(NS_PER_DAY)); |
||||
let min = BigInt::from(crate::NS_MIN_INSTANT - i128::from(NS_PER_DAY)); |
||||
|
||||
min < ns && max > ns |
||||
} |
||||
|
||||
#[inline] |
||||
/// Utility function to convert a `IsoDate` and `IsoTime` values into epoch nanoseconds
|
||||
fn utc_epoch_nanos(date: IsoDate, time: &IsoTime, offset: f64) -> Option<BigInt> { |
||||
let ms = time.to_epoch_ms(); |
||||
let epoch_ms = utils::epoch_days_to_epoch_ms(date.to_epoch_days(), ms); |
||||
|
||||
let epoch_nanos = epoch_ms.mul_add( |
||||
1_000_000f64, |
||||
f64::from(time.microsecond).mul_add(1_000f64, f64::from(time.nanosecond)), |
||||
); |
||||
|
||||
BigInt::from_f64(epoch_nanos - offset) |
||||
} |
||||
|
||||
// ==== `IsoDate` specific utiltiy functions ====
|
||||
|
||||
/// Returns the Epoch days based off the given year, month, and day.
|
||||
#[inline] |
||||
fn iso_date_to_epoch_days(year: i32, month: i32, day: i32) -> i32 { |
||||
// 1. Let resolvedYear be year + floor(month / 12).
|
||||
let resolved_year = year + (f64::from(month) / 12_f64).floor() as i32; |
||||
// 2. Let resolvedMonth be month modulo 12.
|
||||
let resolved_month = month % 12; |
||||
|
||||
// 3. Find a time t such that EpochTimeToEpochYear(t) is resolvedYear, EpochTimeToMonthInYear(t) is resolvedMonth, and EpochTimeToDate(t) is 1.
|
||||
let year_t = utils::epoch_time_for_year(resolved_year); |
||||
let month_t = utils::epoch_time_for_month_given_year(resolved_month, resolved_year); |
||||
|
||||
// 4. Return EpochTimeToDayNumber(t) + date - 1.
|
||||
utils::epoch_time_to_day_number((year_t.abs() + month_t).copysign(year_t)) + day - 1 |
||||
} |
||||
|
||||
#[inline] |
||||
// Determines if the month and day are valid for the given year.
|
||||
fn is_valid_date(year: i32, month: i32, day: i32) -> bool { |
||||
if !(1..=12).contains(&month) { |
||||
return false; |
||||
} |
||||
|
||||
let days_in_month = utils::iso_days_in_month(year, month); |
||||
(1..=days_in_month).contains(&day) |
||||
} |
||||
|
||||
// ==== `IsoTime` specific utilities ====
|
||||
|
||||
#[inline] |
||||
fn is_valid_time(hour: i32, minute: i32, second: i32, ms: i32, mis: i32, ns: i32) -> bool { |
||||
if !(0..=23).contains(&hour) { |
||||
return false; |
||||
} |
||||
|
||||
let min_sec = 0..=59; |
||||
if !min_sec.contains(&minute) || !min_sec.contains(&second) { |
||||
return false; |
||||
} |
||||
|
||||
let sub_second = 0..=999; |
||||
sub_second.contains(&ms) && sub_second.contains(&mis) && sub_second.contains(&ns) |
||||
} |
||||
|
||||
// NOTE(nekevss): Considering the below: Balance can probably be altered from f64.
|
||||
#[inline] |
||||
fn div_mod(dividend: f64, divisor: f64) -> (f64, f64) { |
||||
(dividend.div_euclid(divisor), dividend.rem_euclid(divisor)) |
||||
} |
@ -1,72 +0,0 @@
|
||||
//! Boa's `boa_temporal` crate is an engine agnostic implementation of ECMAScript's Temporal.
|
||||
//!
|
||||
//! IMPORTANT NOTE: Please note that this library is actively being developed and is very
|
||||
//! much experimental and NOT STABLE.
|
||||
//!
|
||||
//! [`Temporal`][proposal] is the Stage 3 proposal for ECMAScript that provides new JS objects and functions
|
||||
//! for working with dates and times that fully supports time zones and non-gregorian calendars.
|
||||
//!
|
||||
//! This library's primary source is the Temporal Proposal [specification][spec].
|
||||
//!
|
||||
//! [proposal]: https://github.com/tc39/proposal-temporal
|
||||
//! [spec]: https://tc39.es/proposal-temporal/
|
||||
#![doc = include_str!("../ABOUT.md")] |
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg", |
||||
html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg" |
||||
)] |
||||
#![cfg_attr(not(test), forbid(clippy::unwrap_used))] |
||||
#![allow(
|
||||
// Currently throws a false positive regarding dependencies that are only used in benchmarks.
|
||||
unused_crate_dependencies, |
||||
clippy::module_name_repetitions, |
||||
clippy::redundant_pub_crate, |
||||
clippy::too_many_lines, |
||||
clippy::cognitive_complexity, |
||||
clippy::missing_errors_doc, |
||||
clippy::let_unit_value, |
||||
clippy::option_if_let_else, |
||||
|
||||
// It may be worth to look if we can fix the issues highlighted by these lints.
|
||||
clippy::cast_possible_truncation, |
||||
clippy::cast_sign_loss, |
||||
clippy::cast_precision_loss, |
||||
clippy::cast_possible_wrap, |
||||
|
||||
// Add temporarily - Needs addressing
|
||||
clippy::missing_panics_doc, |
||||
)] |
||||
|
||||
pub mod components; |
||||
pub mod error; |
||||
pub mod fields; |
||||
pub mod iso; |
||||
pub mod options; |
||||
pub mod parser; |
||||
|
||||
#[doc(hidden)] |
||||
pub(crate) mod utils; |
||||
|
||||
// TODO: evaluate positives and negatives of using tinystr.
|
||||
// Re-exporting tinystr as a convenience, as it is currently tied into the API.
|
||||
pub use tinystr::TinyAsciiStr; |
||||
|
||||
#[doc(inline)] |
||||
pub use error::TemporalError; |
||||
#[doc(inline)] |
||||
pub use fields::TemporalFields; |
||||
|
||||
/// The `Temporal` result type
|
||||
pub type TemporalResult<T> = Result<T, TemporalError>; |
||||
|
||||
// Relevant numeric constants
|
||||
/// Nanoseconds per day constant: 8.64e+13
|
||||
pub const NS_PER_DAY: i64 = MS_PER_DAY as i64 * 1_000_000; |
||||
/// Milliseconds per day constant: 8.64e+7
|
||||
pub const MS_PER_DAY: i32 = 24 * 60 * 60 * 1000; |
||||
/// Max Instant nanosecond constant
|
||||
#[doc(hidden)] |
||||
pub(crate) const NS_MAX_INSTANT: i128 = NS_PER_DAY as i128 * 100_000_000i128; |
||||
/// Min Instant nanosecond constant
|
||||
#[doc(hidden)] |
||||
pub(crate) const NS_MIN_INSTANT: i128 = -NS_MAX_INSTANT; |
@ -1,437 +0,0 @@
|
||||
//! Native implementation of the `Temporal` options.
|
||||
//!
|
||||
//! Temporal has various instances where user's can define options for how an
|
||||
//! operation may be completed.
|
||||
|
||||
use core::{fmt, str::FromStr}; |
||||
|
||||
use crate::TemporalError; |
||||
|
||||
// ==== Options enums and methods ====
|
||||
|
||||
/// The relevant unit that should be used for the operation that
|
||||
/// this option is provided as a value.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] |
||||
pub enum TemporalUnit { |
||||
/// The `Auto` unit
|
||||
Auto = 0, |
||||
/// The `Nanosecond` unit
|
||||
Nanosecond, |
||||
/// The `Microsecond` unit
|
||||
Microsecond, |
||||
/// The `Millisecond` unit
|
||||
Millisecond, |
||||
/// The `Second` unit
|
||||
Second, |
||||
/// The `Minute` unit
|
||||
Minute, |
||||
/// The `Hour` unit
|
||||
Hour, |
||||
/// The `Day` unit
|
||||
Day, |
||||
/// The `Week` unit
|
||||
Week, |
||||
/// The `Month` unit
|
||||
Month, |
||||
/// The `Year` unit
|
||||
Year, |
||||
} |
||||
|
||||
impl TemporalUnit { |
||||
#[inline] |
||||
#[must_use] |
||||
/// Returns the `MaximumRoundingIncrement` for the current `TemporalUnit`.
|
||||
pub fn to_maximum_rounding_increment(self) -> Option<u16> { |
||||
use TemporalUnit::{ |
||||
Auto, Day, Hour, Microsecond, Millisecond, Minute, Month, Nanosecond, Second, Week, |
||||
Year, |
||||
}; |
||||
// 1. If unit is "year", "month", "week", or "day", then
|
||||
// a. Return undefined.
|
||||
// 2. If unit is "hour", then
|
||||
// a. Return 24.
|
||||
// 3. If unit is "minute" or "second", then
|
||||
// a. Return 60.
|
||||
// 4. Assert: unit is one of "millisecond", "microsecond", or "nanosecond".
|
||||
// 5. Return 1000.
|
||||
match self { |
||||
Year | Month | Week | Day => None, |
||||
Hour => Some(24), |
||||
Minute | Second => Some(60), |
||||
Millisecond | Microsecond | Nanosecond => Some(1000), |
||||
Auto => unreachable!(), |
||||
} |
||||
} |
||||
|
||||
// TODO: potentiall use a u64
|
||||
/// Returns the `Nanosecond amount for any given value.`
|
||||
#[must_use] |
||||
pub fn as_nanoseconds(&self) -> Option<f64> { |
||||
use TemporalUnit::{ |
||||
Auto, Day, Hour, Microsecond, Millisecond, Minute, Month, Nanosecond, Second, Week, |
||||
Year, |
||||
}; |
||||
match self { |
||||
Year | Month | Week | Day | Auto => None, |
||||
Hour => Some(3600e9), |
||||
Minute => Some(60e9), |
||||
Second => Some(1e9), |
||||
Millisecond => Some(1e6), |
||||
Microsecond => Some(1e3), |
||||
Nanosecond => Some(1f64), |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// A parsing error for `TemporalUnit`
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct ParseTemporalUnitError; |
||||
|
||||
impl fmt::Display for ParseTemporalUnitError { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
f.write_str("provided string was not a valid TemporalUnit") |
||||
} |
||||
} |
||||
|
||||
impl FromStr for TemporalUnit { |
||||
type Err = ParseTemporalUnitError; |
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
match s { |
||||
"auto" => Ok(Self::Auto), |
||||
"year" | "years" => Ok(Self::Year), |
||||
"month" | "months" => Ok(Self::Month), |
||||
"week" | "weeks" => Ok(Self::Week), |
||||
"day" | "days" => Ok(Self::Day), |
||||
"hour" | "hours" => Ok(Self::Hour), |
||||
"minute" | "minutes" => Ok(Self::Minute), |
||||
"second" | "seconds" => Ok(Self::Second), |
||||
"millisecond" | "milliseconds" => Ok(Self::Millisecond), |
||||
"microsecond" | "microseconds" => Ok(Self::Microsecond), |
||||
"nanosecond" | "nanoseconds" => Ok(Self::Nanosecond), |
||||
_ => Err(ParseTemporalUnitError), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl fmt::Display for TemporalUnit { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
match self { |
||||
Self::Auto => "auto", |
||||
Self::Year => "constrain", |
||||
Self::Month => "month", |
||||
Self::Week => "week", |
||||
Self::Day => "day", |
||||
Self::Hour => "hour", |
||||
Self::Minute => "minute", |
||||
Self::Second => "second", |
||||
Self::Millisecond => "millsecond", |
||||
Self::Microsecond => "microsecond", |
||||
Self::Nanosecond => "nanosecond", |
||||
} |
||||
.fmt(f) |
||||
} |
||||
} |
||||
|
||||
/// `ArithmeticOverflow` can also be used as an
|
||||
/// assignment overflow and consists of the "constrain"
|
||||
/// and "reject" options.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
||||
pub enum ArithmeticOverflow { |
||||
/// Constrain option
|
||||
Constrain, |
||||
/// Constrain option
|
||||
Reject, |
||||
} |
||||
|
||||
/// A parsing error for `ArithemeticOverflow`
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct ParseArithmeticOverflowError; |
||||
|
||||
impl fmt::Display for ParseArithmeticOverflowError { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
f.write_str("provided string was not a valid overflow value") |
||||
} |
||||
} |
||||
|
||||
impl FromStr for ArithmeticOverflow { |
||||
type Err = ParseArithmeticOverflowError; |
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
match s { |
||||
"constrain" => Ok(Self::Constrain), |
||||
"reject" => Ok(Self::Reject), |
||||
_ => Err(ParseArithmeticOverflowError), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl fmt::Display for ArithmeticOverflow { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
match self { |
||||
Self::Constrain => "constrain", |
||||
Self::Reject => "reject", |
||||
} |
||||
.fmt(f) |
||||
} |
||||
} |
||||
|
||||
/// `Duration` overflow options.
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub enum DurationOverflow { |
||||
/// Constrain option
|
||||
Constrain, |
||||
/// Balance option
|
||||
Balance, |
||||
} |
||||
|
||||
/// A parsing error for `DurationOverflow`.
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct ParseDurationOverflowError; |
||||
|
||||
impl fmt::Display for ParseDurationOverflowError { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
f.write_str("provided string was not a valid duration overflow value") |
||||
} |
||||
} |
||||
|
||||
impl FromStr for DurationOverflow { |
||||
type Err = ParseDurationOverflowError; |
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
match s { |
||||
"constrain" => Ok(Self::Constrain), |
||||
"balance" => Ok(Self::Balance), |
||||
_ => Err(ParseDurationOverflowError), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl fmt::Display for DurationOverflow { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
match self { |
||||
Self::Constrain => "constrain", |
||||
Self::Balance => "balance", |
||||
} |
||||
.fmt(f) |
||||
} |
||||
} |
||||
|
||||
/// The disambiguation options for an instant.
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub enum InstantDisambiguation { |
||||
/// Compatible option
|
||||
Compatible, |
||||
/// Earlier option
|
||||
Earlier, |
||||
/// Later option
|
||||
Later, |
||||
/// Reject option
|
||||
Reject, |
||||
} |
||||
|
||||
/// A parsing error on `InstantDisambiguation` options.
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct ParseInstantDisambiguationError; |
||||
|
||||
impl fmt::Display for ParseInstantDisambiguationError { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
f.write_str("provided string was not a valid instant disambiguation value") |
||||
} |
||||
} |
||||
|
||||
impl FromStr for InstantDisambiguation { |
||||
type Err = ParseInstantDisambiguationError; |
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
match s { |
||||
"compatible" => Ok(Self::Compatible), |
||||
"earlier" => Ok(Self::Earlier), |
||||
"later" => Ok(Self::Later), |
||||
"reject" => Ok(Self::Reject), |
||||
_ => Err(ParseInstantDisambiguationError), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl fmt::Display for InstantDisambiguation { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
match self { |
||||
Self::Compatible => "compatible", |
||||
Self::Earlier => "earlier", |
||||
Self::Later => "later", |
||||
Self::Reject => "reject", |
||||
} |
||||
.fmt(f) |
||||
} |
||||
} |
||||
|
||||
/// Offset disambiguation options.
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub enum OffsetDisambiguation { |
||||
/// Use option
|
||||
Use, |
||||
/// Prefer option
|
||||
Prefer, |
||||
/// Ignore option
|
||||
Ignore, |
||||
/// Reject option
|
||||
Reject, |
||||
} |
||||
|
||||
/// A parsing error for `OffsetDisambiguation` parsing.
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct ParseOffsetDisambiguationError; |
||||
|
||||
impl fmt::Display for ParseOffsetDisambiguationError { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
f.write_str("provided string was not a valid offset disambiguation value") |
||||
} |
||||
} |
||||
|
||||
impl FromStr for OffsetDisambiguation { |
||||
type Err = ParseOffsetDisambiguationError; |
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
match s { |
||||
"use" => Ok(Self::Use), |
||||
"prefer" => Ok(Self::Prefer), |
||||
"ignore" => Ok(Self::Ignore), |
||||
"reject" => Ok(Self::Reject), |
||||
_ => Err(ParseOffsetDisambiguationError), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl fmt::Display for OffsetDisambiguation { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
match self { |
||||
Self::Use => "use", |
||||
Self::Prefer => "prefer", |
||||
Self::Ignore => "ignore", |
||||
Self::Reject => "reject", |
||||
} |
||||
.fmt(f) |
||||
} |
||||
} |
||||
|
||||
// TODO: Figure out what to do with intl's RoundingMode
|
||||
|
||||
/// Declares the specified `RoundingMode` for the operation.
|
||||
#[derive(Debug, Copy, Clone, Default)] |
||||
pub enum TemporalRoundingMode { |
||||
/// Ceil RoundingMode
|
||||
Ceil, |
||||
/// Floor RoundingMode
|
||||
Floor, |
||||
/// Expand RoundingMode
|
||||
Expand, |
||||
/// Truncate RoundingMode
|
||||
Trunc, |
||||
/// HalfCeil RoundingMode
|
||||
HalfCeil, |
||||
/// HalfFloor RoundingMode
|
||||
HalfFloor, |
||||
/// HalfExpand RoundingMode - Default
|
||||
#[default] |
||||
HalfExpand, |
||||
/// HalfTruncate RoundingMode
|
||||
HalfTrunc, |
||||
/// HalfEven RoundingMode
|
||||
HalfEven, |
||||
} |
||||
|
||||
/// The `UnsignedRoundingMode`
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
||||
pub enum TemporalUnsignedRoundingMode { |
||||
/// `Infinity` `RoundingMode`
|
||||
Infinity, |
||||
/// `Zero` `RoundingMode`
|
||||
Zero, |
||||
/// `HalfInfinity` `RoundingMode`
|
||||
HalfInfinity, |
||||
/// `HalfZero` `RoundingMode`
|
||||
HalfZero, |
||||
/// `HalfEven` `RoundingMode`
|
||||
HalfEven, |
||||
} |
||||
|
||||
impl TemporalRoundingMode { |
||||
#[inline] |
||||
#[must_use] |
||||
/// Negates the current `RoundingMode`.
|
||||
pub const fn negate(self) -> Self { |
||||
use TemporalRoundingMode::{ |
||||
Ceil, Expand, Floor, HalfCeil, HalfEven, HalfExpand, HalfFloor, HalfTrunc, Trunc, |
||||
}; |
||||
|
||||
match self { |
||||
Ceil => Self::Floor, |
||||
Floor => Self::Ceil, |
||||
HalfCeil => Self::HalfFloor, |
||||
HalfFloor => Self::HalfCeil, |
||||
Trunc => Self::Trunc, |
||||
Expand => Self::Expand, |
||||
HalfTrunc => Self::HalfTrunc, |
||||
HalfExpand => Self::HalfExpand, |
||||
HalfEven => Self::HalfEven, |
||||
} |
||||
} |
||||
|
||||
#[inline] |
||||
#[must_use] |
||||
/// Returns the `UnsignedRoundingMode`
|
||||
pub const fn get_unsigned_round_mode(self, is_negative: bool) -> TemporalUnsignedRoundingMode { |
||||
use TemporalRoundingMode::{ |
||||
Ceil, Expand, Floor, HalfCeil, HalfEven, HalfExpand, HalfFloor, HalfTrunc, Trunc, |
||||
}; |
||||
|
||||
match self { |
||||
Ceil if !is_negative => TemporalUnsignedRoundingMode::Infinity, |
||||
Ceil => TemporalUnsignedRoundingMode::Zero, |
||||
Floor if !is_negative => TemporalUnsignedRoundingMode::Zero, |
||||
Floor | Trunc | Expand => TemporalUnsignedRoundingMode::Infinity, |
||||
HalfCeil if !is_negative => TemporalUnsignedRoundingMode::HalfInfinity, |
||||
HalfCeil | HalfTrunc => TemporalUnsignedRoundingMode::HalfZero, |
||||
HalfFloor if !is_negative => TemporalUnsignedRoundingMode::HalfZero, |
||||
HalfFloor | HalfExpand => TemporalUnsignedRoundingMode::HalfInfinity, |
||||
HalfEven => TemporalUnsignedRoundingMode::HalfEven, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl FromStr for TemporalRoundingMode { |
||||
type Err = TemporalError; |
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
match s { |
||||
"ceil" => Ok(Self::Ceil), |
||||
"floor" => Ok(Self::Floor), |
||||
"expand" => Ok(Self::Expand), |
||||
"trunc" => Ok(Self::Trunc), |
||||
"halfCeil" => Ok(Self::HalfCeil), |
||||
"halfFloor" => Ok(Self::HalfFloor), |
||||
"halfExpand" => Ok(Self::HalfExpand), |
||||
"halfTrunc" => Ok(Self::HalfTrunc), |
||||
"halfEven" => Ok(Self::HalfEven), |
||||
_ => Err(TemporalError::range().with_message("RoundingMode not an accepted value.")), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl fmt::Display for TemporalRoundingMode { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
match self { |
||||
Self::Ceil => "ceil", |
||||
Self::Floor => "floor", |
||||
Self::Expand => "expand", |
||||
Self::Trunc => "trunc", |
||||
Self::HalfCeil => "halfCeil", |
||||
Self::HalfFloor => "halfFloor", |
||||
Self::HalfExpand => "halfExpand", |
||||
Self::HalfTrunc => "halfTrunc", |
||||
Self::HalfEven => "halfEven", |
||||
} |
||||
.fmt(f) |
||||
} |
||||
} |
@ -1,180 +0,0 @@
|
||||
/// Parsing for Temporal's `Annotations`.
|
||||
use crate::{ |
||||
assert_syntax, |
||||
parser::{ |
||||
grammar::{ |
||||
is_a_key_char, is_a_key_leading_char, is_annotation_close, |
||||
is_annotation_key_value_separator, is_annotation_value_component, is_critical_flag, |
||||
}, |
||||
time_zone, |
||||
time_zone::TimeZoneAnnotation, |
||||
Cursor, |
||||
}, |
||||
TemporalError, TemporalResult, |
||||
}; |
||||
|
||||
use super::grammar::{is_annotation_open, is_hyphen}; |
||||
|
||||
/// A `KeyValueAnnotation` Parse Node.
|
||||
#[derive(Debug, Clone)] |
||||
pub(crate) struct KeyValueAnnotation { |
||||
/// An `Annotation`'s Key.
|
||||
pub(crate) key: String, |
||||
/// An `Annotation`'s value.
|
||||
pub(crate) value: String, |
||||
/// Whether the annotation was flagged as critical.
|
||||
pub(crate) critical: bool, |
||||
} |
||||
|
||||
/// Strictly a Parsing Intermediary for the checking the common annotation backing.
|
||||
pub(crate) struct AnnotationSet { |
||||
pub(crate) tz: Option<TimeZoneAnnotation>, |
||||
pub(crate) calendar: Option<String>, |
||||
} |
||||
|
||||
/// Parse a `TimeZoneAnnotation` `Annotations` set
|
||||
pub(crate) fn parse_annotation_set( |
||||
zoned: bool, |
||||
cursor: &mut Cursor, |
||||
) -> TemporalResult<AnnotationSet> { |
||||
// Parse the first annotation.
|
||||
let tz_annotation = time_zone::parse_ambiguous_tz_annotation(cursor)?; |
||||
if tz_annotation.is_none() && zoned { |
||||
return Err( |
||||
TemporalError::syntax().with_message("ZonedDateTime must have a TimeZone annotation.") |
||||
); |
||||
} |
||||
|
||||
// Parse any `Annotations`
|
||||
let annotations = cursor.check_or(false, is_annotation_open); |
||||
|
||||
if annotations { |
||||
let annotations = parse_annotations(cursor)?; |
||||
return Ok(AnnotationSet { |
||||
tz: tz_annotation, |
||||
calendar: annotations.calendar, |
||||
}); |
||||
} |
||||
|
||||
Ok(AnnotationSet { |
||||
tz: tz_annotation, |
||||
calendar: None, |
||||
}) |
||||
} |
||||
|
||||
/// An internal crate type to house any recognized annotations that are found.
|
||||
#[derive(Default)] |
||||
pub(crate) struct RecognizedAnnotations { |
||||
pub(crate) calendar: Option<String>, |
||||
} |
||||
|
||||
/// Parse any number of `KeyValueAnnotation`s
|
||||
pub(crate) fn parse_annotations(cursor: &mut Cursor) -> TemporalResult<RecognizedAnnotations> { |
||||
let mut annotations = RecognizedAnnotations::default(); |
||||
|
||||
let mut calendar_crit = false; |
||||
while cursor.check_or(false, is_annotation_open) { |
||||
let kv = parse_kv_annotation(cursor)?; |
||||
|
||||
if &kv.key == "u-ca" { |
||||
if annotations.calendar.is_none() { |
||||
annotations.calendar = Some(kv.value); |
||||
calendar_crit = kv.critical; |
||||
continue; |
||||
} |
||||
|
||||
if calendar_crit || kv.critical { |
||||
return Err(TemporalError::syntax().with_message( |
||||
"Cannot have critical flag with duplicate calendar annotations", |
||||
)); |
||||
} |
||||
} else if kv.critical { |
||||
return Err(TemporalError::syntax().with_message("Unrecognized critical annotation.")); |
||||
} |
||||
} |
||||
|
||||
Ok(annotations) |
||||
} |
||||
|
||||
/// Parse an annotation with an `AnnotationKey`=`AnnotationValue` pair.
|
||||
fn parse_kv_annotation(cursor: &mut Cursor) -> TemporalResult<KeyValueAnnotation> { |
||||
assert_syntax!( |
||||
is_annotation_open(cursor.abrupt_next()?), |
||||
"Invalid annotation open character." |
||||
); |
||||
|
||||
let critical = cursor.check_or(false, is_critical_flag); |
||||
cursor.advance_if(critical); |
||||
|
||||
// Parse AnnotationKey.
|
||||
let annotation_key = parse_annotation_key(cursor)?; |
||||
assert_syntax!( |
||||
is_annotation_key_value_separator(cursor.abrupt_next()?), |
||||
"Invalid annotation key-value separator" |
||||
); |
||||
|
||||
// Parse AnnotationValue.
|
||||
let annotation_value = parse_annotation_value(cursor)?; |
||||
assert_syntax!( |
||||
is_annotation_close(cursor.abrupt_next()?), |
||||
"Invalid annotion closing character" |
||||
); |
||||
|
||||
Ok(KeyValueAnnotation { |
||||
key: annotation_key, |
||||
value: annotation_value, |
||||
critical, |
||||
}) |
||||
} |
||||
|
||||
/// Parse an `AnnotationKey`.
|
||||
fn parse_annotation_key(cursor: &mut Cursor) -> TemporalResult<String> { |
||||
let key_start = cursor.pos(); |
||||
assert_syntax!( |
||||
is_a_key_leading_char(cursor.abrupt_next()?), |
||||
"Invalid key leading character." |
||||
); |
||||
|
||||
while let Some(potential_key_char) = cursor.next() { |
||||
// End of key.
|
||||
if cursor.check_or(false, is_annotation_key_value_separator) { |
||||
// Return found key
|
||||
return Ok(cursor.slice(key_start, cursor.pos())); |
||||
} |
||||
|
||||
assert_syntax!( |
||||
is_a_key_char(potential_key_char), |
||||
"Invalid annotation key character." |
||||
); |
||||
} |
||||
|
||||
Err(TemporalError::abrupt_end()) |
||||
} |
||||
|
||||
/// Parse an `AnnotationValue`.
|
||||
fn parse_annotation_value(cursor: &mut Cursor) -> TemporalResult<String> { |
||||
let value_start = cursor.pos(); |
||||
cursor.advance(); |
||||
while let Some(potential_value_char) = cursor.next() { |
||||
if cursor.check_or(false, is_annotation_close) { |
||||
// Return the determined AnnotationValue.
|
||||
return Ok(cursor.slice(value_start, cursor.pos())); |
||||
} |
||||
|
||||
if is_hyphen(potential_value_char) { |
||||
assert_syntax!( |
||||
cursor.peek().map_or(false, is_annotation_value_component), |
||||
"Missing annotation value compoenent after '-'" |
||||
); |
||||
cursor.advance(); |
||||
continue; |
||||
} |
||||
|
||||
assert_syntax!( |
||||
is_annotation_value_component(potential_value_char), |
||||
"Invalid annotation value component character." |
||||
); |
||||
} |
||||
|
||||
Err(TemporalError::abrupt_end()) |
||||
} |
@ -1,311 +0,0 @@
|
||||
//! Parsing for Temporal's ISO8601 `Date` and `DateTime`.
|
||||
|
||||
use crate::{ |
||||
assert_syntax, |
||||
parser::{ |
||||
annotations, |
||||
grammar::{is_date_time_separator, is_sign, is_utc_designator}, |
||||
nodes::TimeZone, |
||||
time, |
||||
time::TimeSpec, |
||||
time_zone, Cursor, IsoParseRecord, |
||||
}, |
||||
TemporalError, TemporalResult, |
||||
}; |
||||
|
||||
use super::grammar::{is_annotation_open, is_hyphen}; |
||||
use bitflags::bitflags; |
||||
|
||||
#[derive(Debug, Default, Clone)] |
||||
/// A `DateTime` Parse Node that contains the date, time, and offset info.
|
||||
pub(crate) struct DateTimeRecord { |
||||
/// Date
|
||||
pub(crate) date: DateRecord, |
||||
/// Time
|
||||
pub(crate) time: Option<TimeSpec>, |
||||
/// Tz Offset
|
||||
pub(crate) time_zone: Option<TimeZone>, |
||||
} |
||||
|
||||
#[derive(Default, Debug, Clone, Copy)] |
||||
/// The record of a parsed date.
|
||||
pub(crate) struct DateRecord { |
||||
/// Date Year
|
||||
pub(crate) year: i32, |
||||
/// Date Month
|
||||
pub(crate) month: i32, |
||||
/// Date Day
|
||||
pub(crate) day: i32, |
||||
} |
||||
|
||||
bitflags! { |
||||
/// Parsing flags for `AnnotatedDateTime` parsing.
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct DateTimeFlags: u8 { |
||||
const ZONED = 0b0000_0001; |
||||
const TIME_REQ = 0b0000_0010; |
||||
const UTC_REQ = 0b0000_0100; |
||||
} |
||||
} |
||||
|
||||
/// This function handles parsing for [`AnnotatedDateTime`][datetime],
|
||||
/// [`AnnotatedDateTimeTimeRequred`][time], and
|
||||
/// [`TemporalInstantString.`][instant] according to the requirements
|
||||
/// provided via Spec.
|
||||
///
|
||||
/// [datetime]: https://tc39.es/proposal-temporal/#prod-AnnotatedDateTime
|
||||
/// [time]: https://tc39.es/proposal-temporal/#prod-AnnotatedDateTimeTimeRequired
|
||||
/// [instant]: https://tc39.es/proposal-temporal/#prod-TemporalInstantString
|
||||
pub(crate) fn parse_annotated_date_time( |
||||
flags: DateTimeFlags, |
||||
cursor: &mut Cursor, |
||||
) -> TemporalResult<IsoParseRecord> { |
||||
let date_time = parse_date_time( |
||||
flags.contains(DateTimeFlags::TIME_REQ), |
||||
flags.contains(DateTimeFlags::UTC_REQ), |
||||
cursor, |
||||
)?; |
||||
|
||||
// Peek Annotation presence
|
||||
// Throw error if annotation does not exist and zoned is true, else return.
|
||||
if !cursor.check_or(false, is_annotation_open) { |
||||
if flags.contains(DateTimeFlags::ZONED) { |
||||
return Err(TemporalError::syntax() |
||||
.with_message("ZonedDateTime must have a TimeZoneAnnotation.")); |
||||
} |
||||
|
||||
cursor.close()?; |
||||
|
||||
return Ok(IsoParseRecord { |
||||
date: date_time.date, |
||||
time: date_time.time, |
||||
tz: date_time.time_zone, |
||||
calendar: None, |
||||
}); |
||||
} |
||||
|
||||
let mut tz = TimeZone::default(); |
||||
|
||||
if let Some(tz_info) = date_time.time_zone { |
||||
tz = tz_info; |
||||
} |
||||
|
||||
let annotation_set = |
||||
annotations::parse_annotation_set(flags.contains(DateTimeFlags::ZONED), cursor)?; |
||||
|
||||
if let Some(annotated_tz) = annotation_set.tz { |
||||
tz = annotated_tz.tz; |
||||
} |
||||
|
||||
let tz = if tz.name.is_some() || tz.offset.is_some() { |
||||
Some(tz) |
||||
} else { |
||||
None |
||||
}; |
||||
|
||||
cursor.close()?; |
||||
|
||||
Ok(IsoParseRecord { |
||||
date: date_time.date, |
||||
time: date_time.time, |
||||
tz, |
||||
calendar: annotation_set.calendar, |
||||
}) |
||||
} |
||||
|
||||
/// Parses a `DateTime` record.
|
||||
fn parse_date_time( |
||||
time_required: bool, |
||||
utc_required: bool, |
||||
cursor: &mut Cursor, |
||||
) -> TemporalResult<DateTimeRecord> { |
||||
let date = parse_date(cursor)?; |
||||
|
||||
// If there is no `DateTimeSeparator`, return date early.
|
||||
if !cursor.check_or(false, is_date_time_separator) { |
||||
if time_required { |
||||
return Err(TemporalError::syntax().with_message("Missing a required Time target.")); |
||||
} |
||||
|
||||
return Ok(DateTimeRecord { |
||||
date, |
||||
time: None, |
||||
time_zone: None, |
||||
}); |
||||
} |
||||
|
||||
cursor.advance(); |
||||
|
||||
let time = time::parse_time_spec(cursor)?; |
||||
|
||||
let time_zone = if cursor.check_or(false, |ch| is_sign(ch) || is_utc_designator(ch)) { |
||||
Some(time_zone::parse_date_time_utc(cursor)?) |
||||
} else { |
||||
if utc_required { |
||||
return Err(TemporalError::syntax().with_message("DateTimeUTCOffset is required.")); |
||||
} |
||||
None |
||||
}; |
||||
|
||||
Ok(DateTimeRecord { |
||||
date, |
||||
time: Some(time), |
||||
time_zone, |
||||
}) |
||||
} |
||||
|
||||
/// Parses `Date` record.
|
||||
fn parse_date(cursor: &mut Cursor) -> TemporalResult<DateRecord> { |
||||
let year = parse_date_year(cursor)?; |
||||
let hyphenated = cursor |
||||
.check(is_hyphen) |
||||
.ok_or_else(TemporalError::abrupt_end)?; |
||||
|
||||
cursor.advance_if(hyphenated); |
||||
|
||||
let month = parse_date_month(cursor)?; |
||||
|
||||
if hyphenated { |
||||
assert_syntax!(cursor.check_or(false, is_hyphen), "Invalid hyphen usage."); |
||||
} |
||||
cursor.advance_if(cursor.check_or(false, is_hyphen)); |
||||
|
||||
let day = parse_date_day(cursor)?; |
||||
|
||||
Ok(DateRecord { year, month, day }) |
||||
} |
||||
|
||||
// ==== `YearMonth` and `MonthDay` parsing functions ====
|
||||
|
||||
/// Parses a `DateSpecYearMonth`
|
||||
pub(crate) fn parse_year_month(cursor: &mut Cursor) -> TemporalResult<(i32, i32)> { |
||||
let year = parse_date_year(cursor)?; |
||||
|
||||
cursor.advance_if(cursor.check_or(false, is_hyphen)); |
||||
|
||||
let month = parse_date_month(cursor)?; |
||||
|
||||
assert_syntax!( |
||||
cursor.check_or(true, is_annotation_open), |
||||
"Expected an end or AnnotationOpen" |
||||
); |
||||
|
||||
Ok((year, month)) |
||||
} |
||||
|
||||
/// Parses a `DateSpecMonthDay`
|
||||
pub(crate) fn parse_month_day(cursor: &mut Cursor) -> TemporalResult<(i32, i32)> { |
||||
let dash_one = cursor |
||||
.check(is_hyphen) |
||||
.ok_or_else(TemporalError::abrupt_end)?; |
||||
let dash_two = cursor |
||||
.peek() |
||||
.map(is_hyphen) |
||||
.ok_or_else(TemporalError::abrupt_end)?; |
||||
|
||||
if dash_two && dash_one { |
||||
cursor.advance_n(2); |
||||
} else if dash_two && !dash_one { |
||||
return Err(TemporalError::syntax().with_message("MonthDay requires two dashes")); |
||||
} |
||||
|
||||
let month = parse_date_month(cursor)?; |
||||
|
||||
cursor.advance_if(cursor.check_or(false, is_hyphen)); |
||||
|
||||
let day = parse_date_day(cursor)?; |
||||
|
||||
assert_syntax!( |
||||
cursor.check_or(true, is_annotation_open), |
||||
"Expected an end or AnnotationOpen" |
||||
); |
||||
|
||||
Ok((month, day)) |
||||
} |
||||
|
||||
// ==== Unit Parsers ====
|
||||
|
||||
fn parse_date_year(cursor: &mut Cursor) -> TemporalResult<i32> { |
||||
if cursor.check_or(false, is_sign) { |
||||
let sign = if cursor.expect_next() == '+' { 1 } else { -1 }; |
||||
let year_start = cursor.pos(); |
||||
|
||||
for _ in 0..6 { |
||||
let year_digit = cursor.abrupt_next()?; |
||||
assert_syntax!( |
||||
year_digit.is_ascii_digit(), |
||||
"Year must be made up of digits." |
||||
); |
||||
} |
||||
|
||||
let year_value = cursor |
||||
.slice(year_start, cursor.pos()) |
||||
.parse::<i32>() |
||||
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?; |
||||
|
||||
// 13.30.1 Static Semantics: Early Errors
|
||||
//
|
||||
// It is a Syntax Error if DateYear is "-000000" or "−000000" (U+2212 MINUS SIGN followed by 000000).
|
||||
if sign == -1 && year_value == 0 { |
||||
return Err(TemporalError::syntax().with_message("Cannot have negative 0 years.")); |
||||
} |
||||
|
||||
let year = sign * year_value; |
||||
|
||||
if !(-271_820..=275_760).contains(&year) { |
||||
return Err(TemporalError::range() |
||||
.with_message("Year is outside of the minimum supported range.")); |
||||
} |
||||
|
||||
return Ok(year); |
||||
} |
||||
|
||||
let year_start = cursor.pos(); |
||||
|
||||
for _ in 0..4 { |
||||
let year_digit = cursor.abrupt_next()?; |
||||
assert_syntax!( |
||||
year_digit.is_ascii_digit(), |
||||
"Year must be made up of digits." |
||||
); |
||||
} |
||||
|
||||
let year_value = cursor |
||||
.slice(year_start, cursor.pos()) |
||||
.parse::<i32>() |
||||
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?; |
||||
|
||||
Ok(year_value) |
||||
} |
||||
|
||||
fn parse_date_month(cursor: &mut Cursor) -> TemporalResult<i32> { |
||||
let start = cursor.pos(); |
||||
for _ in 0..2 { |
||||
let digit = cursor.abrupt_next()?; |
||||
assert_syntax!(digit.is_ascii_digit(), "Month must be a digit"); |
||||
} |
||||
let month_value = cursor |
||||
.slice(start, cursor.pos()) |
||||
.parse::<i32>() |
||||
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?; |
||||
if !(1..=12).contains(&month_value) { |
||||
return Err(TemporalError::syntax().with_message("DateMonth must be in a range of 1-12")); |
||||
} |
||||
Ok(month_value) |
||||
} |
||||
|
||||
fn parse_date_day(cursor: &mut Cursor) -> TemporalResult<i32> { |
||||
let start = cursor.pos(); |
||||
for _ in 0..2 { |
||||
let digit = cursor.abrupt_next()?; |
||||
assert_syntax!(digit.is_ascii_digit(), "Date must be a digit"); |
||||
} |
||||
let day_value = cursor |
||||
.slice(start, cursor.pos()) |
||||
.parse::<i32>() |
||||
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?; |
||||
if !(1..=31).contains(&day_value) { |
||||
return Err(TemporalError::syntax().with_message("DateDay must be in a range of 1-31")); |
||||
} |
||||
Ok(day_value) |
||||
} |
@ -1,247 +0,0 @@
|
||||
use crate::{ |
||||
assert_syntax, |
||||
parser::{ |
||||
grammar::{ |
||||
is_day_designator, is_decimal_separator, is_duration_designator, is_hour_designator, |
||||
is_minute_designator, is_month_designator, is_second_designator, is_sign, |
||||
is_time_designator, is_week_designator, is_year_designator, |
||||
}, |
||||
time::parse_fraction, |
||||
Cursor, |
||||
}, |
||||
TemporalError, TemporalResult, |
||||
}; |
||||
|
||||
/// An ISO8601 `DurationRecord` Parse Node.
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub(crate) struct DurationParseRecord { |
||||
/// Duration Sign
|
||||
pub(crate) sign: bool, |
||||
/// A `DateDuration` record.
|
||||
pub(crate) date: DateDuration, |
||||
/// A `TimeDuration` record.
|
||||
pub(crate) time: TimeDuration, |
||||
} |
||||
|
||||
/// A `DateDuration` Parse Node.
|
||||
#[derive(Default, Debug, Clone, Copy)] |
||||
pub(crate) struct DateDuration { |
||||
/// Years value.
|
||||
pub(crate) years: i32, |
||||
/// Months value.
|
||||
pub(crate) months: i32, |
||||
/// Weeks value.
|
||||
pub(crate) weeks: i32, |
||||
/// Days value.
|
||||
pub(crate) days: i32, |
||||
} |
||||
|
||||
/// A `TimeDuration` Parse Node
|
||||
#[derive(Default, Debug, Clone, Copy)] |
||||
pub(crate) struct TimeDuration { |
||||
/// Hours value.
|
||||
pub(crate) hours: i32, |
||||
/// Hours fraction value.
|
||||
pub(crate) fhours: f64, |
||||
/// Minutes value with fraction.
|
||||
pub(crate) minutes: i32, |
||||
/// Minutes fraction value.
|
||||
pub(crate) fminutes: f64, |
||||
/// Seconds value with fraction.
|
||||
pub(crate) seconds: i32, |
||||
/// Seconds fraction value,
|
||||
pub(crate) fseconds: f64, |
||||
} |
||||
|
||||
pub(crate) fn parse_duration(cursor: &mut Cursor) -> TemporalResult<DurationParseRecord> { |
||||
let sign = if cursor |
||||
.check(is_sign) |
||||
.ok_or_else(TemporalError::abrupt_end)? |
||||
{ |
||||
cursor.expect_next() == '+' |
||||
} else { |
||||
true |
||||
}; |
||||
|
||||
assert_syntax!( |
||||
is_duration_designator(cursor.abrupt_next()?), |
||||
"DurationDisgnator is missing." |
||||
); |
||||
|
||||
let date = if cursor.check_or(false, is_time_designator) { |
||||
Some(DateDuration::default()) |
||||
} else { |
||||
Some(parse_date_duration(cursor)?) |
||||
}; |
||||
|
||||
let time = if cursor.check_or(false, is_time_designator) { |
||||
cursor.advance(); |
||||
Some(parse_time_duration(cursor)?) |
||||
} else { |
||||
None |
||||
}; |
||||
|
||||
cursor.close()?; |
||||
|
||||
Ok(DurationParseRecord { |
||||
sign, |
||||
date: date.unwrap_or_default(), |
||||
time: time.unwrap_or_default(), |
||||
}) |
||||
} |
||||
|
||||
#[derive(PartialEq, PartialOrd, Eq, Ord)] |
||||
enum DateUnit { |
||||
None = 0, |
||||
Year, |
||||
Month, |
||||
Week, |
||||
Day, |
||||
} |
||||
|
||||
pub(crate) fn parse_date_duration(cursor: &mut Cursor) -> TemporalResult<DateDuration> { |
||||
let mut date = DateDuration::default(); |
||||
|
||||
let mut previous_unit = DateUnit::None; |
||||
while cursor.check_or(false, |ch| ch.is_ascii_digit()) { |
||||
let digit_start = cursor.pos(); |
||||
|
||||
while cursor.next().is_some() { |
||||
if !cursor.check_or(false, |ch| ch.is_ascii_digit()) { |
||||
break; |
||||
} |
||||
} |
||||
|
||||
let value = cursor |
||||
.slice(digit_start, cursor.pos()) |
||||
.parse::<i32>() |
||||
.map_err(|err| TemporalError::syntax().with_message(err.to_string()))?; |
||||
|
||||
match cursor.next() { |
||||
Some(ch) if is_year_designator(ch) => { |
||||
if previous_unit > DateUnit::Year { |
||||
return Err( |
||||
TemporalError::syntax().with_message("Not a valid DateDuration order") |
||||
); |
||||
} |
||||
date.years = value; |
||||
previous_unit = DateUnit::Year; |
||||
} |
||||
Some(ch) if is_month_designator(ch) => { |
||||
if previous_unit > DateUnit::Month { |
||||
return Err( |
||||
TemporalError::syntax().with_message("Not a valid DateDuration order") |
||||
); |
||||
} |
||||
date.months = value; |
||||
previous_unit = DateUnit::Month; |
||||
} |
||||
Some(ch) if is_week_designator(ch) => { |
||||
if previous_unit > DateUnit::Week { |
||||
return Err( |
||||
TemporalError::syntax().with_message("Not a valid DateDuration order") |
||||
); |
||||
} |
||||
date.weeks = value; |
||||
previous_unit = DateUnit::Week; |
||||
} |
||||
Some(ch) if is_day_designator(ch) => { |
||||
if previous_unit > DateUnit::Day { |
||||
return Err( |
||||
TemporalError::syntax().with_message("Not a valid DateDuration order") |
||||
); |
||||
} |
||||
date.days = value; |
||||
previous_unit = DateUnit::Day; |
||||
} |
||||
Some(_) | None => return Err(TemporalError::abrupt_end()), |
||||
} |
||||
} |
||||
|
||||
Ok(date) |
||||
} |
||||
|
||||
#[derive(PartialEq, PartialOrd, Eq, Ord)] |
||||
enum TimeUnit { |
||||
None = 0, |
||||
Hour, |
||||
Minute, |
||||
Second, |
||||
} |
||||
|
||||
pub(crate) fn parse_time_duration(cursor: &mut Cursor) -> TemporalResult<TimeDuration> { |
||||
let mut time = TimeDuration::default(); |
||||
|
||||
assert_syntax!( |
||||
cursor.check_or(false, |ch| ch.is_ascii_digit()), |
||||
"TimeDuration designator must have values after." |
||||
); |
||||
|
||||
let mut previous_unit = TimeUnit::None; |
||||
let mut fraction_present = false; |
||||
while cursor.check_or(false, |ch| ch.is_ascii_digit()) { |
||||
let digit_start = cursor.pos(); |
||||
|
||||
while cursor.next().is_some() { |
||||
if !cursor.check_or(false, |ch| ch.is_ascii_digit()) { |
||||
break; |
||||
} |
||||
} |
||||
|
||||
let value = cursor |
||||
.slice(digit_start, cursor.pos()) |
||||
.parse::<i32>() |
||||
.map_err(|err| TemporalError::syntax().with_message(err.to_string()))?; |
||||
|
||||
let fraction = if cursor.check_or(false, is_decimal_separator) { |
||||
fraction_present = true; |
||||
parse_fraction(cursor)? |
||||
} else { |
||||
0.0 |
||||
}; |
||||
|
||||
match cursor.next() { |
||||
Some(ch) if is_hour_designator(ch) => { |
||||
if previous_unit > TimeUnit::Hour { |
||||
return Err( |
||||
TemporalError::syntax().with_message("Not a valid DateDuration order") |
||||
); |
||||
} |
||||
time.hours = value; |
||||
time.fhours = fraction; |
||||
previous_unit = TimeUnit::Hour; |
||||
} |
||||
Some(ch) if is_minute_designator(ch) => { |
||||
if previous_unit > TimeUnit::Minute { |
||||
return Err( |
||||
TemporalError::syntax().with_message("Not a valid DateDuration order") |
||||
); |
||||
} |
||||
time.minutes = value; |
||||
time.fminutes = fraction; |
||||
previous_unit = TimeUnit::Minute; |
||||
} |
||||
Some(ch) if is_second_designator(ch) => { |
||||
if previous_unit > TimeUnit::Second { |
||||
return Err( |
||||
TemporalError::syntax().with_message("Not a valid DateDuration order") |
||||
); |
||||
} |
||||
time.seconds = value; |
||||
time.fseconds = fraction; |
||||
previous_unit = TimeUnit::Second; |
||||
} |
||||
Some(_) | None => return Err(TemporalError::abrupt_end()), |
||||
} |
||||
|
||||
if fraction_present { |
||||
assert_syntax!( |
||||
cursor.check_or(true, |ch| !ch.is_ascii_digit()), |
||||
"Invalid duration value provided after fraction." |
||||
); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
Ok(time) |
||||
} |
@ -1,136 +0,0 @@
|
||||
//! ISO8601 specific grammar checks.
|
||||
|
||||
/// Checks if char is a `AKeyLeadingChar`.
|
||||
#[inline] |
||||
pub(crate) const fn is_a_key_leading_char(ch: char) -> bool { |
||||
ch.is_ascii_lowercase() || ch == '_' |
||||
} |
||||
|
||||
/// Checks if char is an `AKeyChar`.
|
||||
#[inline] |
||||
pub(crate) const fn is_a_key_char(ch: char) -> bool { |
||||
is_a_key_leading_char(ch) || ch.is_ascii_digit() || ch == '-' |
||||
} |
||||
|
||||
/// Checks if char is an `AnnotationValueComponent`.
|
||||
pub(crate) const fn is_annotation_value_component(ch: char) -> bool { |
||||
ch.is_ascii_digit() || ch.is_ascii_alphabetic() |
||||
} |
||||
|
||||
/// Checks if char is a `TZLeadingChar`.
|
||||
#[inline] |
||||
pub(crate) const fn is_tz_leading_char(ch: char) -> bool { |
||||
ch.is_ascii_alphabetic() || ch == '_' || ch == '.' |
||||
} |
||||
|
||||
/// Checks if char is a `TZChar`.
|
||||
#[inline] |
||||
pub(crate) const fn is_tz_char(ch: char) -> bool { |
||||
is_tz_leading_char(ch) || ch.is_ascii_digit() || ch == '-' || ch == '+' |
||||
} |
||||
|
||||
/// Checks if char is a `TimeZoneIANAName` Separator.
|
||||
pub(crate) const fn is_tz_name_separator(ch: char) -> bool { |
||||
ch == '/' |
||||
} |
||||
|
||||
/// Checks if char is an ascii sign.
|
||||
pub(crate) const fn is_ascii_sign(ch: char) -> bool { |
||||
ch == '+' || ch == '-' |
||||
} |
||||
|
||||
/// Checks if char is an ascii sign or U+2212
|
||||
pub(crate) const fn is_sign(ch: char) -> bool { |
||||
is_ascii_sign(ch) || ch == '\u{2212}' |
||||
} |
||||
|
||||
/// Checks if char is a `TimeSeparator`.
|
||||
pub(crate) const fn is_time_separator(ch: char) -> bool { |
||||
ch == ':' |
||||
} |
||||
|
||||
/// Checks if char is a `TimeDesignator`.
|
||||
pub(crate) const fn is_time_designator(ch: char) -> bool { |
||||
ch == 'T' || ch == 't' |
||||
} |
||||
|
||||
/// Checks if char is a `DateTimeSeparator`.
|
||||
pub(crate) const fn is_date_time_separator(ch: char) -> bool { |
||||
is_time_designator(ch) || ch == '\u{0020}' |
||||
} |
||||
|
||||
/// Checks if char is a `UtcDesignator`.
|
||||
pub(crate) const fn is_utc_designator(ch: char) -> bool { |
||||
ch == 'Z' || ch == 'z' |
||||
} |
||||
|
||||
/// Checks if char is a `DurationDesignator`.
|
||||
pub(crate) const fn is_duration_designator(ch: char) -> bool { |
||||
ch == 'P' || ch == 'p' |
||||
} |
||||
|
||||
/// Checks if char is a `YearDesignator`.
|
||||
pub(crate) const fn is_year_designator(ch: char) -> bool { |
||||
ch == 'Y' || ch == 'y' |
||||
} |
||||
|
||||
/// Checks if char is a `MonthsDesignator`.
|
||||
pub(crate) const fn is_month_designator(ch: char) -> bool { |
||||
ch == 'M' || ch == 'm' |
||||
} |
||||
|
||||
/// Checks if char is a `WeekDesignator`.
|
||||
pub(crate) const fn is_week_designator(ch: char) -> bool { |
||||
ch == 'W' || ch == 'w' |
||||
} |
||||
|
||||
/// Checks if char is a `DayDesignator`.
|
||||
pub(crate) const fn is_day_designator(ch: char) -> bool { |
||||
ch == 'D' || ch == 'd' |
||||
} |
||||
|
||||
/// checks if char is a `DayDesignator`.
|
||||
pub(crate) const fn is_hour_designator(ch: char) -> bool { |
||||
ch == 'H' || ch == 'h' |
||||
} |
||||
|
||||
/// Checks if char is a `MinuteDesignator`.
|
||||
pub(crate) const fn is_minute_designator(ch: char) -> bool { |
||||
is_month_designator(ch) |
||||
} |
||||
|
||||
/// checks if char is a `SecondDesignator`.
|
||||
pub(crate) const fn is_second_designator(ch: char) -> bool { |
||||
ch == 'S' || ch == 's' |
||||
} |
||||
|
||||
/// Checks if char is a `DecimalSeparator`.
|
||||
pub(crate) const fn is_decimal_separator(ch: char) -> bool { |
||||
ch == '.' || ch == ',' |
||||
} |
||||
|
||||
/// Checks if char is an `AnnotationOpen`.
|
||||
pub(crate) const fn is_annotation_open(ch: char) -> bool { |
||||
ch == '[' |
||||
} |
||||
|
||||
/// Checks if char is an `AnnotationClose`.
|
||||
pub(crate) const fn is_annotation_close(ch: char) -> bool { |
||||
ch == ']' |
||||
} |
||||
|
||||
/// Checks if char is an `CriticalFlag`.
|
||||
pub(crate) const fn is_critical_flag(ch: char) -> bool { |
||||
ch == '!' |
||||
} |
||||
|
||||
/// Checks if char is the `AnnotationKeyValueSeparator`.
|
||||
pub(crate) const fn is_annotation_key_value_separator(ch: char) -> bool { |
||||
ch == '=' |
||||
} |
||||
|
||||
/// Checks if char is a hyphen. Hyphens are used as a Date separator
|
||||
/// and as a `AttributeValueComponent` separator.
|
||||
pub(crate) const fn is_hyphen(ch: char) -> bool { |
||||
ch == '-' |
||||
} |
@ -1,290 +0,0 @@
|
||||
//! This module implements parsing for ISO 8601 grammar.
|
||||
|
||||
use crate::{TemporalError, TemporalResult}; |
||||
|
||||
use datetime::DateRecord; |
||||
use nodes::{IsoDate, IsoDateTime, IsoTime, TimeZone}; |
||||
use time::TimeSpec; |
||||
|
||||
mod annotations; |
||||
pub(crate) mod datetime; |
||||
pub(crate) mod duration; |
||||
mod grammar; |
||||
mod nodes; |
||||
mod time; |
||||
pub(crate) mod time_zone; |
||||
|
||||
use self::{datetime::DateTimeFlags, grammar::is_annotation_open}; |
||||
|
||||
#[cfg(test)] |
||||
mod tests; |
||||
|
||||
// TODO: optimize where possible.
|
||||
|
||||
/// `assert_syntax!` is a parser specific utility macro for asserting a syntax test, and returning a
|
||||
/// `SyntaxError` with the provided message if the test fails.
|
||||
#[macro_export] |
||||
macro_rules! assert_syntax { |
||||
($cond:expr, $msg:literal) => { |
||||
if !$cond { |
||||
return Err(TemporalError::syntax().with_message($msg)); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
/// A utility function for parsing a `DateTime` string
|
||||
pub(crate) fn parse_date_time(target: &str) -> TemporalResult<IsoParseRecord> { |
||||
datetime::parse_annotated_date_time(DateTimeFlags::empty(), &mut Cursor::new(target)) |
||||
} |
||||
|
||||
/// A utility function for parsing an `Instant` string
|
||||
#[allow(unused)] |
||||
pub(crate) fn parse_instant(target: &str) -> TemporalResult<IsoParseRecord> { |
||||
datetime::parse_annotated_date_time( |
||||
DateTimeFlags::UTC_REQ | DateTimeFlags::TIME_REQ, |
||||
&mut Cursor::new(target), |
||||
) |
||||
} |
||||
|
||||
/// A utility function for parsing a `YearMonth` string
|
||||
pub(crate) fn parse_year_month(target: &str) -> TemporalResult<IsoParseRecord> { |
||||
let mut cursor = Cursor::new(target); |
||||
let ym = datetime::parse_year_month(&mut cursor); |
||||
|
||||
let Ok(year_month) = ym else { |
||||
cursor.pos = 0; |
||||
return datetime::parse_annotated_date_time(DateTimeFlags::empty(), &mut cursor); |
||||
}; |
||||
|
||||
let calendar = if cursor.check_or(false, is_annotation_open) { |
||||
let set = annotations::parse_annotation_set(false, &mut cursor)?; |
||||
set.calendar |
||||
} else { |
||||
None |
||||
}; |
||||
|
||||
cursor.close()?; |
||||
|
||||
Ok(IsoParseRecord { |
||||
date: DateRecord { |
||||
year: year_month.0, |
||||
month: year_month.1, |
||||
day: 1, |
||||
}, |
||||
time: None, |
||||
tz: None, |
||||
calendar, |
||||
}) |
||||
} |
||||
|
||||
/// A utilty function for parsing a `MonthDay` String.
|
||||
pub(crate) fn parse_month_day(target: &str) -> TemporalResult<IsoParseRecord> { |
||||
let mut cursor = Cursor::new(target); |
||||
let md = datetime::parse_month_day(&mut cursor); |
||||
|
||||
let Ok(month_day) = md else { |
||||
cursor.pos = 0; |
||||
return datetime::parse_annotated_date_time(DateTimeFlags::empty(), &mut cursor); |
||||
}; |
||||
|
||||
let calendar = if cursor.check_or(false, is_annotation_open) { |
||||
let set = annotations::parse_annotation_set(false, &mut cursor)?; |
||||
set.calendar |
||||
} else { |
||||
None |
||||
}; |
||||
|
||||
cursor.close()?; |
||||
|
||||
Ok(IsoParseRecord { |
||||
date: DateRecord { |
||||
year: 0, |
||||
month: month_day.0, |
||||
day: month_day.1, |
||||
}, |
||||
time: None, |
||||
tz: None, |
||||
calendar, |
||||
}) |
||||
} |
||||
|
||||
/// An `IsoParseRecord` is an intermediary record returned by ISO parsing functions.
|
||||
///
|
||||
/// `IsoParseRecord` is converted into the ISO AST Nodes.
|
||||
#[derive(Default, Debug)] |
||||
pub(crate) struct IsoParseRecord { |
||||
/// Parsed Date Record
|
||||
pub(crate) date: DateRecord, |
||||
/// Parsed Time
|
||||
pub(crate) time: Option<TimeSpec>, |
||||
/// Parsed `TimeZone` data (UTCOffset | IANA name)
|
||||
pub(crate) tz: Option<TimeZone>, |
||||
/// The parsed calendar value.
|
||||
pub(crate) calendar: Option<String>, |
||||
} |
||||
|
||||
// TODO: Phase out the below and integrate parsing with Temporal components.
|
||||
|
||||
/// Parse a [`TemporalTimeZoneString`][proposal].
|
||||
///
|
||||
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalTimeZoneString
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct TemporalTimeZoneString; |
||||
|
||||
impl TemporalTimeZoneString { |
||||
/// Parses a targeted string as a `TimeZone`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The parse will error if the provided target is not valid
|
||||
/// Iso8601 grammar.
|
||||
pub fn parse(cursor: &mut Cursor) -> TemporalResult<TimeZone> { |
||||
time_zone::parse_time_zone(cursor) |
||||
} |
||||
} |
||||
|
||||
/// Parser for a [`TemporalInstantString`][proposal].
|
||||
///
|
||||
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalInstantString
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct TemporalInstantString; |
||||
|
||||
impl TemporalInstantString { |
||||
/// Parses a targeted string as an `Instant`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The parse will error if the provided target is not valid
|
||||
/// Iso8601 grammar.
|
||||
pub fn parse(cursor: &mut Cursor) -> TemporalResult<IsoDateTime> { |
||||
let parse_record = datetime::parse_annotated_date_time( |
||||
DateTimeFlags::UTC_REQ | DateTimeFlags::TIME_REQ, |
||||
cursor, |
||||
)?; |
||||
|
||||
let date = IsoDate { |
||||
year: parse_record.date.year, |
||||
month: parse_record.date.month, |
||||
day: parse_record.date.day, |
||||
calendar: parse_record.calendar, |
||||
}; |
||||
|
||||
let time = parse_record.time.map_or_else(IsoTime::default, |time| { |
||||
IsoTime::from_components(time.hour, time.minute, time.second, time.fraction) |
||||
}); |
||||
|
||||
Ok(IsoDateTime { |
||||
date, |
||||
time, |
||||
tz: parse_record.tz, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
// ==== Mini cursor implementation for Iso8601 targets ====
|
||||
|
||||
/// `Cursor` is a small cursor implementation for parsing Iso8601 grammar.
|
||||
#[derive(Debug)] |
||||
pub struct Cursor { |
||||
pos: u32, |
||||
source: Vec<char>, |
||||
} |
||||
|
||||
impl Cursor { |
||||
/// Create a new cursor from a source `String` value.
|
||||
#[must_use] |
||||
pub fn new(source: &str) -> Self { |
||||
Self { |
||||
pos: 0, |
||||
source: source.chars().collect(), |
||||
} |
||||
} |
||||
|
||||
/// Returns a string value from a slice of the cursor.
|
||||
fn slice(&self, start: u32, end: u32) -> String { |
||||
self.source[start as usize..end as usize].iter().collect() |
||||
} |
||||
|
||||
/// Get current position
|
||||
const fn pos(&self) -> u32 { |
||||
self.pos |
||||
} |
||||
|
||||
/// Peek the value at next position (current + 1).
|
||||
fn peek(&self) -> Option<char> { |
||||
self.peek_n(1) |
||||
} |
||||
|
||||
/// Peek the value at n len from current.
|
||||
fn peek_n(&self, n: u32) -> Option<char> { |
||||
let target = (self.pos + n) as usize; |
||||
if target < self.source.len() { |
||||
Some(self.source[target]) |
||||
} else { |
||||
None |
||||
} |
||||
} |
||||
|
||||
/// Runs the provided check on the current position.
|
||||
fn check<F>(&self, f: F) -> Option<bool> |
||||
where |
||||
F: FnOnce(char) -> bool, |
||||
{ |
||||
self.peek_n(0).map(f) |
||||
} |
||||
|
||||
/// Runs the provided check on current position returns the default value if None.
|
||||
fn check_or<F>(&self, default: bool, f: F) -> bool |
||||
where |
||||
F: FnOnce(char) -> bool, |
||||
{ |
||||
self.peek_n(0).map_or(default, f) |
||||
} |
||||
|
||||
/// Returns `Cursor`'s current char and advances to the next position.
|
||||
fn next(&mut self) -> Option<char> { |
||||
let result = self.peek_n(0); |
||||
self.advance(); |
||||
result |
||||
} |
||||
|
||||
/// Utility method that returns next charactor unwrapped char
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This will panic if the next value has not been confirmed to exist.
|
||||
fn expect_next(&mut self) -> char { |
||||
self.next().expect("Invalid use of expect_next.") |
||||
} |
||||
|
||||
/// A utility next method that returns an `AbruptEnd` error if invalid.
|
||||
fn abrupt_next(&mut self) -> TemporalResult<char> { |
||||
self.next().ok_or_else(TemporalError::abrupt_end) |
||||
} |
||||
|
||||
/// Advances the cursor's position by 1.
|
||||
fn advance(&mut self) { |
||||
self.pos += 1; |
||||
} |
||||
|
||||
/// Utility function to advance when a condition is true
|
||||
fn advance_if(&mut self, condition: bool) { |
||||
if condition { |
||||
self.advance(); |
||||
} |
||||
} |
||||
|
||||
/// Advances the cursor's position by `n`.
|
||||
fn advance_n(&mut self, n: u32) { |
||||
self.pos += n; |
||||
} |
||||
|
||||
/// Closes the current cursor by checking if all contents have been consumed. If not, returns an error for invalid syntax.
|
||||
fn close(&mut self) -> TemporalResult<()> { |
||||
if (self.pos as usize) < self.source.len() { |
||||
return Err(TemporalError::syntax() |
||||
.with_message("Unexpected syntax at the end of an ISO target.")); |
||||
} |
||||
Ok(()) |
||||
} |
||||
} |
@ -1,90 +0,0 @@
|
||||
//! AST nodes for Temporal's implementation of ISO8601 grammar.
|
||||
|
||||
// TODO: Slowly remove the below nodes in favor of Temporal components.
|
||||
|
||||
/// An ISO Date Node consisting of non-validated date fields and calendar value.
|
||||
#[derive(Default, Debug)] |
||||
pub struct IsoDate { |
||||
/// Date Year
|
||||
pub year: i32, |
||||
/// Date Month
|
||||
pub month: i32, |
||||
/// Date Day
|
||||
pub day: i32, |
||||
/// The calendar value.
|
||||
pub calendar: Option<String>, |
||||
} |
||||
|
||||
/// The `IsoTime` node consists of non-validated time fields.
|
||||
#[derive(Default, Debug, Clone, Copy)] |
||||
pub struct IsoTime { |
||||
/// An hour value between 0-23
|
||||
pub hour: u8, |
||||
/// A minute value between 0-59
|
||||
pub minute: u8, |
||||
/// A second value between 0-60
|
||||
pub second: u8, |
||||
/// A millisecond value between 0-999
|
||||
pub millisecond: u16, |
||||
/// A microsecond value between 0-999
|
||||
pub microsecond: u16, |
||||
/// A nanosecond value between 0-999
|
||||
pub nanosecond: u16, |
||||
} |
||||
|
||||
impl IsoTime { |
||||
#[must_use] |
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] |
||||
/// A utility initialization function to create `ISOTime` from the `TimeSpec` components.
|
||||
pub fn from_components(hour: u8, minute: u8, second: u8, fraction: f64) -> Self { |
||||
// Note: Precision on nanoseconds drifts, so opting for round over floor or ceil for now.
|
||||
// e.g. 0.329402834 becomes 329.402833.999
|
||||
let millisecond = fraction * 1000f64; |
||||
let micros = millisecond.rem_euclid(1f64) * 1000f64; |
||||
let nanos = micros.rem_euclid(1f64) * 1000f64; |
||||
|
||||
Self { |
||||
hour, |
||||
minute, |
||||
second, |
||||
millisecond: millisecond.floor() as u16, |
||||
microsecond: micros.floor() as u16, |
||||
nanosecond: nanos.round() as u16, |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// The `IsoDateTime` node output by the ISO parser
|
||||
#[derive(Default, Debug)] |
||||
pub struct IsoDateTime { |
||||
/// The `ISODate` record
|
||||
pub date: IsoDate, |
||||
/// The `ISOTime` record
|
||||
pub time: IsoTime, |
||||
/// The `TimeZone` value for this `ISODateTime`
|
||||
pub tz: Option<TimeZone>, |
||||
} |
||||
|
||||
/// `TimeZone` data
|
||||
#[derive(Default, Debug, Clone)] |
||||
pub struct TimeZone { |
||||
/// TimeZoneIANAName
|
||||
pub name: Option<String>, |
||||
/// TimeZoneOffset
|
||||
pub offset: Option<UTCOffset>, |
||||
} |
||||
|
||||
/// A full precision `UtcOffset`
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct UTCOffset { |
||||
/// The `+`/`-` sign of this `UtcOffset`
|
||||
pub sign: i8, |
||||
/// The hour value of the `UtcOffset`
|
||||
pub hour: u8, |
||||
/// The minute value of the `UtcOffset`.
|
||||
pub minute: u8, |
||||
/// The second value of the `UtcOffset`.
|
||||
pub second: u8, |
||||
/// Any sub second components of the `UTCOffset`
|
||||
pub fraction: f64, |
||||
} |
@ -1,244 +0,0 @@
|
||||
use std::str::FromStr; |
||||
|
||||
use crate::{ |
||||
components::{DateTime, Duration, MonthDay, YearMonth}, |
||||
parser::{parse_date_time, Cursor, TemporalInstantString}, |
||||
}; |
||||
|
||||
#[test] |
||||
fn temporal_parser_basic() { |
||||
let basic = "20201108"; |
||||
let basic_separated = "2020-11-08"; |
||||
|
||||
let basic_result = basic.parse::<DateTime<()>>().unwrap(); |
||||
|
||||
let sep_result = basic_separated.parse::<DateTime<()>>().unwrap(); |
||||
|
||||
assert_eq!(basic_result.iso_year(), 2020); |
||||
assert_eq!(basic_result.iso_month(), 11); |
||||
assert_eq!(basic_result.iso_day(), 8); |
||||
assert_eq!(basic_result.iso_year(), sep_result.iso_year()); |
||||
assert_eq!(basic_result.iso_month(), sep_result.iso_month()); |
||||
assert_eq!(basic_result.iso_day(), sep_result.iso_day()); |
||||
} |
||||
|
||||
#[test] |
||||
#[allow(clippy::cast_possible_truncation)] |
||||
fn temporal_date_time_max() { |
||||
// Fractions not accurate, but for testing purposes.
|
||||
let date_time = |
||||
"+002020-11-08T12:28:32.329402834[!America/Argentina/ComodRivadavia][!u-ca=iso8601]"; |
||||
|
||||
let result = date_time.parse::<DateTime<()>>().unwrap(); |
||||
|
||||
assert_eq!(result.hour(), 12); |
||||
assert_eq!(result.minute(), 28); |
||||
assert_eq!(result.second(), 32); |
||||
assert_eq!(result.millisecond(), 329); |
||||
assert_eq!(result.microsecond(), 402); |
||||
assert_eq!(result.nanosecond(), 834); |
||||
} |
||||
|
||||
#[test] |
||||
fn temporal_year_parsing() { |
||||
let long = "+002020-11-08"; |
||||
let bad_year = "-000000-11-08"; |
||||
|
||||
let result_good = long.parse::<DateTime<()>>().unwrap(); |
||||
assert_eq!(result_good.iso_year(), 2020); |
||||
|
||||
let err_result = bad_year.parse::<DateTime<()>>(); |
||||
assert!( |
||||
err_result.is_err(), |
||||
"Invalid extended year parsing: \"{bad_year}\" should fail to parse." |
||||
); |
||||
} |
||||
|
||||
#[test] |
||||
fn temporal_annotated_date_time() { |
||||
let basic = "2020-11-08[America/Argentina/ComodRivadavia][u-ca=iso8601][foo=bar]"; |
||||
let omitted = "+0020201108[u-ca=iso8601][f-1a2b=a0sa-2l4s]"; |
||||
|
||||
let result = parse_date_time(basic).unwrap(); |
||||
|
||||
let tz = &result.tz.unwrap().name.unwrap(); |
||||
|
||||
assert_eq!(tz, "America/Argentina/ComodRivadavia"); |
||||
|
||||
assert_eq!(&result.calendar, &Some("iso8601".to_string())); |
||||
|
||||
let omit_result = parse_date_time(omitted).unwrap(); |
||||
|
||||
assert!(&omit_result.tz.is_none()); |
||||
|
||||
assert_eq!(&omit_result.calendar, &Some("iso8601".to_string())); |
||||
} |
||||
|
||||
#[test] |
||||
fn temporal_year_month() { |
||||
let possible_year_months = [ |
||||
"+002020-11", |
||||
"2020-11[u-ca=iso8601]", |
||||
"+00202011", |
||||
"202011[u-ca=iso8601]", |
||||
"+002020-11-07T12:28:32[!u-ca=iso8601]", |
||||
]; |
||||
|
||||
for ym in possible_year_months { |
||||
let result = ym.parse::<YearMonth<()>>().unwrap(); |
||||
|
||||
assert_eq!(result.year(), 2020); |
||||
assert_eq!(result.month(), 11); |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn temporal_month_day() { |
||||
let possible_month_day = [ |
||||
"11-07", |
||||
"1107[+04:00]", |
||||
"--11-07", |
||||
"--1107[+04:00]", |
||||
"+002020-11-07T12:28:32[!u-ca=iso8601]", |
||||
]; |
||||
|
||||
for md in possible_month_day { |
||||
let result = md.parse::<MonthDay<()>>().unwrap(); |
||||
|
||||
assert_eq!(result.month(), 11); |
||||
assert_eq!(result.day(), 7); |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn temporal_invalid_annotations() { |
||||
let invalid_annotations = [ |
||||
"2020-11-11[!u-ca=iso8601][u-ca=iso8601]", |
||||
"2020-11-11[u-ca=iso8601][!u-ca=iso8601]", |
||||
"2020-11-11[u-ca=iso8601][!rip=this-invalid-annotation]", |
||||
]; |
||||
|
||||
for invalid in invalid_annotations { |
||||
let err_result = invalid.parse::<MonthDay<()>>(); |
||||
assert!( |
||||
err_result.is_err(), |
||||
"Invalid ISO annotation parsing: \"{invalid}\" should fail parsing." |
||||
); |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn temporal_valid_instant_strings() { |
||||
let instants = [ |
||||
"1970-01-01T00:00+00:00[!Africa/Abidjan]", |
||||
"1970-01-01T00:00+00:00[UTC]", |
||||
"1970-01-01T00:00Z[!Europe/Vienna]", |
||||
]; |
||||
|
||||
for test in instants { |
||||
let result = TemporalInstantString::parse(&mut Cursor::new(test)); |
||||
assert!(result.is_ok()); |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
#[allow(clippy::cast_possible_truncation)] |
||||
#[allow(clippy::float_cmp)] |
||||
fn temporal_duration_parsing() { |
||||
let durations = [ |
||||
"p1y1m1dt1h1m1s", |
||||
"P1Y1M1W1DT1H1M1.1S", |
||||
"-P1Y1M1W1DT1H1M1.123456789S", |
||||
"-P1Y3wT0,5H", |
||||
]; |
||||
|
||||
for dur in durations { |
||||
let ok_result = Duration::from_str(dur); |
||||
assert!( |
||||
ok_result.is_ok(), |
||||
"Failing to parse a valid ISO 8601 target: \"{dur}\" should pass." |
||||
); |
||||
} |
||||
|
||||
let sub_second = durations[2].parse::<Duration>().unwrap(); |
||||
|
||||
assert_eq!(sub_second.time().milliseconds(), -123.0); |
||||
assert_eq!(sub_second.time().microseconds(), -456.0); |
||||
assert_eq!(sub_second.time().nanoseconds(), -789.0); |
||||
|
||||
let test_result = durations[3].parse::<Duration>().unwrap(); |
||||
|
||||
assert_eq!(test_result.date().years(), -1f64); |
||||
assert_eq!(test_result.date().weeks(), -3f64); |
||||
assert_eq!(test_result.time().minutes(), -30.0); |
||||
} |
||||
|
||||
#[test] |
||||
fn temporal_invalid_durations() { |
||||
let invalids = [ |
||||
"P1Y1M1W0,5D", |
||||
"P1Y1M1W1DT1H1M1.123456789123S", |
||||
"+PT", |
||||
"P1Y1M1W1DT1H0.5M0.5S", |
||||
]; |
||||
|
||||
for test in invalids { |
||||
let err = test.parse::<Duration>(); |
||||
assert!( |
||||
err.is_err(), |
||||
"Invalid ISO8601 Duration target: \"{test}\" should fail duration parsing." |
||||
); |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn temporal_invalid_iso_datetime_strings() { |
||||
// NOTE: The below tests were initially pulled from test262's `argument-string-invalid`
|
||||
const INVALID_DATETIME_STRINGS: [&str; 34] = [ |
||||
"", // 1
|
||||
"invalid iso8601", |
||||
"2020-01-00", |
||||
"2020-01-32", |
||||
"2020-02-30", |
||||
"2021-02-29", |
||||
"2020-00-01", |
||||
"2020-13-01", |
||||
"2020-01-01T", |
||||
"2020-01-01T25:00:00", |
||||
"2020-01-01T01:60:00", |
||||
"2020-01-01T01:60:61", |
||||
"2020-01-01junk", |
||||
"2020-01-01T00:00:00junk", |
||||
"2020-01-01T00:00:00+00:00junk", |
||||
"2020-01-01T00:00:00+00:00[UTC]junk", |
||||
"2020-01-01T00:00:00+00:00[UTC][u-ca=iso8601]junk", |
||||
"02020-01-01", |
||||
"2020-001-01", |
||||
"2020-01-001", |
||||
"2020-01-01T001", |
||||
"2020-01-01T01:001", |
||||
"2020-01-01T01:01:001", |
||||
"2020-W01-1", |
||||
"2020-001", |
||||
"+0002020-01-01", |
||||
// TODO: Add the non-existent calendar test back to the test cases.
|
||||
// may be valid in other contexts, but insufficient information for PlainDate:
|
||||
"2020-01", |
||||
"+002020-01", |
||||
"01-01", |
||||
"2020-W01", |
||||
"P1Y", |
||||
"-P12Y", |
||||
// valid, but outside the supported range:
|
||||
"-999999-01-01", |
||||
"+999999-01-01", |
||||
]; |
||||
|
||||
for invalid_target in INVALID_DATETIME_STRINGS { |
||||
let error_result = invalid_target.parse::<DateTime<()>>(); |
||||
assert!( |
||||
error_result.is_err(), |
||||
"Invalid ISO8601 `DateTime` target: \"{invalid_target}\" should fail parsing." |
||||
); |
||||
} |
||||
} |
@ -1,134 +0,0 @@
|
||||
//! Parsing of ISO8601 Time Values
|
||||
|
||||
use super::{ |
||||
grammar::{is_decimal_separator, is_time_separator}, |
||||
Cursor, |
||||
}; |
||||
use crate::{assert_syntax, TemporalError, TemporalResult}; |
||||
|
||||
/// Parsed Time info
|
||||
#[derive(Debug, Default, Clone, Copy)] |
||||
pub(crate) struct TimeSpec { |
||||
/// An hour
|
||||
pub(crate) hour: u8, |
||||
/// A minute value
|
||||
pub(crate) minute: u8, |
||||
/// A second value.
|
||||
pub(crate) second: u8, |
||||
/// A floating point number representing the sub-second values
|
||||
pub(crate) fraction: f64, |
||||
} |
||||
|
||||
/// Parse `TimeSpec`
|
||||
pub(crate) fn parse_time_spec(cursor: &mut Cursor) -> TemporalResult<TimeSpec> { |
||||
let hour = parse_hour(cursor)?; |
||||
|
||||
if !cursor.check_or(false, |ch| is_time_separator(ch) || ch.is_ascii_digit()) { |
||||
return Ok(TimeSpec { |
||||
hour, |
||||
minute: 0, |
||||
second: 0, |
||||
fraction: 0.0, |
||||
}); |
||||
} |
||||
|
||||
let separator_present = cursor.check_or(false, is_time_separator); |
||||
cursor.advance_if(separator_present); |
||||
|
||||
let minute = parse_minute_second(cursor, false)?; |
||||
|
||||
if !cursor.check_or(false, |ch| is_time_separator(ch) || ch.is_ascii_digit()) { |
||||
return Ok(TimeSpec { |
||||
hour, |
||||
minute, |
||||
second: 0, |
||||
fraction: 0.0, |
||||
}); |
||||
} else if cursor.check_or(false, is_time_separator) && !separator_present { |
||||
return Err(TemporalError::syntax().with_message("Invalid TimeSeparator")); |
||||
} |
||||
|
||||
cursor.advance_if(separator_present); |
||||
|
||||
let second = parse_minute_second(cursor, true)?; |
||||
|
||||
let fraction = if cursor.check_or(false, is_decimal_separator) { |
||||
parse_fraction(cursor)? |
||||
} else { |
||||
0.0 |
||||
}; |
||||
|
||||
Ok(TimeSpec { |
||||
hour, |
||||
minute, |
||||
second, |
||||
fraction, |
||||
}) |
||||
} |
||||
|
||||
pub(crate) fn parse_hour(cursor: &mut Cursor) -> TemporalResult<u8> { |
||||
let start = cursor.pos(); |
||||
for _ in 0..2 { |
||||
let digit = cursor.abrupt_next()?; |
||||
assert_syntax!(digit.is_ascii_digit(), "Hour must be a digit."); |
||||
} |
||||
let hour_value = cursor |
||||
.slice(start, cursor.pos()) |
||||
.parse::<u8>() |
||||
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?; |
||||
if !(0..=23).contains(&hour_value) { |
||||
return Err(TemporalError::syntax().with_message("Hour must be in a range of 0-23")); |
||||
} |
||||
Ok(hour_value) |
||||
} |
||||
|
||||
// NOTE: `TimeSecond` is a 60 inclusive `MinuteSecond`.
|
||||
/// Parse `MinuteSecond`
|
||||
pub(crate) fn parse_minute_second(cursor: &mut Cursor, inclusive: bool) -> TemporalResult<u8> { |
||||
let start = cursor.pos(); |
||||
for _ in 0..2 { |
||||
let digit = cursor.abrupt_next()?; |
||||
assert_syntax!(digit.is_ascii_digit(), "MinuteSecond must be a digit."); |
||||
} |
||||
let min_sec_value = cursor |
||||
.slice(start, cursor.pos()) |
||||
.parse::<u8>() |
||||
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?; |
||||
|
||||
let valid_range = if inclusive { 0..=60 } else { 0..=59 }; |
||||
if !valid_range.contains(&min_sec_value) { |
||||
return Err(TemporalError::syntax().with_message("MinuteSecond must be in a range of 0-59")); |
||||
} |
||||
Ok(min_sec_value) |
||||
} |
||||
|
||||
/// Parse a `Fraction` value
|
||||
///
|
||||
/// This is primarily used in ISO8601 to add percision past
|
||||
/// a second.
|
||||
pub(crate) fn parse_fraction(cursor: &mut Cursor) -> TemporalResult<f64> { |
||||
let mut fraction_components = Vec::default(); |
||||
|
||||
// Assert that the first char provided is a decimal separator.
|
||||
assert_syntax!( |
||||
is_decimal_separator(cursor.abrupt_next()?), |
||||
"fraction must begin with a valid decimal separator." |
||||
); |
||||
fraction_components.push('.'); |
||||
|
||||
while cursor.check_or(false, |ch| ch.is_ascii_digit()) { |
||||
fraction_components.push(cursor.abrupt_next()?); |
||||
} |
||||
|
||||
assert_syntax!( |
||||
fraction_components.len() <= 10, |
||||
"Fraction component cannot exceed 9 digits." |
||||
); |
||||
|
||||
let fraction_value = fraction_components |
||||
.iter() |
||||
.collect::<String>() |
||||
.parse::<f64>() |
||||
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?; |
||||
Ok(fraction_value) |
||||
} |
@ -1,229 +0,0 @@
|
||||
//! ISO8601 parsing for Time Zone and Offset data.
|
||||
|
||||
use super::{ |
||||
grammar::{ |
||||
is_a_key_char, is_a_key_leading_char, is_annotation_close, |
||||
is_annotation_key_value_separator, is_annotation_open, is_critical_flag, |
||||
is_decimal_separator, is_sign, is_time_separator, is_tz_char, is_tz_leading_char, |
||||
is_tz_name_separator, is_utc_designator, |
||||
}, |
||||
nodes::{TimeZone, UTCOffset}, |
||||
time::{parse_fraction, parse_hour, parse_minute_second}, |
||||
Cursor, |
||||
}; |
||||
use crate::{assert_syntax, TemporalError, TemporalResult}; |
||||
|
||||
/// A `TimeZoneAnnotation`.
|
||||
#[derive(Debug, Clone)] |
||||
#[allow(unused)] |
||||
pub(crate) struct TimeZoneAnnotation { |
||||
/// Critical Flag for the annotation.
|
||||
pub(crate) critical: bool, |
||||
/// TimeZone Data
|
||||
pub(crate) tz: TimeZone, |
||||
} |
||||
|
||||
// ==== Time Zone Annotation Parsing ====
|
||||
|
||||
pub(crate) fn parse_ambiguous_tz_annotation( |
||||
cursor: &mut Cursor, |
||||
) -> TemporalResult<Option<TimeZoneAnnotation>> { |
||||
// Peek position + 1 to check for critical flag.
|
||||
let mut current_peek = 1; |
||||
let critical = cursor |
||||
.peek_n(current_peek) |
||||
.map(is_critical_flag) |
||||
.ok_or_else(TemporalError::abrupt_end)?; |
||||
|
||||
// Advance cursor if critical flag present.
|
||||
if critical { |
||||
current_peek += 1; |
||||
} |
||||
|
||||
let leading_char = cursor |
||||
.peek_n(current_peek) |
||||
.ok_or_else(TemporalError::abrupt_end)?; |
||||
|
||||
if is_tz_leading_char(leading_char) || is_sign(leading_char) { |
||||
// Ambigious start values when lowercase alpha that is shared between `TzLeadingChar` and `KeyLeadingChar`.
|
||||
if is_a_key_leading_char(leading_char) { |
||||
let mut peek_pos = current_peek + 1; |
||||
while let Some(ch) = cursor.peek_n(peek_pos) { |
||||
if is_tz_name_separator(ch) || (is_tz_char(ch) && !is_a_key_char(ch)) { |
||||
let tz = parse_tz_annotation(cursor)?; |
||||
return Ok(Some(tz)); |
||||
} else if is_annotation_key_value_separator(ch) |
||||
|| (is_a_key_char(ch) && !is_tz_char(ch)) |
||||
{ |
||||
return Ok(None); |
||||
} else if is_annotation_close(ch) { |
||||
return Err(TemporalError::syntax().with_message("Invalid Annotation")); |
||||
} |
||||
|
||||
peek_pos += 1; |
||||
} |
||||
return Err(TemporalError::abrupt_end()); |
||||
} |
||||
let tz = parse_tz_annotation(cursor)?; |
||||
return Ok(Some(tz)); |
||||
} |
||||
|
||||
if is_a_key_leading_char(leading_char) { |
||||
return Ok(None); |
||||
}; |
||||
|
||||
Err(TemporalError::syntax().with_message("Unexpected character in ambiguous annotation.")) |
||||
} |
||||
|
||||
fn parse_tz_annotation(cursor: &mut Cursor) -> TemporalResult<TimeZoneAnnotation> { |
||||
assert_syntax!( |
||||
is_annotation_open(cursor.abrupt_next()?), |
||||
"Invalid annotation opening character." |
||||
); |
||||
|
||||
let critical = cursor.check_or(false, is_critical_flag); |
||||
cursor.advance_if(critical); |
||||
|
||||
let tz = parse_time_zone(cursor)?; |
||||
|
||||
assert_syntax!( |
||||
is_annotation_close(cursor.abrupt_next()?), |
||||
"Invalid annotation closing character." |
||||
); |
||||
|
||||
Ok(TimeZoneAnnotation { critical, tz }) |
||||
} |
||||
|
||||
/// Parses the [`TimeZoneIdentifier`][tz] node.
|
||||
///
|
||||
/// [tz]: https://tc39.es/proposal-temporal/#prod-TimeZoneIdentifier
|
||||
pub(crate) fn parse_time_zone(cursor: &mut Cursor) -> TemporalResult<TimeZone> { |
||||
let is_iana = cursor |
||||
.check(is_tz_leading_char) |
||||
.ok_or_else(TemporalError::abrupt_end)?; |
||||
let is_offset = cursor.check_or(false, is_sign); |
||||
|
||||
if is_iana { |
||||
return parse_tz_iana_name(cursor); |
||||
} else if is_offset { |
||||
let offset = parse_utc_offset_minute_precision(cursor)?; |
||||
return Ok(TimeZone { |
||||
name: None, |
||||
offset: Some(offset), |
||||
}); |
||||
} |
||||
|
||||
Err(TemporalError::syntax().with_message("Invalid leading character for a TimeZoneIdentifier")) |
||||
} |
||||
|
||||
/// Parse a `TimeZoneIANAName` Parse Node
|
||||
fn parse_tz_iana_name(cursor: &mut Cursor) -> TemporalResult<TimeZone> { |
||||
let tz_name_start = cursor.pos(); |
||||
while let Some(potential_value_char) = cursor.next() { |
||||
if cursor.check_or(false, is_annotation_close) { |
||||
// Return the valid TimeZoneIANAName
|
||||
return Ok(TimeZone { |
||||
name: Some(cursor.slice(tz_name_start, cursor.pos())), |
||||
offset: None, |
||||
}); |
||||
} |
||||
|
||||
if is_tz_name_separator(potential_value_char) { |
||||
assert_syntax!( |
||||
cursor.peek_n(2).map_or(false, is_tz_char), |
||||
"Missing IANA name component after '/'" |
||||
); |
||||
continue; |
||||
} |
||||
|
||||
assert_syntax!( |
||||
is_tz_char(potential_value_char), |
||||
"Invalid TimeZone IANA name character." |
||||
); |
||||
} |
||||
|
||||
Err(TemporalError::abrupt_end()) |
||||
} |
||||
|
||||
// ==== Utc Offset Parsing ====
|
||||
|
||||
/// Parse a full precision `UtcOffset`
|
||||
pub(crate) fn parse_date_time_utc(cursor: &mut Cursor) -> TemporalResult<TimeZone> { |
||||
if cursor.check_or(false, is_utc_designator) { |
||||
cursor.advance(); |
||||
return Ok(TimeZone { |
||||
name: Some("UTC".to_owned()), |
||||
offset: None, |
||||
}); |
||||
} |
||||
|
||||
let separated = cursor.peek_n(3).map_or(false, is_time_separator); |
||||
|
||||
let mut utc_to_minute = parse_utc_offset_minute_precision(cursor)?; |
||||
|
||||
if cursor.check_or(false, is_time_separator) && !separated { |
||||
return Err(TemporalError::syntax().with_message("Invalid time separator in UTC offset.")); |
||||
} |
||||
cursor.advance_if(cursor.check_or(false, is_time_separator)); |
||||
|
||||
// Return early on None or AnnotationOpen.
|
||||
if cursor.check_or(true, is_annotation_open) { |
||||
return Ok(TimeZone { |
||||
name: None, |
||||
offset: Some(utc_to_minute), |
||||
}); |
||||
} |
||||
|
||||
// If `UtcOffsetWithSubMinuteComponents`, continue parsing.
|
||||
utc_to_minute.second = parse_minute_second(cursor, true)?; |
||||
|
||||
let sub_second = if cursor.check_or(false, is_decimal_separator) { |
||||
parse_fraction(cursor)? |
||||
} else { |
||||
0.0 |
||||
}; |
||||
|
||||
utc_to_minute.fraction = sub_second; |
||||
|
||||
Ok(TimeZone { |
||||
name: None, |
||||
offset: Some(utc_to_minute), |
||||
}) |
||||
} |
||||
|
||||
/// Parse an `UtcOffsetMinutePrecision` node
|
||||
pub(crate) fn parse_utc_offset_minute_precision(cursor: &mut Cursor) -> TemporalResult<UTCOffset> { |
||||
let sign = if cursor.check_or(false, is_sign) { |
||||
if cursor.expect_next() == '+' { |
||||
1 |
||||
} else { |
||||
-1 |
||||
} |
||||
} else { |
||||
1 |
||||
}; |
||||
let hour = parse_hour(cursor)?; |
||||
|
||||
// If at the end of the utc, then return.
|
||||
if !cursor.check_or(false, |ch| ch.is_ascii_digit() || is_time_separator(ch)) { |
||||
return Ok(UTCOffset { |
||||
sign, |
||||
hour, |
||||
minute: 0, |
||||
second: 0, |
||||
fraction: 0.0, |
||||
}); |
||||
} |
||||
// Advance cursor beyond any TimeSeparator
|
||||
cursor.advance_if(cursor.check_or(false, is_time_separator)); |
||||
|
||||
let minute = parse_minute_second(cursor, false)?; |
||||
|
||||
Ok(UTCOffset { |
||||
sign, |
||||
hour, |
||||
minute, |
||||
second: 0, |
||||
fraction: 0.0, |
||||
}) |
||||
} |
@ -1,341 +0,0 @@
|
||||
//! Utility date and time equations for Temporal
|
||||
|
||||
use crate::{ |
||||
options::{TemporalRoundingMode, TemporalUnsignedRoundingMode}, |
||||
TemporalError, TemporalResult, MS_PER_DAY, |
||||
}; |
||||
|
||||
// NOTE: Review the below for optimizations and add ALOT of tests.
|
||||
|
||||
/// Converts and validates an `Option<f64>` rounding increment value into a valid increment result.
|
||||
pub(crate) fn to_rounding_increment(increment: Option<f64>) -> TemporalResult<f64> { |
||||
let inc = increment.unwrap_or(1.0); |
||||
|
||||
if !inc.is_finite() { |
||||
return Err(TemporalError::range().with_message("roundingIncrement must be finite.")); |
||||
} |
||||
|
||||
let integer = inc.trunc(); |
||||
|
||||
if !(1.0..=1_000_000_000f64).contains(&integer) { |
||||
return Err( |
||||
TemporalError::range().with_message("roundingIncrement is not within a valid range.") |
||||
); |
||||
} |
||||
|
||||
Ok(integer) |
||||
} |
||||
|
||||
fn apply_unsigned_rounding_mode( |
||||
x: f64, |
||||
r1: f64, |
||||
r2: f64, |
||||
unsigned_rounding_mode: TemporalUnsignedRoundingMode, |
||||
) -> f64 { |
||||
// 1. If x is equal to r1, return r1.
|
||||
if (x - r1).abs() == 0.0 { |
||||
return r1; |
||||
}; |
||||
// 2. Assert: r1 < x < r2.
|
||||
assert!(r1 < x && x < r2); |
||||
// 3. Assert: unsignedRoundingMode is not undefined.
|
||||
|
||||
// 4. If unsignedRoundingMode is zero, return r1.
|
||||
if unsigned_rounding_mode == TemporalUnsignedRoundingMode::Zero { |
||||
return r1; |
||||
}; |
||||
// 5. If unsignedRoundingMode is infinity, return r2.
|
||||
if unsigned_rounding_mode == TemporalUnsignedRoundingMode::Infinity { |
||||
return r2; |
||||
}; |
||||
|
||||
// 6. Let d1 be x – r1.
|
||||
let d1 = x - r1; |
||||
// 7. Let d2 be r2 – x.
|
||||
let d2 = r2 - x; |
||||
// 8. If d1 < d2, return r1.
|
||||
if d1 < d2 { |
||||
return r1; |
||||
} |
||||
// 9. If d2 < d1, return r2.
|
||||
if d2 < d1 { |
||||
return r2; |
||||
} |
||||
// 10. Assert: d1 is equal to d2.
|
||||
assert!((d1 - d2).abs() == 0.0); |
||||
|
||||
// 11. If unsignedRoundingMode is half-zero, return r1.
|
||||
if unsigned_rounding_mode == TemporalUnsignedRoundingMode::HalfZero { |
||||
return r1; |
||||
}; |
||||
// 12. If unsignedRoundingMode is half-infinity, return r2.
|
||||
if unsigned_rounding_mode == TemporalUnsignedRoundingMode::HalfInfinity { |
||||
return r2; |
||||
}; |
||||
// 13. Assert: unsignedRoundingMode is half-even.
|
||||
assert!(unsigned_rounding_mode == TemporalUnsignedRoundingMode::HalfEven); |
||||
// 14. Let cardinality be (r1 / (r2 – r1)) modulo 2.
|
||||
let cardinality = (r1 / (r2 - r1)) % 2.0; |
||||
// 15. If cardinality is 0, return r1.
|
||||
if cardinality == 0.0 { |
||||
return r1; |
||||
} |
||||
// 16. Return r2.
|
||||
r2 |
||||
} |
||||
|
||||
/// 13.28 `RoundNumberToIncrement ( x, increment, roundingMode )`
|
||||
pub(crate) fn round_number_to_increment( |
||||
x: f64, |
||||
increment: f64, |
||||
rounding_mode: TemporalRoundingMode, |
||||
) -> f64 { |
||||
// 1. Let quotient be x / increment.
|
||||
let mut quotient = x / increment; |
||||
|
||||
// 2. If quotient < 0, then
|
||||
let is_negative = if quotient < 0_f64 { |
||||
// a. Let isNegative be true.
|
||||
// b. Set quotient to -quotient.
|
||||
quotient = -quotient; |
||||
true |
||||
// 3. Else,
|
||||
} else { |
||||
// a. Let isNegative be false.
|
||||
false |
||||
}; |
||||
|
||||
// 4. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, isNegative).
|
||||
let unsigned_rounding_mode = rounding_mode.get_unsigned_round_mode(is_negative); |
||||
// 5. Let r1 be the largest integer such that r1 ≤ quotient.
|
||||
let r1 = quotient.floor(); |
||||
// 6. Let r2 be the smallest integer such that r2 > quotient.
|
||||
let r2 = quotient.ceil(); |
||||
// 7. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode).
|
||||
let mut rounded = apply_unsigned_rounding_mode(quotient, r1, r2, unsigned_rounding_mode); |
||||
// 8. If isNegative is true, set rounded to -rounded.
|
||||
if is_negative { |
||||
rounded = -rounded; |
||||
}; |
||||
// 9. Return rounded × increment.
|
||||
rounded * increment |
||||
} |
||||
|
||||
/// Rounds provided number assuming that the increment is greater than 0.
|
||||
pub(crate) fn round_number_to_increment_as_if_positive( |
||||
nanos: f64, |
||||
increment_nanos: f64, |
||||
rounding_mode: TemporalRoundingMode, |
||||
) -> f64 { |
||||
// 1. Let quotient be x / increment.
|
||||
let quotient = nanos / increment_nanos; |
||||
// 2. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, false).
|
||||
let unsigned_rounding_mode = rounding_mode.get_unsigned_round_mode(false); |
||||
// 3. Let r1 be the largest integer such that r1 ≤ quotient.
|
||||
let r1 = quotient.floor(); |
||||
// 4. Let r2 be the smallest integer such that r2 > quotient.
|
||||
let r2 = quotient.ceil(); |
||||
// 5. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode).
|
||||
let rounded = apply_unsigned_rounding_mode(quotient, r1, r2, unsigned_rounding_mode); |
||||
// 6. Return rounded × increment.
|
||||
rounded * increment_nanos |
||||
} |
||||
|
||||
pub(crate) fn validate_temporal_rounding_increment( |
||||
increment: u32, |
||||
dividend: u64, |
||||
inclusive: bool, |
||||
) -> TemporalResult<()> { |
||||
let max = if inclusive { dividend } else { dividend - 1 }; |
||||
|
||||
if u64::from(increment) > max { |
||||
return Err(TemporalError::range().with_message("roundingIncrement exceeds maximum.")); |
||||
} |
||||
|
||||
if dividend.rem_euclid(u64::from(increment)) != 0 { |
||||
return Err( |
||||
TemporalError::range().with_message("dividend is not divisble by roundingIncrement.") |
||||
); |
||||
} |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
// ==== Begin Date Equations ====
|
||||
|
||||
pub(crate) const MS_PER_HOUR: f64 = 3_600_000f64; |
||||
pub(crate) const MS_PER_MINUTE: f64 = 60_000f64; |
||||
|
||||
/// `EpochDaysToEpochMS`
|
||||
///
|
||||
/// Functionally the same as Date's abstract operation `MakeDate`
|
||||
pub(crate) fn epoch_days_to_epoch_ms(day: i32, time: f64) -> f64 { |
||||
f64::from(day).mul_add(f64::from(MS_PER_DAY), time).floor() |
||||
} |
||||
|
||||
/// `EpochTimeToDayNumber`
|
||||
///
|
||||
/// This equation is the equivalent to `ECMAScript`'s `Date(t)`
|
||||
pub(crate) fn epoch_time_to_day_number(t: f64) -> i32 { |
||||
(t / f64::from(MS_PER_DAY)).floor() as i32 |
||||
} |
||||
|
||||
/// Mathematically determine the days in a year.
|
||||
pub(crate) fn mathematical_days_in_year(y: i32) -> i32 { |
||||
if y % 4 != 0 { |
||||
365 |
||||
} else if y % 4 == 0 && y % 100 != 0 { |
||||
366 |
||||
} else if y % 100 == 0 && y % 400 != 0 { |
||||
365 |
||||
} else { |
||||
// Assert that y is divisble by 400 to ensure we are returning the correct result.
|
||||
assert_eq!(y % 400, 0); |
||||
366 |
||||
} |
||||
} |
||||
|
||||
/// Returns the epoch day number for a given year.
|
||||
pub(crate) fn epoch_day_number_for_year(y: f64) -> f64 { |
||||
365.0f64.mul_add(y - 1970.0, ((y - 1969.0) / 4.0).floor()) - ((y - 1901.0) / 100.0).floor() |
||||
+ ((y - 1601.0) / 400.0).floor() |
||||
} |
||||
|
||||
pub(crate) fn epoch_time_for_year(y: i32) -> f64 { |
||||
f64::from(MS_PER_DAY) * epoch_day_number_for_year(f64::from(y)) |
||||
} |
||||
|
||||
pub(crate) fn epoch_time_to_epoch_year(t: f64) -> i32 { |
||||
// roughly calculate the largest possible year given the time t,
|
||||
// then check and refine the year.
|
||||
let day_count = epoch_time_to_day_number(t); |
||||
let mut year = (day_count / 365) + 1970; |
||||
loop { |
||||
if epoch_time_for_year(year) <= t { |
||||
break; |
||||
} |
||||
year -= 1; |
||||
} |
||||
|
||||
year |
||||
} |
||||
|
||||
/// Returns either 1 (true) or 0 (false)
|
||||
pub(crate) fn mathematical_in_leap_year(t: f64) -> i32 { |
||||
mathematical_days_in_year(epoch_time_to_epoch_year(t)) - 365 |
||||
} |
||||
|
||||
pub(crate) fn epoch_time_to_month_in_year(t: f64) -> u8 { |
||||
const DAYS: [i32; 11] = [30, 58, 89, 120, 150, 181, 212, 242, 272, 303, 333]; |
||||
const LEAP_DAYS: [i32; 11] = [30, 59, 90, 121, 151, 182, 213, 242, 272, 303, 334]; |
||||
|
||||
let in_leap_year = mathematical_in_leap_year(t) == 1; |
||||
let day = epoch_time_to_day_in_year(t); |
||||
|
||||
let result = if in_leap_year { |
||||
LEAP_DAYS.binary_search(&day) |
||||
} else { |
||||
DAYS.binary_search(&day) |
||||
}; |
||||
|
||||
match result { |
||||
Ok(i) | Err(i) => i as u8, |
||||
} |
||||
} |
||||
|
||||
pub(crate) fn epoch_time_for_month_given_year(m: i32, y: i32) -> f64 { |
||||
let leap_day = mathematical_days_in_year(y) - 365; |
||||
|
||||
let days = match m { |
||||
0 => 1, |
||||
1 => 31, |
||||
2 => 59 + leap_day, |
||||
3 => 90 + leap_day, |
||||
4 => 121 + leap_day, |
||||
5 => 151 + leap_day, |
||||
6 => 182 + leap_day, |
||||
7 => 213 + leap_day, |
||||
8 => 243 + leap_day, |
||||
9 => 273 + leap_day, |
||||
10 => 304 + leap_day, |
||||
11 => 334 + leap_day, |
||||
_ => unreachable!(), |
||||
}; |
||||
|
||||
f64::from(MS_PER_DAY) * f64::from(days) |
||||
} |
||||
|
||||
pub(crate) fn epoch_time_to_date(t: f64) -> u8 { |
||||
const OFFSETS: [i16; 12] = [ |
||||
1, -30, -58, -89, -119, -150, -180, -211, -242, -272, -303, -333, |
||||
]; |
||||
let day_in_year = epoch_time_to_day_in_year(t); |
||||
let in_leap_year = mathematical_in_leap_year(t); |
||||
let month = epoch_time_to_month_in_year(t); |
||||
|
||||
// Cast from i32 to usize should be safe as the return must be 0-11
|
||||
let mut date = day_in_year + i32::from(OFFSETS[month as usize]); |
||||
|
||||
if month >= 2 { |
||||
date -= in_leap_year; |
||||
} |
||||
|
||||
// This return of date should be < 31.
|
||||
date as u8 |
||||
} |
||||
|
||||
pub(crate) fn epoch_time_to_day_in_year(t: f64) -> i32 { |
||||
epoch_time_to_day_number(t) |
||||
- (epoch_day_number_for_year(f64::from(epoch_time_to_epoch_year(t))) as i32) |
||||
} |
||||
|
||||
// EpochTimeTOWeekDay -> REMOVED
|
||||
|
||||
// ==== End Date Equations ====
|
||||
|
||||
// ==== Begin Calendar Equations ====
|
||||
|
||||
// NOTE: below was the iso methods in temporal::calendar -> Need to be reassessed.
|
||||
|
||||
/// 12.2.31 `ISODaysInMonth ( year, month )`
|
||||
pub(crate) fn iso_days_in_month(year: i32, month: i32) -> i32 { |
||||
match month { |
||||
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, |
||||
4 | 6 | 9 | 11 => 30, |
||||
2 => 28 + mathematical_in_leap_year(epoch_time_for_year(year)), |
||||
_ => unreachable!("ISODaysInMonth panicking is an implementation error."), |
||||
} |
||||
} |
||||
|
||||
// The below calendar abstract equations/utilities were removed for being unused.
|
||||
// 12.2.32 `ToISOWeekOfYear ( year, month, day )`
|
||||
// 12.2.33 `ISOMonthCode ( month )`
|
||||
// 12.2.39 `ToISODayOfYear ( year, month, day )`
|
||||
// 12.2.40 `ToISODayOfWeek ( year, month, day )`
|
||||
|
||||
// ==== End Calendar Equations ====
|
||||
|
||||
// ==== Tests =====
|
||||
|
||||
// TODO(nekevss): Add way more to the below.
|
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use super::*; |
||||
|
||||
#[test] |
||||
fn time_to_month() { |
||||
let oct_2023 = 1_696_459_917_000_f64; |
||||
let mar_1_2020 = 1_583_020_800_000_f64; |
||||
let feb_29_2020 = 1_582_934_400_000_f64; |
||||
let mar_1_2021 = 1_614_556_800_000_f64; |
||||
|
||||
assert_eq!(epoch_time_to_month_in_year(oct_2023), 9); |
||||
assert_eq!(epoch_time_to_month_in_year(mar_1_2020), 2); |
||||
assert_eq!(mathematical_in_leap_year(mar_1_2020), 1); |
||||
assert_eq!(epoch_time_to_month_in_year(feb_29_2020), 1); |
||||
assert_eq!(mathematical_in_leap_year(feb_29_2020), 1); |
||||
assert_eq!(epoch_time_to_month_in_year(mar_1_2021), 2); |
||||
assert_eq!(mathematical_in_leap_year(mar_1_2021), 0); |
||||
} |
||||
} |
Loading…
Reference in new issue