diff --git a/src/epoch.rs b/src/epoch.rs index 79cc681..f09b2ee 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -21,6 +21,7 @@ use core::hash::{Hash, Hasher}; use core::ops::{Add, AddAssign, Sub, SubAssign}; use crate::ParsingErrors; +use crate::Weekday; #[cfg(feature = "python")] use pyo3::prelude::*; @@ -36,7 +37,7 @@ use core::str::FromStr; use std::time::SystemTime; #[cfg(not(feature = "std"))] -use num_traits::Float; +use num_traits::{Euclid, Float}; const TT_OFFSET_MS: i64 = 32_184; const ET_OFFSET_US: i64 = 32_184_935; @@ -1186,6 +1187,25 @@ impl Epoch { ) } } + + /// Builds an Epoch from given `week`: elapsed weeks counter into the desired Time scale, and "ns" amount of nanoseconds since closest Sunday Midnight. + /// For example, this is how GPS vehicles describe a GPST epoch. + #[must_use] + pub fn from_time_of_week(week: u32, nanoseconds: u64, ts: TimeScale) -> Self { + let duration = i64::from(week) * Weekday::DAYS_PER_WEEK_I64 * Unit::Day + + Duration::from_parts(0, nanoseconds); + let gh_187 = match ts { + TimeScale::UTC | TimeScale::TT | TimeScale::TAI => 1.0 * Unit::Day, + _ => Duration::ZERO, + }; + Self::from_duration(duration - gh_187, ts) + } + + #[must_use] + /// Builds an UTC Epoch from given `week`: elapsed weeks counter and "ns" amount of nanoseconds since closest Sunday Midnight. + pub fn from_time_of_week_utc(week: u32, nanoseconds: u64) -> Self { + Self::from_time_of_week(week, nanoseconds, TimeScale::UTC) + } } #[cfg_attr(feature = "python", pymethods)] @@ -2158,6 +2178,7 @@ impl Epoch { Self::compute_gregorian(self.to_tai_duration()) } + #[must_use] /// Floors this epoch to the closest provided duration /// /// # Example @@ -2180,6 +2201,7 @@ impl Epoch { Self::from_duration(self.to_duration().floor(duration), self.time_scale) } + #[must_use] /// Ceils this epoch to the closest provided duration in the TAI time system /// /// # Example @@ -2203,6 +2225,7 @@ impl Epoch { Self::from_duration(self.to_duration().ceil(duration), self.time_scale) } + #[must_use] /// Rounds this epoch to the closest provided duration in TAI /// /// # Example @@ -2219,6 +2242,7 @@ impl Epoch { Self::from_duration(self.to_duration().round(duration), self.time_scale) } + #[must_use] /// Copies this epoch and sets it to the new time scale provided. pub fn in_time_scale(&self, new_time_scale: TimeScale) -> Self { let mut me = *self; @@ -2226,6 +2250,189 @@ impl Epoch { me } + #[must_use] + /// Converts this epoch into the time of week, represented as a rolling week counter into that time scale and the number of nanoseconds since closest Sunday midnight into that week. + /// This is usually how GNSS receivers describe a timestamp. + pub fn to_time_of_week(&self) -> (u32, u64) { + // wk: rolling week counter into timescale + // fractional days in this time scale + let wk = div_euclid_f64( + self.to_duration().to_unit(Unit::Day), + Weekday::DAYS_PER_WEEK, + ); + let mut start_of_week = self.previous_weekday_at_midnight(Weekday::Sunday); + let ref_epoch = self.time_scale.ref_epoch(); + // restrict start of week/sunday to the start of the time scale + if start_of_week < ref_epoch { + start_of_week = ref_epoch; + } + let dw = *self - start_of_week; // difference in weekdays [0..6] + (wk as u32, dw.nanoseconds) + } + + #[must_use] + /// Returns the weekday in provided time scale **ASSUMING** that the reference epoch of that time scale is a Monday. + /// You _probably_ do not want to use this. You probably either want `weekday()` or `weekday_utc()`. + /// Several time scales do _not_ have a reference day that's on a Monday, e.g. BDT. + pub fn weekday_in_time_scale(&self, time_scale: TimeScale) -> Weekday { + (rem_euclid_f64( + self.to_duration_in_time_scale(time_scale) + .to_unit(Unit::Day), + Weekday::DAYS_PER_WEEK, + ) + .floor() as u8) + .into() + } + + /// Returns weekday (uses the TAI representation for this calculation). + pub fn weekday(&self) -> Weekday { + // J1900 was a Monday so we just have to modulo the number of days by the number of days per week. + // The function call will be optimized away. + self.weekday_in_time_scale(TimeScale::TAI) + } + + /// Returns weekday in UTC timescale + pub fn weekday_utc(&self) -> Weekday { + self.weekday_in_time_scale(TimeScale::UTC) + } + + /// Returns the next weekday. + /// + /// ``` + /// use hifitime::prelude::*; + /// + /// let epoch = Epoch::from_gregorian_utc_at_midnight(1988, 1, 2); + /// assert_eq!(epoch.weekday_utc(), Weekday::Saturday); + /// assert_eq!(epoch.next(Weekday::Sunday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 3)); + /// assert_eq!(epoch.next(Weekday::Monday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 4)); + /// assert_eq!(epoch.next(Weekday::Tuesday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 5)); + /// assert_eq!(epoch.next(Weekday::Wednesday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 6)); + /// assert_eq!(epoch.next(Weekday::Thursday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 7)); + /// assert_eq!(epoch.next(Weekday::Friday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 8)); + /// assert_eq!(epoch.next(Weekday::Saturday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 9)); + /// ``` + pub fn next(&self, weekday: Weekday) -> Self { + let delta_days = self.weekday() - weekday; + if delta_days == Duration::ZERO { + *self + 7 * Unit::Day + } else { + *self + delta_days + } + } + + pub fn next_weekday_at_midnight(&self, weekday: Weekday) -> Self { + self.next(weekday).with_hms_strict(0, 0, 0) + } + + pub fn next_weekday_at_noon(&self, weekday: Weekday) -> Self { + self.next(weekday).with_hms_strict(12, 0, 0) + } + + /// Returns the next weekday. + /// + /// ``` + /// use hifitime::prelude::*; + /// + /// let epoch = Epoch::from_gregorian_utc_at_midnight(1988, 1, 2); + /// assert_eq!(epoch.previous(Weekday::Friday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 1)); + /// assert_eq!(epoch.previous(Weekday::Thursday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 31)); + /// assert_eq!(epoch.previous(Weekday::Wednesday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 30)); + /// assert_eq!(epoch.previous(Weekday::Tuesday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 29)); + /// assert_eq!(epoch.previous(Weekday::Monday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 28)); + /// assert_eq!(epoch.previous(Weekday::Sunday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 27)); + /// assert_eq!(epoch.previous(Weekday::Saturday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 26)); + /// ``` + pub fn previous(&self, weekday: Weekday) -> Self { + let delta_days = weekday - self.weekday(); + if delta_days == Duration::ZERO { + *self - 7 * Unit::Day + } else { + *self - delta_days + } + } + + pub fn previous_weekday_at_midnight(&self, weekday: Weekday) -> Self { + self.previous(weekday).with_hms_strict(0, 0, 0) + } + + pub fn previous_weekday_at_noon(&self, weekday: Weekday) -> Self { + self.previous(weekday).with_hms_strict(12, 0, 0) + } + + /// Returns the duration since the start of the year + pub fn duration_in_year(&self) -> Duration { + let year = Self::compute_gregorian(self.to_duration()).0; + let start_of_year = Self::from_gregorian(year, 1, 1, 0, 0, 0, 0, self.time_scale); + self.to_duration() - start_of_year.to_duration() + } + + /// Returns the number of days since the start of the year. + pub fn day_of_year(&self) -> f64 { + self.duration_in_year().to_unit(Unit::Day) + } + + /// Returns the hours of the Gregorian representation of this epoch in the time scale it was initialized in. + pub fn hours(&self) -> u64 { + self.to_duration().decompose().2 + } + + /// Returns the minutes of the Gregorian representation of this epoch in the time scale it was initialized in. + pub fn minutes(&self) -> u64 { + self.to_duration().decompose().3 + } + + /// Returns the seconds of the Gregorian representation of this epoch in the time scale it was initialized in. + pub fn seconds(&self) -> u64 { + self.to_duration().decompose().4 + } + + /// Returns the milliseconds of the Gregorian representation of this epoch in the time scale it was initialized in. + pub fn milliseconds(&self) -> u64 { + self.to_duration().decompose().5 + } + + /// Returns the microseconds of the Gregorian representation of this epoch in the time scale it was initialized in. + pub fn microseconds(&self) -> u64 { + self.to_duration().decompose().6 + } + + /// Returns the nanoseconds of the Gregorian representation of this epoch in the time scale it was initialized in. + pub fn nanoseconds(&self) -> u64 { + self.to_duration().decompose().7 + } + + /// Returns a copy of self where the time is set to the provided hours, minutes, seconds + /// Invalid number of hours, minutes, and seconds will overflow into their higher unit. + /// Warning: this does _not_ set the subdivisions of second to zero. + pub fn with_hms(&self, hours: u64, minutes: u64, seconds: u64) -> Self { + let (sign, days, _, _, _, milliseconds, microseconds, nanoseconds) = + self.to_duration().decompose(); + Self::from_duration( + Duration::compose( + sign, + days, + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds, + ), + self.time_scale, + ) + } + + /// Returns a copy of self where the time is set to the provided hours, minutes, seconds + /// Invalid number of hours, minutes, and seconds will overflow into their higher unit. + /// Warning: this will set the subdivisions of seconds to zero. + pub fn with_hms_strict(&self, hours: u64, minutes: u64, seconds: u64) -> Self { + let (sign, days, _, _, _, _, _, _) = self.to_duration().decompose(); + Self::from_duration( + Duration::compose(sign, days, hours, minutes, seconds, 0, 0, 0), + self.time_scale, + ) + } + // Python helpers #[cfg(feature = "python")] diff --git a/src/lib.rs b/src/lib.rs index 4db2407..1bce8a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,6 +60,24 @@ pub const J2000_TO_J1900_DURATION: Duration = Duration { nanoseconds: 3_155_716_800_000_000_000, }; +/// The Ephemeris Time reference epoch J2000. +pub const J2000_REF_EPOCH_ET: Epoch = Epoch { + duration_since_j1900_tai: Duration { + centuries: 0, + nanoseconds: 3_155_716_767_816_072_748, + }, + time_scale: TimeScale::ET, +}; + +/// The Dynamic Barycentric Time reference epoch J2000. +pub const J2000_REF_EPOCH_TDB: Epoch = Epoch { + duration_since_j1900_tai: Duration { + centuries: 0, + nanoseconds: 3_155_716_767_816_072_704, + }, + time_scale: TimeScale::ET, +}; + mod parser; mod epoch; @@ -78,6 +96,9 @@ pub use timeunits::*; mod timeseries; pub use timeseries::*; +mod weekday; +pub use weekday::*; + /// This module defines all of the deprecated methods. mod deprecated; @@ -85,7 +106,7 @@ mod deprecated; pub mod prelude { pub use crate::{ deprecated::TimeSystem, Duration, Epoch, Errors, Freq, Frequencies, TimeScale, TimeSeries, - TimeUnits, Unit, + TimeUnits, Unit, Weekday, }; } @@ -132,6 +153,8 @@ pub enum ParsingErrors { UnknownFormat, UnknownOrMissingUnit, UnsupportedTimeSystem, + /// Non recognized Weekday description + ParseWeekdayError, } impl fmt::Display for Errors { diff --git a/src/timescale.rs b/src/timescale.rs index 5583998..b4673ec 100644 --- a/src/timescale.rs +++ b/src/timescale.rs @@ -17,7 +17,16 @@ use serde_derive::{Deserialize, Serialize}; use core::fmt; use core::str::FromStr; -use crate::{Duration, Epoch, Errors, ParsingErrors, SECONDS_PER_DAY}; +use crate::{ + Duration, Epoch, Errors, ParsingErrors, J2000_REF_EPOCH_ET, J2000_REF_EPOCH_TDB, + J2000_TO_J1900_DURATION, SECONDS_PER_DAY, +}; + +/// The J1900 reference epoch (1900-01-01 at noon) TAI. +pub const J1900_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration::ZERO); + +/// The J2000 reference epoch (2000-01-01 at midnight) TAI. +pub const J2000_REF_EPOCH: Epoch = Epoch::from_tai_duration(J2000_TO_J1900_DURATION); /// GPS reference epoch is UTC midnight between 05 January and 06 January 1980; cf. . pub const GPST_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration { @@ -99,6 +108,19 @@ impl TimeScale { pub const fn is_gnss(&self) -> bool { matches!(self, Self::GPST | Self::GST | Self::BDT) } + + /// Returns Reference Epoch (t(0)) for given timescale + pub const fn ref_epoch(&self) -> Epoch { + match self { + Self::GPST => GPST_REF_EPOCH, + Self::GST => GST_REF_EPOCH, + Self::BDT => BDT_REF_EPOCH, + Self::ET => J2000_REF_EPOCH_ET, + Self::TDB => J2000_REF_EPOCH_TDB, + // Explicit on purpose in case more time scales end up being supported. + Self::TT | Self::TAI | Self::UTC => J1900_REF_EPOCH, + } + } } impl fmt::Display for TimeScale { diff --git a/src/weekday.rs b/src/weekday.rs new file mode 100644 index 0000000..9c1f7ae --- /dev/null +++ b/src/weekday.rs @@ -0,0 +1,172 @@ +/* + * Hifitime, part of the Nyx Space tools + * Copyright (C) 2022 Christopher Rabotin et al. (cf. AUTHORS.md) + * This Source Code Form is subject to the terms of the Apache + * v. 2.0. If a copy of the Apache License was not distributed with this + * file, You can obtain one at https://www.apache.org/licenses/LICENSE-2.0. + * + * Documentation: https://nyxspace.com/ + */ + +use crate::{Duration, ParsingErrors, Unit}; +use core::fmt; +use core::ops::{Add, AddAssign, Sub, SubAssign}; +use core::str::FromStr; + +#[cfg(feature = "python")] +use pyo3::prelude::*; + +#[cfg(feature = "serde")] +use serde_derive::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[repr(u8)] +#[cfg_attr(feature = "python", pyclass)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Weekday { + Monday = 0, + Tuesday = 1, + Wednesday = 2, + Thursday = 3, + Friday = 4, + Saturday = 5, + Sunday = 6, +} + +impl Default for Weekday { + fn default() -> Self { + Self::Monday + } +} + +impl Weekday { + /// Max: last weekday <=> `Sunday`, used only for conversion to/from u8. + const MAX: u8 = 7; + /// Trivial, but avoid magic numbers. + pub(crate) const DAYS_PER_WEEK: f64 = 7.0; + /// Trivial, but avoid magic numbers. + pub(crate) const DAYS_PER_WEEK_I64: i64 = 7; +} + +impl From for Weekday { + fn from(u: u8) -> Self { + match u.rem_euclid(Self::MAX) { + 0 => Self::Monday, + 1 => Self::Tuesday, + 2 => Self::Wednesday, + 3 => Self::Thursday, + 4 => Self::Friday, + 5 => Self::Saturday, + 6 => Self::Sunday, + _ => Self::default(), // Defaults back to default for other values. + } + } +} + +impl From for Weekday { + fn from(i: i8) -> Self { + Self::from((i.rem_euclid(Self::MAX as i8) + Self::MAX as i8) as u8) + } +} + +impl From for u8 { + fn from(week: Weekday) -> Self { + match week { + Weekday::Monday => 0, + Weekday::Tuesday => 1, + Weekday::Wednesday => 2, + Weekday::Thursday => 3, + Weekday::Friday => 4, + Weekday::Saturday => 5, + Weekday::Sunday => 6, + } + } +} + +impl FromStr for Weekday { + type Err = ParsingErrors; + fn from_str(s: &str) -> Result { + match s.trim() { + "monday" | "Monday" | "MONDAY" => Ok(Self::Monday), + "tuesday" | "Tuesday" | "TUESDAY" => Ok(Self::Tuesday), + "wednesday" | "Wednesday" | "WEDNESDAY" => Ok(Self::Wednesday), + "thursday" | "Thursday" | "THURSDAY" => Ok(Self::Thursday), + "friday" | "Friday" | "FRIDAY" => Ok(Self::Friday), + "saturday" | "Saturday" | "SATURDAY" => Ok(Self::Saturday), + "sunday" | "Sunday" | "SUNDAY" => Ok(Self::Sunday), + _ => Err(ParsingErrors::ParseWeekdayError), + } + } +} + +impl Add for Weekday { + type Output = Self; + fn add(self, rhs: Self) -> Self { + Self::from(u8::from(self) + u8::from(rhs)) + } +} + +impl Sub for Weekday { + type Output = Duration; + fn sub(self, rhs: Self) -> Self::Output { + // We can safely cast the weekdays as u8 into i8 because the maximum value is 6, and the max value of a i8 is 127. + let self_i8 = u8::from(self) as i8; + let mut rhs_i8 = u8::from(rhs) as i8; + if rhs_i8 - self_i8 < 0 { + rhs_i8 += 7; + } + i64::from(rhs_i8 - self_i8) * Unit::Day + } +} + +impl Add for Weekday { + type Output = Self; + fn add(self, rhs: u8) -> Self { + Self::from(u8::from(self) + rhs) + } +} + +impl Sub for Weekday { + type Output = Self; + fn sub(self, rhs: u8) -> Self { + // We can safely cast the weekdays as u8 into i8 because the maximum value is 6, and the max value of a i8 is 127. + Self::from(u8::from(self) as i8 - rhs as i8) + } +} + +impl AddAssign for Weekday { + fn add_assign(&mut self, rhs: u8) { + *self = *self + rhs; + } +} + +impl SubAssign for Weekday { + fn sub_assign(&mut self, rhs: u8) { + *self = *self - rhs; + } +} + +impl fmt::Display for Weekday { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +#[test] +fn test_wrapping() { + assert_eq!(Weekday::default(), Weekday::Monday); + assert_eq!(Weekday::from(Weekday::MAX), Weekday::Monday); + + let monday = Weekday::default(); + for i in 0..24 { + // Test wrapping + let add = monday + i; + let expected: Weekday = i.rem_euclid(Weekday::MAX.into()).into(); + assert_eq!( + add, expected, + "expecting {:?} got {:?} for {:02} conversion", + expected, add, i + ); + // Test FromStr + } +} diff --git a/tests/epoch.rs b/tests/epoch.rs index 1a7d51d..d5b6a3c 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -2,9 +2,10 @@ extern crate core; use hifitime::{ - is_gregorian_valid, Duration, Epoch, TimeScale, TimeUnits, Unit, BDT_REF_EPOCH, - DAYS_GPS_TAI_OFFSET, GPST_REF_EPOCH, GST_REF_EPOCH, J1900_OFFSET, J2000_OFFSET, MJD_OFFSET, - SECONDS_BDT_TAI_OFFSET, SECONDS_GPS_TAI_OFFSET, SECONDS_GST_TAI_OFFSET, SECONDS_PER_DAY, + is_gregorian_valid, Duration, Epoch, TimeScale, TimeUnits, Unit, Weekday, BDT_REF_EPOCH, + DAYS_GPS_TAI_OFFSET, GPST_REF_EPOCH, GST_REF_EPOCH, J1900_OFFSET, J1900_REF_EPOCH, + J2000_OFFSET, MJD_OFFSET, SECONDS_BDT_TAI_OFFSET, SECONDS_GPS_TAI_OFFSET, + SECONDS_GST_TAI_OFFSET, SECONDS_PER_DAY, }; #[cfg(feature = "std")] @@ -1015,12 +1016,17 @@ fn test_leap_seconds_iers() { let epoch_from_utc_greg = Epoch::from_gregorian_tai_hms(1971, 12, 31, 23, 59, 59); // Just after it. let epoch_from_utc_greg1 = Epoch::from_gregorian_tai_hms(1972, 1, 1, 0, 0, 0); + assert_eq!(epoch_from_utc_greg1.day_of_year(), 0.0); assert_eq!(epoch_from_utc_greg.leap_seconds_iers(), 0); // The first leap second is special; it adds 10 seconds. assert_eq!(epoch_from_utc_greg1.leap_seconds_iers(), 10); // Just before the second leap second. let epoch_from_utc_greg = Epoch::from_gregorian_tai_hms(1972, 6, 30, 23, 59, 59); + assert_eq!( + epoch_from_utc_greg.duration_in_year(), + (31 + 29 + 31 + 30 + 31 + 30) * Unit::Day - Unit::Second + ); // Just after it. let epoch_from_utc_greg1 = Epoch::from_gregorian_tai_hms(1972, 7, 1, 0, 0, 0); assert_eq!(epoch_from_utc_greg.leap_seconds_iers(), 10); @@ -1337,3 +1343,210 @@ fn test_minmax() { assert_eq!(e1, e1.max(e0)); assert_eq!(e1, e0.max(e1)); } + +#[test] +fn test_weekday() { + // Ensure that even when we switch the time scale of the underlying Epoch, we're still correctly computing the weekday. + let permutate_time_scale = |e: Epoch, expect: Weekday| { + for new_time_scale in [ + TimeScale::BDT, + TimeScale::ET, + TimeScale::GPST, + TimeScale::GST, + TimeScale::TAI, + TimeScale::TDB, + TimeScale::TT, + TimeScale::UTC, + ] { + let e_ts = e.in_time_scale(new_time_scale); + assert_eq!(e_ts.weekday(), expect, "error with {new_time_scale}"); + } + }; + // J1900 was a monday + let j1900 = J1900_REF_EPOCH; + assert_eq!(j1900.weekday(), Weekday::Monday); + permutate_time_scale(j1900, Weekday::Monday); + // 1 nanosec into TAI: still a monday + let j1900_1ns = Epoch::from_gregorian_tai(1900, 01, 01, 0, 0, 0, 1); + assert_eq!(j1900_1ns.weekday(), Weekday::Monday); + permutate_time_scale(j1900_1ns, Weekday::Monday); + // some portion of that day: still a mon day + let j1900_10h_123_ns = Epoch::from_gregorian_tai(1900, 01, 01, 10, 00, 00, 123); + assert_eq!(j1900_10h_123_ns.weekday(), Weekday::Monday); + permutate_time_scale(j1900_10h_123_ns, Weekday::Monday); + // Day +1: tuesday + let j1901 = j1900 + Duration::from_days(1.0); + assert_eq!(j1901.weekday(), Weekday::Tuesday); + permutate_time_scale(j1901, Weekday::Tuesday); + // 1 ns into tuesday, still a tuesday + let j1901 = j1901 + Duration::from_nanoseconds(1.0); + assert_eq!(j1901.weekday(), Weekday::Tuesday); + permutate_time_scale(j1901, Weekday::Tuesday); + // 6 days into TAI was a sunday + let e = j1900 + Duration::from_days(6.0); + assert_eq!(e.weekday(), Weekday::Sunday); + permutate_time_scale(e, Weekday::Sunday); + // 6 days + some tiny offset, still a sunday + let e = e + Duration::from_nanoseconds(10000.0); + assert_eq!(e.weekday(), Weekday::Sunday); + permutate_time_scale(e, Weekday::Sunday); + // 7 days into TAI: back to a monday + let e = j1900 + Duration::from_days(7.0); + assert_eq!(e.weekday(), Weekday::Monday); + permutate_time_scale(e, Weekday::Monday); + // 2022/12/01 was a thursday + let epoch = Epoch::from_gregorian_utc_at_midnight(2022, 12, 01); + assert_eq!(epoch.weekday_utc(), Weekday::Thursday); + permutate_time_scale(epoch, Weekday::Thursday); + // 2022/11/28 was a monday + let epoch = Epoch::from_gregorian_utc_at_midnight(2022, 11, 28); + assert_eq!(epoch.weekday_utc(), Weekday::Monday); + permutate_time_scale(epoch, Weekday::Monday); + // 1988/01/02 was a Saturday + let epoch = Epoch::from_gregorian_utc_at_midnight(1988, 1, 2); + assert_eq!(epoch.weekday_utc(), Weekday::Saturday); + permutate_time_scale(epoch, Weekday::Saturday); +} + +#[test] +fn test_get_time() { + let epoch = Epoch::from_gregorian_utc(2022, 12, 01, 10, 11, 12, 13); + assert_eq!(epoch.hours(), 10); + assert_eq!(epoch.minutes(), 11); + assert_eq!(epoch.seconds(), 12); + assert_eq!(epoch.milliseconds(), 0); + assert_eq!(epoch.microseconds(), 0); + assert_eq!(epoch.nanoseconds(), 13); + + let epoch_midnight = epoch.with_hms(0, 0, 0); + assert_eq!( + epoch_midnight, + Epoch::from_gregorian_utc_at_midnight(2022, 12, 01) + 13 * Unit::Nanosecond + ); + + let epoch_midnight = epoch.with_hms_strict(0, 0, 0); + assert_eq!( + epoch_midnight, + Epoch::from_gregorian_utc_at_midnight(2022, 12, 01) + ); +} + +#[test] +fn test_start_of_week() { + // 2022/12/01 + some offset, was a thursday + let epoch = Epoch::from_gregorian_utc(2022, 12, 01, 10, 11, 12, 13); + assert_eq!(epoch.weekday_utc(), Weekday::Thursday); + // 2022/11/27 was the related sunday / start of week + assert_eq!( + epoch.previous_weekday_at_midnight(Weekday::Sunday), + Epoch::from_gregorian_utc_at_midnight(2022, 11, 27) + ); + assert_eq!( + epoch + .previous_weekday_at_midnight(Weekday::Sunday) + .weekday_utc(), + Weekday::Sunday + ); + + let epoch = Epoch::from_gregorian_utc(2022, 09, 15, 01, 01, 01, 01); + assert_eq!(epoch.weekday_utc(), Weekday::Thursday); + assert_eq!( + epoch.previous_weekday_at_midnight(Weekday::Sunday), + Epoch::from_gregorian_utc_at_midnight(2022, 09, 11) + ); + assert_eq!( + epoch + .previous_weekday_at_midnight(Weekday::Sunday) + .weekday_utc(), + Weekday::Sunday + ); +} + +#[test] +fn test_time_of_week() { + // GPST + // https://www.labsat.co.uk/index.php/en/gps-time-calculator + // 01/12/2022 00:00:00 <=> (2238, 345_618_000_000_000) + // 2238 weeks since 1980 + 345_600_000_000_000 ns since previous Sunday + // + 18_000_000_000 ns for elapsed leap seconds + let epoch = Epoch::from_time_of_week(2238, 345_618_000_000_000, TimeScale::GPST); + assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 01, 00, 00, 00, 00)); + assert_eq!(epoch.to_time_of_week(), (2238, 345_618_000_000_000)); + + let epoch_utc = epoch.in_time_scale(TimeScale::TT); + let (utc_wk, utc_tow) = epoch_utc.to_time_of_week(); + assert_eq!( + Epoch::from_time_of_week(utc_wk, utc_tow, TimeScale::TT), + epoch_utc + ); + + // 06/01/1980 01:00:00 = 1H into GPST <=> (0, 3_618_000_000_000) + let epoch = Epoch::from_time_of_week(0, 3_618_000_000_000, TimeScale::GPST); + assert_eq!(epoch.to_gregorian_utc(), (1980, 01, 06, 01, 00, 0 + 18, 00)); + assert_eq!(epoch.to_time_of_week(), (0, 3_618_000_000_000)); + + // 01/01/1981 01:00:00 = 51W + 1 hour into GPS epoch <=> 51, 349_218_000_000_000 + let epoch = Epoch::from_time_of_week(51, 349_218_000_000_000, TimeScale::GPST); + assert_eq!(epoch.to_gregorian_utc(), (1981, 01, 01, 01, 00, 18, 00)); + assert_eq!(epoch.to_time_of_week(), (51, 349_218_000_000_000)); + + // 06/25/1980 13:07:19 = 24W + 13:07:19 into GPS epoch <=> 24, 306_457_000_000_000 + let epoch = Epoch::from_time_of_week(24, 306_457_000_000_000, TimeScale::GPST); + assert_eq!( + epoch.to_gregorian_utc(), + (1980, 06, 25, 13, 07, 18 + 19, 00) + ); + assert_eq!(epoch.to_time_of_week(), (24, 306_457_000_000_000)); + + // add 1 nanos + let epoch = Epoch::from_time_of_week(2238, 345_618_000_000_001, TimeScale::GPST); + assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 01, 00, 00, 00, 01)); + + // add 1/2 day + let epoch = Epoch::from_time_of_week(2238, 475_218_000_000_000, TimeScale::GPST); + assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 02, 12, 00, 00, 00)); + + // add 1/2 day + 3 hours + 27 min + 19s +10ns + let epoch = Epoch::from_time_of_week(2238, 487_657_000_000_010, TimeScale::GPST); + assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 02, 15, 27, 19, 10)); + + // 1H into Galileo timescale + let epoch = Epoch::from_time_of_week(0, 3_600_000_000_000, TimeScale::GST); + let expected_tai = TimeScale::GST.ref_epoch() + Duration::from_hours(1.0); + assert_eq!(epoch.to_gregorian_utc(), expected_tai.to_gregorian_utc()); + assert_eq!(epoch.to_time_of_week(), (0, 3_600_000_000_000)); + + // 1W + 128H into Galileo timescale + let epoch = Epoch::from_time_of_week(1, 128 * 3600 * 1_000_000_000, TimeScale::GST); + let expected_tai = + TimeScale::GST.ref_epoch() + Duration::from_days(7.0) + Duration::from_hours(128.0); + assert_eq!(epoch.to_gregorian_utc(), expected_tai.to_gregorian_utc()); + assert_eq!(epoch.to_time_of_week(), (1, 128 * 3600 * 1_000_000_000)); + + // 13.5H into BeiDou timescale + let epoch = Epoch::from_time_of_week( + 0, + 13 * 3600 * 1_000_000_000 + 1800 * 1_000_000_000, + TimeScale::BDT, + ); + let expected_tai = TimeScale::BDT.ref_epoch() + Duration::from_hours(13.5); + assert_eq!(epoch.to_gregorian_utc(), expected_tai.to_gregorian_utc()); + assert_eq!( + epoch.to_time_of_week(), + (0, 13 * 3600 * 1_000_000_000 + 1800 * 1_000_000_000) + ); + + // 10W + 36.25 H into BeiDou Timescale + let epoch = Epoch::from_time_of_week( + 10, + 36 * 3600 * 1_000_000_000 + 900 * 1_000_000_000, + TimeScale::BDT, + ); + let expected_tai = + TimeScale::BDT.ref_epoch() + Duration::from_days(70.0) + Duration::from_hours(36.25); + assert_eq!(epoch.to_gregorian_utc(), expected_tai.to_gregorian_utc()); + assert_eq!( + epoch.to_time_of_week(), + (10, 36 * 3600 * 1_000_000_000 + 900 * 1_000_000_000) + ); +} diff --git a/tests/weekday.rs b/tests/weekday.rs new file mode 100644 index 0000000..8efa4b5 --- /dev/null +++ b/tests/weekday.rs @@ -0,0 +1,259 @@ +#[cfg(feature = "std")] +extern crate core; + +use core::str::FromStr; + +use hifitime::{Duration, Epoch, TimeUnits, Unit, Weekday}; + +#[test] +fn test_basic_ops() { + assert_eq!(Weekday::default(), Weekday::Monday); + + let monday = Weekday::default(); + + assert_eq!(monday - 1, Weekday::Sunday); + assert_eq!(monday - 2, Weekday::Saturday); + assert_eq!(monday - 3, Weekday::Friday); + assert_eq!(monday - 4, Weekday::Thursday); + assert_eq!(monday - 5, Weekday::Wednesday); + assert_eq!(monday - 6, Weekday::Tuesday); + assert_eq!(monday - 7, monday); + assert_eq!(monday - 8, Weekday::Sunday); + assert_eq!(monday - 9, Weekday::Saturday); + assert_eq!(monday - 13, Weekday::Tuesday); + assert_eq!(monday - 14, monday); + assert_eq!(monday - 15, Weekday::Sunday); + + let i: i8 = -1; + let weekday: Weekday = i.into(); + assert_eq!(weekday, Weekday::Sunday); + let i: i8 = -2; + let weekday: Weekday = i.into(); + assert_eq!(weekday, Weekday::Saturday); +} + +#[test] +fn test_weekday_differences() { + let monday = Weekday::Monday; + + for day_num in 0..15_u8 { + let day = Weekday::from(day_num); + let neg_delta: Duration = monday - day; + let pos_delta: Duration = day - monday; + // Check reciprocity + if day_num % 7 == 0 { + assert_eq!(pos_delta + neg_delta, Duration::ZERO); + } else { + assert_eq!(pos_delta + neg_delta, 7 * Unit::Day); + } + // Check actual value + assert_eq!(neg_delta, i64::from(day_num % 7) * Unit::Day); + } + + // Start in the middle of the week + for day_num in 0..15_u8 { + let day = Weekday::from(day_num); + let neg_delta: Duration = Weekday::Wednesday - day; + let pos_delta: Duration = day - Weekday::Wednesday; + // Check reciprocity + if day_num % 7 == 2 { + assert_eq!(pos_delta + neg_delta, Duration::ZERO); + } else { + assert_eq!(pos_delta + neg_delta, 7 * Unit::Day); + } + // Check actual value + if day_num % 7 <= 2 { + assert_eq!(pos_delta, i64::from(2 - day_num % 7) * Unit::Day); + } else { + assert_eq!(neg_delta, i64::from(day_num % 7 - 2) * Unit::Day); + } + // Test FromStr + assert_eq!(Weekday::from_str(&format!("{day}")).unwrap(), day); + } +} + +#[test] +fn test_next() { + let epoch = Epoch::from_gregorian_utc_at_midnight(1988, 1, 2); + assert_eq!(epoch.weekday_utc(), Weekday::Saturday); + assert_eq!( + epoch.next(Weekday::Sunday), + Epoch::from_gregorian_utc_at_midnight(1988, 1, 3) + ); + assert_eq!( + epoch.next(Weekday::Monday), + Epoch::from_gregorian_utc_at_midnight(1988, 1, 4) + ); + assert_eq!( + epoch.next(Weekday::Tuesday), + Epoch::from_gregorian_utc_at_midnight(1988, 1, 5) + ); + assert_eq!( + epoch.next(Weekday::Wednesday), + Epoch::from_gregorian_utc_at_midnight(1988, 1, 6) + ); + assert_eq!( + epoch.next(Weekday::Thursday), + Epoch::from_gregorian_utc_at_midnight(1988, 1, 7) + ); + assert_eq!( + epoch.next(Weekday::Friday), + Epoch::from_gregorian_utc_at_midnight(1988, 1, 8) + ); + assert_eq!( + epoch.next(Weekday::Saturday), + Epoch::from_gregorian_utc_at_midnight(1988, 1, 9) + ); + + // Try with non zero subseconds + let epoch = epoch + 159.microseconds(); + assert_eq!( + epoch.next_weekday_at_midnight(Weekday::Sunday), + Epoch::from_gregorian_utc_at_midnight(1988, 1, 3) + ); + assert_eq!( + epoch.next_weekday_at_noon(Weekday::Sunday), + Epoch::from_gregorian_utc_at_noon(1988, 1, 3) + ); + assert_eq!( + epoch.next_weekday_at_midnight(Weekday::Monday), + Epoch::from_gregorian_utc_at_midnight(1988, 1, 4) + ); + assert_eq!( + epoch.next_weekday_at_noon(Weekday::Monday), + Epoch::from_gregorian_utc_at_noon(1988, 1, 4) + ); + assert_eq!( + epoch.next_weekday_at_midnight(Weekday::Tuesday), + Epoch::from_gregorian_utc_at_midnight(1988, 1, 5) + ); + assert_eq!( + epoch.next_weekday_at_noon(Weekday::Tuesday), + Epoch::from_gregorian_utc_at_noon(1988, 1, 5) + ); + assert_eq!( + epoch.next_weekday_at_midnight(Weekday::Wednesday), + Epoch::from_gregorian_utc_at_midnight(1988, 1, 6) + ); + assert_eq!( + epoch.next_weekday_at_noon(Weekday::Wednesday), + Epoch::from_gregorian_utc_at_noon(1988, 1, 6) + ); + assert_eq!( + epoch.next_weekday_at_midnight(Weekday::Thursday), + Epoch::from_gregorian_utc_at_midnight(1988, 1, 7) + ); + assert_eq!( + epoch.next_weekday_at_noon(Weekday::Thursday), + Epoch::from_gregorian_utc_at_noon(1988, 1, 7) + ); + assert_eq!( + epoch.next_weekday_at_midnight(Weekday::Friday), + Epoch::from_gregorian_utc_at_midnight(1988, 1, 8) + ); + assert_eq!( + epoch.next_weekday_at_noon(Weekday::Friday), + Epoch::from_gregorian_utc_at_noon(1988, 1, 8) + ); + assert_eq!( + epoch.next_weekday_at_midnight(Weekday::Saturday), + Epoch::from_gregorian_utc_at_midnight(1988, 1, 9) + ); + assert_eq!( + epoch.next_weekday_at_noon(Weekday::Saturday), + Epoch::from_gregorian_utc_at_noon(1988, 1, 9) + ); +} + +#[test] +fn test_previous() { + let epoch = Epoch::from_gregorian_utc_at_midnight(1988, 1, 2); + assert_eq!( + epoch.previous(Weekday::Friday), + Epoch::from_gregorian_utc_at_midnight(1988, 1, 1) + ); + assert_eq!( + epoch.previous(Weekday::Thursday), + Epoch::from_gregorian_utc_at_midnight(1987, 12, 31) + ); + assert_eq!( + epoch.previous(Weekday::Wednesday), + Epoch::from_gregorian_utc_at_midnight(1987, 12, 30) + ); + assert_eq!( + epoch.previous(Weekday::Tuesday), + Epoch::from_gregorian_utc_at_midnight(1987, 12, 29) + ); + assert_eq!( + epoch.previous(Weekday::Monday), + Epoch::from_gregorian_utc_at_midnight(1987, 12, 28) + ); + assert_eq!( + epoch.previous(Weekday::Sunday), + Epoch::from_gregorian_utc_at_midnight(1987, 12, 27) + ); + assert_eq!( + epoch.previous(Weekday::Saturday), + Epoch::from_gregorian_utc_at_midnight(1987, 12, 26) + ); + + // Try with non zero subseconds + let epoch = epoch + 159.microseconds(); + assert_eq!( + epoch.previous_weekday_at_midnight(Weekday::Friday), + Epoch::from_gregorian_utc_at_midnight(1988, 1, 1) + ); + assert_eq!( + epoch.previous_weekday_at_midnight(Weekday::Thursday), + Epoch::from_gregorian_utc_at_midnight(1987, 12, 31) + ); + assert_eq!( + epoch.previous_weekday_at_midnight(Weekday::Wednesday), + Epoch::from_gregorian_utc_at_midnight(1987, 12, 30) + ); + assert_eq!( + epoch.previous_weekday_at_midnight(Weekday::Tuesday), + Epoch::from_gregorian_utc_at_midnight(1987, 12, 29) + ); + assert_eq!( + epoch.previous_weekday_at_midnight(Weekday::Monday), + Epoch::from_gregorian_utc_at_midnight(1987, 12, 28) + ); + assert_eq!( + epoch.previous_weekday_at_midnight(Weekday::Sunday), + Epoch::from_gregorian_utc_at_midnight(1987, 12, 27) + ); + assert_eq!( + epoch.previous_weekday_at_midnight(Weekday::Saturday), + Epoch::from_gregorian_utc_at_midnight(1987, 12, 26) + ); + + assert_eq!( + epoch.previous_weekday_at_noon(Weekday::Friday), + Epoch::from_gregorian_utc_at_noon(1988, 1, 1) + ); + assert_eq!( + epoch.previous_weekday_at_noon(Weekday::Thursday), + Epoch::from_gregorian_utc_at_noon(1987, 12, 31) + ); + assert_eq!( + epoch.previous_weekday_at_noon(Weekday::Wednesday), + Epoch::from_gregorian_utc_at_noon(1987, 12, 30) + ); + assert_eq!( + epoch.previous_weekday_at_noon(Weekday::Tuesday), + Epoch::from_gregorian_utc_at_noon(1987, 12, 29) + ); + assert_eq!( + epoch.previous_weekday_at_noon(Weekday::Monday), + Epoch::from_gregorian_utc_at_noon(1987, 12, 28) + ); + assert_eq!( + epoch.previous_weekday_at_noon(Weekday::Sunday), + Epoch::from_gregorian_utc_at_noon(1987, 12, 27) + ); + assert_eq!( + epoch.previous_weekday_at_noon(Weekday::Saturday), + Epoch::from_gregorian_utc_at_noon(1987, 12, 26) + ); +}