diff --git a/src/builtins/core/year_month.rs b/src/builtins/core/year_month.rs index 547a7a03c..04e2f7639 100644 --- a/src/builtins/core/year_month.rs +++ b/src/builtins/core/year_month.rs @@ -1,6 +1,6 @@ //! This module implements `YearMonth` and any directly related algorithms. -use alloc::string::String; +use alloc::{format, string::String}; use core::{cmp::Ordering, str::FromStr}; use tinystr::TinyAsciiStr; @@ -9,7 +9,6 @@ use crate::{ iso::{year_month_within_limits, IsoDate}, options::{ArithmeticOverflow, DifferenceOperation, DifferenceSettings, DisplayCalendar}, parsers::{FormattableCalendar, FormattableDate, FormattableYearMonth}, - utils::pad_iso_year, Calendar, MonthCode, TemporalError, TemporalResult, TemporalUnwrap, }; @@ -103,11 +102,19 @@ impl PlainYearMonth { self.iso.year } - /// Returns the padded ISO year string + /// 3.5.11 PadISOYear ( y ) + /// + /// Returns a String representation of y suitable for inclusion in an ISO 8601 string. #[inline] #[must_use] pub fn padded_iso_year_string(&self) -> String { - pad_iso_year(self.iso.year) + let year = self.iso.year; + if (0..9999).contains(&year) { + return format!("{:04}", year); + } + let year_sign = if year > 0 { "+" } else { "-" }; + let year_string = format!("{:06}", year.abs()); + format!("{year_sign}{year_string}",) } /// Returns the iso month value for this `YearMonth`. diff --git a/src/iso.rs b/src/iso.rs index 1029bbf62..4770886b2 100644 --- a/src/iso.rs +++ b/src/iso.rs @@ -91,7 +91,7 @@ impl IsoDateTime { // 3. Let epochMilliseconds be 𝔽((epochNanoseconds - remainderNs) / 10^6). let epoch_millis = (mathematical_nanos - remainder_nanos) / 1_000_000; - let (year, month, day) = utils::ymd_from_epoch_milliseconds(epoch_millis); + let (year, month, day) = utils::Epoch::new(epoch_millis).ymd(); // 7. Let hour be ℝ(! HourFromTime(epochMilliseconds)). let hour = epoch_millis.div_euclid(3_600_000).rem_euclid(24); @@ -344,8 +344,7 @@ impl IsoDate { /// Equivalent to `BalanceISODate`. pub(crate) fn balance(year: i32, month: i32, day: i32) -> Self { let epoch_days = iso_date_to_epoch_days(year, month, day); - let ms = utils::epoch_days_to_epoch_ms(epoch_days, 0); - let (year, month, day) = utils::ymd_from_epoch_milliseconds(ms); + let (year, month, day) = utils::Epoch::from_days(epoch_days).ymd(); Self::new_unchecked(year, month, day) } @@ -367,7 +366,7 @@ impl IsoDate { /// Equivalent to `IsoDateToEpochDays` #[inline] pub(crate) fn to_epoch_days(self) -> i32 { - utils::epoch_days_from_gregorian_date(self.year, self.month, self.day) + utils::Epoch::from_gregorian_date(self.year, self.month, self.day).days() } /// Returns if the current `IsoDate` is valid. @@ -488,12 +487,13 @@ impl IsoDate { // NOTE: Below is adapted from the polyfill. Preferring this as it avoids looping. // 11. Let weeks be 0. - let days = utils::epoch_days_from_gregorian_date(other.year, other.month, other.day) - - utils::epoch_days_from_gregorian_date( + let days = utils::Epoch::from_gregorian_date(other.year, other.month, other.day).days() + - utils::Epoch::from_gregorian_date( constrained.year, constrained.month, constrained.day, - ); + ) + .days(); let (weeks, days) = if largest_unit == TemporalUnit::Week { (days / 7, days % 7) @@ -905,8 +905,7 @@ const MAX_EPOCH_DAYS: i32 = 10i32.pow(8) + 1; #[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 utils::epoch_days_from_gregorian_date(date.year, date.month, date.day).abs() > MAX_EPOCH_DAYS - { + if utils::Epoch::from_gregorian_date(date.year, date.month, date.day).days() > MAX_EPOCH_DAYS { return false; } @@ -927,7 +926,7 @@ fn utc_epoch_nanos(date: IsoDate, time: &IsoTime) -> TemporalResult i128 { let ms = time.to_epoch_ms(); - let epoch_ms = utils::epoch_days_to_epoch_ms(date.to_epoch_days(), ms); + let epoch_ms = utils::Epoch::from_days(date.to_epoch_days()).millis() + ms; epoch_ms as i128 * 1_000_000 + time.microsecond as i128 * 1_000 + time.nanosecond as i128 } @@ -943,10 +942,10 @@ pub(crate) fn iso_date_to_epoch_days(year: i32, month: i32, day: i32) -> i32 { let resolved_month = month.rem_euclid(12) as u8; // 3. Find a time t such that EpochTimeToEpochYear(t) is resolvedYear, // EpochTimeToMonthInYear(t) is resolvedMonth, and EpochTimeToDate(t) is 1. - let epoch_days = utils::epoch_days_from_gregorian_date(resolved_year, resolved_month, 1); + let epoch_days = utils::Epoch::from_gregorian_date(resolved_year, resolved_month, 1); // 4. Return EpochTimeToDayNumber(t) + date - 1. - epoch_days + day - 1 + epoch_days.days() + day - 1 } #[inline] @@ -997,13 +996,13 @@ fn balance_iso_year_month(year: i32, month: i32) -> (i32, u8) { /// Note: month is 1 based. #[inline] pub(crate) fn constrain_iso_day(year: i32, month: u8, day: u8) -> u8 { - let days_in_month = utils::iso_days_in_month(year, month); + let days_in_month = utils::Epoch::from_gregorian_date(year, month, 1).days_in_month(); day.clamp(1, days_in_month) } #[inline] pub(crate) fn is_valid_iso_day(year: i32, month: u8, day: u8) -> bool { - let days_in_month = utils::iso_days_in_month(year, month); + let days_in_month = utils::Epoch::from_gregorian_date(year, month, 1).days_in_month(); (1..=days_in_month).contains(&day) } diff --git a/src/tzdb.rs b/src/tzdb.rs index 34044809b..6db55d4dd 100644 --- a/src/tzdb.rs +++ b/src/tzdb.rs @@ -47,11 +47,12 @@ use tzif::{ }, }; +use crate::utils::Epoch; use crate::{ iso::IsoDateTime, provider::{TimeZoneOffset, TimeZoneProvider, TransitionDirection}, time::EpochNanoseconds, - utils, TemporalError, TemporalResult, + TemporalError, TemporalResult, }; #[cfg(target_family = "unix")] @@ -358,79 +359,31 @@ fn resolve_posix_tz_string_for_epoch_seconds( // TODO: Resolve safety issue around utils. // Using f64 is a hold over from early implementation days and should // be moved away from. + let epoch = Epoch::from_seconds(seconds); + let start_date_epoch = transition_day_to_epoch(epoch.year(), &start.day); + let end_date_epoch = transition_day_to_epoch(epoch.year(), &end.day); - let (is_transition_day, transition) = - cmp_seconds_to_transitions(&start.day, &end.day, seconds)?; + let (is_transition_day, transition_type) = + get_transition_info(start_date_epoch, end_date_epoch, epoch); - let transition = - compute_tz_for_epoch_seconds(is_transition_day, transition, seconds, dst_variant); + let transition_type = + compute_tz_for_epoch(is_transition_day, transition_type, epoch, dst_variant); let std_offset = LocalTimeRecord::from_standard_time(&posix_tz_string.std_info).offset; let dst_offset = LocalTimeRecord::from_daylight_savings_time(&dst_variant.variant_info).offset; - let (old_offset, new_offset) = match transition { - TransitionType::Dst => (std_offset, dst_offset), - TransitionType::Std => (dst_offset, std_offset), - }; - let transition = match transition { - TransitionType::Dst => start, - TransitionType::Std => end, - }; - let year = utils::epoch_time_to_epoch_year(seconds * 1000); - let year_epoch = utils::epoch_days_for_year(year) * 86400; - let leap_day = utils::mathematical_in_leap_year(seconds * 1000) as u16; - - let days = match transition.day { - TransitionDay::NoLeap(day) if day > 59 => day - 1 + leap_day, - TransitionDay::NoLeap(day) => day - 1, - TransitionDay::WithLeap(day) => day, - TransitionDay::Mwd(month, week, day) => { - let days_to_month = utils::month_to_day((month - 1) as u8, leap_day); - let days_in_month = u16::from(utils::iso_days_in_month(year, month as u8) - 1); - - // Month starts in the day... - let day_offset = - (u16::from(utils::epoch_seconds_to_day_of_week(i64::from(year_epoch))) - + days_to_month) - .rem_euclid(7); - - // EXAMPLE: - // - // 0 1 2 3 4 5 6 - // sun mon tue wed thu fri sat - // - - - 0 1 2 3 - // 4 5 6 7 8 9 10 - // 11 12 13 14 15 16 17 - // 18 19 20 21 22 23 24 - // 25 26 27 28 29 30 - - // - // The day_offset = 3, since the month starts on a wednesday. - // - // We're looking for the second friday of the month. Thus, since the month started before - // a friday, we need to start counting from week 0: - // - // day_of_month = (week - u16::from(day_offset <= day)) * 7 + day - day_offset = (2 - 1) * 7 + 5 - 3 = 9 - // - // This works if the month started on a day before the day we want (day_offset <= day). However, if that's not the - // case, we need to start counting on week 1. For example, calculate the day of the month for the third monday - // of the month: - // - // day_of_month = (week - u16::from(day_offset <= day)) * 7 + day - day_offset = (3 - 0) * 7 + 1 - 3 = 19 - let mut day_of_month = (week - u16::from(day_offset <= day)) * 7 + day - day_offset; - - // If we're on week 5, we need to clamp to the last valid day. - if day_of_month > days_in_month - 1 { - day_of_month -= 7 - } - - days_to_month + day_of_month - } + let offset = match transition_type { + TransitionType::Dst => dst_offset, + TransitionType::Std => std_offset, }; // Transition time is on local time, so we need to add the UTC offset to get the correct UTC timestamp // for the transition. - let transition_epoch = - i64::from(year_epoch) + i64::from(days) * 86400 + transition.time.0 - old_offset; + let transition_epoch = match transition_type { + TransitionType::Dst => start_date_epoch.seconds() + start.time.0 - std_offset, + TransitionType::Std => end_date_epoch.seconds() + end.time.0 - dst_offset, + }; + Ok(TimeZoneOffset { - offset: new_offset, + offset, transition_epoch: Some(transition_epoch), }) } @@ -456,10 +409,13 @@ fn resolve_posix_tz_string( // NOTE: // STD -> DST == start // DST -> STD == end - let (is_transition_day, is_dst) = - cmp_seconds_to_transitions(&dst.start_date.day, &dst.end_date.day, seconds)?; + let epoch = Epoch::from_seconds(seconds); + let start_transition = transition_day_to_epoch(epoch.year(), &dst.start_date.day); + let end_transition = transition_day_to_epoch(epoch.year(), &dst.end_date.day); + let (is_transition_day, is_dst) = get_transition_info(start_transition, end_transition, epoch); + if is_transition_day { - let time = utils::epoch_ms_to_ms_in_day(seconds * 1_000) as i64 / 1_000; + let time = epoch.millis_since_start_of_day() as i64 / 1_000; let transition_time = if is_dst == TransitionType::Dst { dst.start_date.time.0 } else { @@ -493,103 +449,59 @@ fn resolve_posix_tz_string( } } -fn compute_tz_for_epoch_seconds( +fn compute_tz_for_epoch( is_transition_day: bool, - transition: TransitionType, - seconds: i64, + typ: TransitionType, + epoch: Epoch, dst_variant: &DstTransitionInfo, ) -> TransitionType { - if is_transition_day && transition == TransitionType::Dst { - let time = utils::epoch_ms_to_ms_in_day(seconds * 1_000) / 1_000; + let time = epoch.millis_since_start_of_day() / 1000; + if is_transition_day && typ == TransitionType::Dst { let transition_time = dst_variant.start_date.time.0 - dst_variant.variant_info.offset.0; if i64::from(time) < transition_time { return TransitionType::Std; } } else if is_transition_day { - let time = utils::epoch_ms_to_ms_in_day(seconds * 1_000) / 1_000; let transition_time = dst_variant.end_date.time.0 - dst_variant.variant_info.offset.0; if i64::from(time) < transition_time { return TransitionType::Dst; } } - transition + typ } -/// The month, week of month, and day of week value built into the POSIX tz string. -/// -/// For more information, see the [POSIX tz string docs](https://sourceware.org/glibc/manual/2.40/html_node/Proleptic-TZ.html) -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -struct Mwd(u16, u16, u16); - -impl Mwd { - fn from_seconds(seconds: i64) -> Self { - let month = utils::epoch_ms_to_month_in_year(seconds * 1_000) as u16; - let day_of_month = utils::epoch_seconds_to_day_of_month(seconds); - let week_of_month = day_of_month / 7 + 1; - let day_of_week = utils::epoch_seconds_to_day_of_week(seconds); - Self(month, week_of_month, u16::from(day_of_week)) +fn transition_day_to_epoch(year: i32, day: &TransitionDay) -> Epoch { + match day { + TransitionDay::Mwd(month, week, day) => { + Epoch::from_posix_date(year, *month as u8, *week as u8, *day as u8) + } + TransitionDay::WithLeap(day) => Epoch::from_year_and_day_of_year(year, *day + 1), + TransitionDay::NoLeap(day) if Epoch::from_year(year).in_leap_year() => { + Epoch::from_year_and_day_of_year(year, *day + u16::from(*day > 59)) + } + TransitionDay::NoLeap(day) => Epoch::from_year_and_day_of_year(year, *day), } } -fn cmp_seconds_to_transitions( - start: &TransitionDay, - end: &TransitionDay, - seconds: i64, -) -> TemporalResult<(bool, TransitionType)> { - let cmp_result = match (start, end) { - ( - TransitionDay::Mwd(start_month, start_week, start_day), - TransitionDay::Mwd(end_month, end_week, end_day), - ) => { - let mwd = Mwd::from_seconds(seconds); - let start = Mwd(*start_month, *start_week, *start_day); - let end = Mwd(*end_month, *end_week, *end_day); - - let is_transition = start == mwd || end == mwd; - let is_dst = if start > end { - mwd < end || start <= mwd - } else { - start <= mwd && mwd < end - }; - - (is_transition, is_dst) - } - (TransitionDay::WithLeap(start), TransitionDay::WithLeap(end)) => { - let day_in_year = utils::epoch_time_to_day_in_year(seconds * 1_000) as u16; - let is_transition = *start == day_in_year || *end == day_in_year; - let is_dst = if start > end { - day_in_year < *end || *start <= day_in_year - } else { - *start <= day_in_year && day_in_year < *end - }; - (is_transition, is_dst) - } - // TODO: do we need to modify the logic for leap years? - (TransitionDay::NoLeap(start), TransitionDay::NoLeap(end)) => { - let day_in_year = utils::epoch_time_to_day_in_year(seconds * 1_000) as u16; - let is_transition = *start == day_in_year || *end == day_in_year; - let is_dst = if start > end { - day_in_year < *end || *start <= day_in_year - } else { - *start <= day_in_year && day_in_year < *end - }; - (is_transition, is_dst) - } - // NOTE: The assumption here is that mismatched day types on - // a POSIX string is an illformed string. - _ => { - return Err( - TemporalError::assert().with_message("Mismatched day types on a POSIX string.") - ) - } +fn get_transition_info( + start_epoch: Epoch, + end_epoch: Epoch, + epoch: Epoch, +) -> (bool, TransitionType) { + let is_transition_day = std::dbg!(start_epoch.days()) == std::dbg!(epoch.days()) + || std::dbg!(end_epoch.days()) == epoch.days(); + let is_dst = if start_epoch > end_epoch { + epoch < end_epoch || start_epoch <= epoch + } else { + start_epoch <= epoch && epoch < end_epoch }; - match cmp_result { - (true, dst) if dst => Ok((true, TransitionType::Dst)), - (true, _) => Ok((true, TransitionType::Std)), - (false, dst) if dst => Ok((false, TransitionType::Dst)), - (false, _) => Ok((false, TransitionType::Std)), + match (is_transition_day, is_dst) { + (true, true) => (true, TransitionType::Dst), + (true, false) => (true, TransitionType::Std), + (false, true) => (false, TransitionType::Dst), + (false, false) => (false, TransitionType::Std), } } @@ -1082,16 +994,13 @@ mod tests { } #[test] - fn mwd_transition_epoch() { - #[cfg(not(target_os = "windows"))] - let tzif = Tzif::read_tzif("Europe/Berlin").unwrap(); - #[cfg(target_os = "windows")] + fn mwd_transition_epoch_with_slim_format() { let tzif = Tzif::from_bytes(jiff_tzdb::get("Europe/Berlin").unwrap().1).unwrap(); let start_date = crate::iso::IsoDate { year: 2028, month: 3, - day: 30, + day: 26, }; let start_time = crate::iso::IsoTime { hour: 6, diff --git a/src/utils.rs b/src/utils.rs index 10ac6a4a8..d45f58764 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,168 +1,217 @@ -//! Utility date and time equations for Temporal +#![allow( + unused, + reason = "prefer to have unused methods instead of having to gate everything behind features" +)] -use alloc::format; -use alloc::string::String; +//! Utility date and time equations for Temporal +// NOTE: Potentially add more tests. use crate::MS_PER_DAY; +pub(crate) const MS_PER_HOUR: i64 = 3_600_000; +pub(crate) const MS_PER_MINUTE: i64 = 60_000; mod neri_schneider; -pub(crate) use neri_schneider::epoch_days_from_gregorian_date; +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct Epoch { + millis: i64, +} -// NOTE: Potentially add more of tests. +impl Epoch { + /// Creates a new epoch. + pub(crate) fn new(millis: i64) -> Self { + Self { millis } + } -// ==== Begin Date Equations ==== + /// Creates a new epoch from a given epoch second. + pub(crate) fn from_seconds(secs: i64) -> Self { + Self { + millis: secs * 1000, + } + } -pub(crate) const MS_PER_HOUR: i64 = 3_600_000; -pub(crate) const MS_PER_MINUTE: i64 = 60_000; + /// Creates a new epoch from a year. + pub(crate) fn from_year(year: i32) -> Self { + Self::from_days(epoch_days_for_year(year)) + } -/// `EpochDaysToEpochMS` -/// -/// Functionally the same as Date's abstract operation `MakeDate` -pub(crate) fn epoch_days_to_epoch_ms(day: i32, time: i64) -> i64 { - (day as i64 * MS_PER_DAY as i64) + time -} + /// `EpochDaysToEpochMS` + /// Creates a new epoch from a given epoch day. + /// + /// Functionally the same as Date's abstract operation `MakeDate` + pub(crate) fn from_days(days: i32) -> Self { + Self::new(i64::from(days) * i64::from(MS_PER_DAY)) + } -/// 3.5.11 PadISOYear ( y ) -/// -/// returns a String representation of y suitable for inclusion in an ISO 8601 string -pub(crate) fn pad_iso_year(year: i32) -> String { - if (0..9999).contains(&year) { - return format!("{:04}", year); - } - let year_sign = if year > 0 { "+" } else { "-" }; - let year_string = format!("{:06}", year.abs()); - format!("{year_sign}{year_string}",) -} + /// Creates a new epoch from a year and a day of year (1-based). + pub(crate) fn from_year_and_day_of_year(year: i32, day: u16) -> Self { + Self::from_days(epoch_days_for_year(year) + i32::from(day) - 1) + } -/// `EpochTimeToDayNumber` -/// -/// This equation is the equivalent to `ECMAScript`'s `Date(t)` -#[cfg(feature = "tzdb")] -pub(crate) fn epoch_time_to_day_number(t: i64) -> i32 { - t.div_euclid(MS_PER_DAY as i64) as i32 -} + /// Creates a new epoch from a gregorian date + pub(crate) fn from_gregorian_date(year: i32, month: u8, day: u8) -> Self { + Self::from_days(neri_schneider::epoch_days_from_gregorian_date( + year, month, day, + )) + } -#[cfg(feature = "tzdb")] -pub(crate) fn epoch_ms_to_ms_in_day(t: i64) -> u32 { - (t.rem_euclid(i64::from(MS_PER_DAY))) as u32 -} + /// Creates a new epoch from a POSIX date. + pub(crate) fn from_posix_date(year: i32, month: u8, week: u8, day: u8) -> Self { + let leap_year = in_leap_year(year); + let days_in_month = days_in_month(month, in_leap_year(year)) - 1; + let days_to_year = epoch_days_for_year(year); + + let days_to_month = + days_to_year + i32::from(day_of_year_until_start_of_month(month, leap_year)); + + // Month starts in the day... + let day_offset = self::day_of_week(days_to_month); + + // EXAMPLE: + // + // 0 1 2 3 4 5 6 + // sun mon tue wed thu fri sat + // - - - 0 1 2 3 + // 4 5 6 7 8 9 10 + // 11 12 13 14 15 16 17 + // 18 19 20 21 22 23 24 + // 25 26 27 28 29 30 - + // + // The day_offset = 3, since the month starts on a wednesday. + // + // We're looking for the second friday of the month. Thus, since the month started before + // a friday, we need to start counting from week 0: + // + // day_of_month = (week - u16::from(day_offset <= day)) * 7 + day - day_offset = (2 - 1) * 7 + 5 - 3 = 9 + // + // This works if the month started on a day before the day we want (day_offset <= day). However, if that's not the + // case, we need to start counting on week 1. For example, calculate the day of the month for the third monday + // of the month: + // + // day_of_month = (week - u16::from(day_offset <= day)) * 7 + day - day_offset = (3 - 0) * 7 + 1 - 3 = 19 + let mut day_of_month = (week - u8::from(day_offset <= day)) * 7 + day - day_offset; + + // If we're on week 5, we need to clamp to the last valid day. + if day_of_month > days_in_month { + day_of_month -= 7 + } + + Self::from_days(days_to_month + i32::from(day_of_month)) + } -/// 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 + /// Gets the total elapsed milliseconds of this epoch. + pub(crate) fn millis(self) -> i64 { + self.millis } -} -pub(crate) fn epoch_time_to_epoch_year(t: i64) -> i32 { - let epoch_days = epoch_ms_to_epoch_days(t); - let (rata_die, shift_constant) = neri_schneider::rata_die_for_epoch_days(epoch_days); - neri_schneider::year(rata_die, shift_constant) -} + /// Gets the total elapsed seconds of this epoch. + pub(crate) fn seconds(self) -> i64 { + self.millis / 1000 + } -/// Returns either 1 (true) or 0 (false) -pub(crate) fn mathematical_in_leap_year(t: i64) -> i32 { - mathematical_days_in_year(epoch_time_to_epoch_year(t)) - 365 -} + /// `EpochTimeToDayNumber` + /// Gets the total elapsed days of this epoch. + /// + /// This equation is the equivalent to `ECMAScript`'s `Date(t)` + pub(crate) fn days(self) -> i32 { + self.millis.div_euclid(i64::from(MS_PER_DAY)) as i32 + } -/// Returns the epoch day number for a given year. -pub(crate) fn epoch_days_for_year(y: i32) -> i32 { - 365 * (y - 1970) + (y - 1969).div_euclid(4) - (y - 1901).div_euclid(100) - + (y - 1601).div_euclid(400) -} + /// Gets the total elapsed years of this epoch. + pub(crate) fn year(self) -> i32 { + let (rata_die, shift_constant) = neri_schneider::rata_die_for_epoch_days(self.days()); + neri_schneider::year(rata_die, shift_constant) + } -// TODO: test limits -pub(crate) fn epoch_time_for_year(y: i32) -> i64 { - i64::from(MS_PER_DAY) * i64::from(epoch_days_for_year(y)) -} + /// Returns the year, month and day of the month for a given millisecond epoch. + pub(crate) fn ymd(self) -> (i32, u8, u8) { + neri_schneider::ymd_from_epoch_days(self.days()) + } -pub(crate) const fn epoch_ms_to_epoch_days(ms: i64) -> i32 { - (ms.div_euclid(MS_PER_DAY as i64)) as i32 -} + /// Returns the total elapsed milliseconds since the last start of day. + pub(crate) fn millis_since_start_of_day(self) -> u32 { + (self.millis.rem_euclid(i64::from(MS_PER_DAY))) as u32 + } -pub(crate) fn ymd_from_epoch_milliseconds(epoch_milliseconds: i64) -> (i32, u8, u8) { - let epoch_days = epoch_ms_to_epoch_days(epoch_milliseconds); - neri_schneider::ymd_from_epoch_days(epoch_days) -} + /// Returns `true` if the epoch is within a leap year. + pub(crate) fn in_leap_year(self) -> bool { + in_leap_year(self.year()) + } -#[cfg(feature = "tzdb")] -pub(crate) fn month_to_day(m: u8, leap_day: u16) -> u16 { - match m { - 0 => 0, - 1 => 31, - 2 => 59 + leap_day, - 3 => 90 + leap_day, - 4 => 120 + leap_day, - 5 => 151 + leap_day, - 6 => 181 + leap_day, - 7 => 212 + leap_day, - 8 => 243 + leap_day, - 9 => 273 + leap_day, - 10 => 304 + leap_day, - 11 => 334 + leap_day, - _ => unreachable!(), + /// Returns the month of the year of this epoch (1-based). + pub(crate) fn month_in_year(self) -> u8 { + let epoch_days = self.days(); + let (rata_die, _) = neri_schneider::rata_die_for_epoch_days(epoch_days); + neri_schneider::month(rata_die) } -} -#[cfg(feature = "tzdb")] -pub(crate) fn epoch_ms_to_month_in_year(t: i64) -> u8 { - let epoch_days = epoch_ms_to_epoch_days(t); - let (rata_die, _) = neri_schneider::rata_die_for_epoch_days(epoch_days); - neri_schneider::month(rata_die) + /// 12.2.31 `ISODaysInMonth ( year, month )` + /// + /// Returns the number of days of the current month (1-based). + pub(crate) fn days_in_month(self) -> u8 { + days_in_month(self.month_in_year(), self.in_leap_year()) + } } -#[cfg(feature = "tzdb")] -pub(crate) fn epoch_time_to_day_in_year(t: i64) -> i32 { - epoch_time_to_day_number(t) - (epoch_days_for_year(epoch_time_to_epoch_year(t))) +/// Returns the elapsed days until the start of the current month (0-based). +fn day_of_year_until_start_of_month(month: u8, leap_year: bool) -> u16 { + let leap_day = u16::from(leap_year); + match month { + 1 => 0, + 2 => 31, + 3 => 59 + leap_day, + 4 => 90 + leap_day, + 5 => 120 + leap_day, + 6 => 151 + leap_day, + 7 => 181 + leap_day, + 8 => 212 + leap_day, + 9 => 243 + leap_day, + 10 => 273 + leap_day, + 11 => 304 + leap_day, + 12 => 334 + leap_day, + _ => unreachable!(), + } } -#[cfg(feature = "tzdb")] -pub(crate) fn epoch_seconds_to_day_of_week(t: i64) -> u8 { - ((t / 86_400) + 4).rem_euclid(7) as u8 +/// Returns the day of the week of the given epoch day. +pub(crate) fn day_of_week(day: i32) -> u8 { + (day + 4).rem_euclid(7) as u8 } -#[cfg(feature = "tzdb")] -pub(crate) fn epoch_seconds_to_day_of_month(t: i64) -> u16 { - let leap_day = mathematical_in_leap_year(t); - epoch_time_to_day_in_year(t * 1_000) as u16 - - month_to_day(epoch_ms_to_month_in_year(t * 1_000) - 1, leap_day as u16) +/// Returns `true` if the year is a leap year. +fn in_leap_year(year: i32) -> bool { + days_in_year(year) > 365 } -// Trait implementations - -// EpochTimeTOWeekDay -> REMOVED - -// ==== End Date Equations ==== - -// ==== Begin Calendar Equations ==== - -// NOTE: below was the iso methods in temporal::calendar -> Need to be reassessed. +/// Returns the number of days the given year has. +fn days_in_year(year: i32) -> u16 { + if year % 4 != 0 { + 365 + } else if year % 4 == 0 && year % 100 != 0 { + 366 + } else if year % 100 == 0 && year % 400 != 0 { + 365 + } else { + // Assert that y is divisble by 400 to ensure we are returning the correct result. + assert_eq!(year % 400, 0); + 366 + } +} /// 12.2.31 `ISODaysInMonth ( year, month )` /// -/// NOTE: month is 1 based -pub(crate) fn iso_days_in_month(year: i32, month: u8) -> u8 { +/// Returns the number of days the current month has (1-based). +fn days_in_month(month: u8, leap_year: bool) -> u8 { 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)) as u8, + 2 => 28 + u8::from(leap_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 ==== +/// Returns the number of days since the epoch for a given year. +fn epoch_days_for_year(year: i32) -> i32 { + 365 * (year - 1970) + (year - 1969).div_euclid(4) - (year - 1901).div_euclid(100) + + (year - 1601).div_euclid(400) +} diff --git a/src/utils/neri_schneider.rs b/src/utils/neri_schneider.rs index 223a6361d..9ffa1ebf2 100644 --- a/src/utils/neri_schneider.rs +++ b/src/utils/neri_schneider.rs @@ -84,7 +84,6 @@ const fn n_two(rata_die: u32) -> u32 { century_rem(rata_die) | 3 } -#[cfg(feature = "tzdb")] const fn n_three(rata_die: u32) -> u32 { 2141 * computational_day_of_year(rata_die) + 197_913 } @@ -143,7 +142,6 @@ pub const fn computational_year(rata_die: u32) -> u32 { 100 * century_number(rata_die) + computational_year_of_century(rata_die) as u32 } -#[cfg(feature = "tzdb")] pub const fn computational_month(rata_die: u32) -> u32 { n_three(rata_die).div_euclid(TWO_POWER_SIXTEEN) } @@ -152,7 +150,6 @@ pub const fn year(computational_rata_die: u32, shift_constant: i32) -> i32 { (computational_year(computational_rata_die) + j(computational_rata_die)) as i32 - shift_constant } -#[cfg(feature = "tzdb")] pub const fn month(compulational_rata_die: u32) -> u8 { (computational_month(compulational_rata_die) - 12 * j(compulational_rata_die)) as u8 }