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