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