From 0b3b40daf8d540188c5c15b4bed6b7a74904a4d2 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Fri, 2 Dec 2022 10:04:03 +0100 Subject: [PATCH 01/22] timeofweek: introduce time of week construction and conversion * (wk, tow) is usually how an epoch is expressed by a GNSS receiver, usually in a GNSS timescale. Wk is a rolling counter, and tow is the amount of seconds since Sunday midnight of that week. `tow` is usually expressed in [s] or [ms] by receivers, we use [ns] here, to maintain crate consistency and never be limited. Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 60 +++++++++++++++++++++++++++++++++++++++++++++++- src/timescale.rs | 9 ++++++++ tests/epoch.rs | 39 +++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/src/epoch.rs b/src/epoch.rs index 79cc681..1c842d5 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -13,7 +13,7 @@ use crate::parser::Token; use crate::{ Errors, TimeScale, BDT_REF_EPOCH, DAYS_PER_YEAR_NLD, ET_EPOCH_S, GPST_REF_EPOCH, GST_REF_EPOCH, J1900_OFFSET, J2000_TO_J1900_DURATION, MJD_OFFSET, NANOSECONDS_PER_MICROSECOND, - NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_SECOND_U32, UNIX_REF_EPOCH, + SECONDS_PER_DAY, NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_SECOND_U32, UNIX_REF_EPOCH, }; use core::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}; use core::fmt; @@ -2225,6 +2225,64 @@ impl Epoch { me.time_scale = new_time_scale; me } + + /// Builds an Epoch from given `wk` week counter into the desired Time scale. + /// `ns` is the amount of nanoseconds into that week, starting on Sunday midnight of that week. + #[cfg(feature = "python")] + #[staticmethod] + pub fn from_timeofweek(wk: u32, ns: u64, ts: TimeScale) -> Self { + let week = Duration::from_seconds(wk as f64 * SECONDS_PER_DAY * 7.0); + Self::from_duration(week + (ns as f64) * Unit::Nanosecond, ts) + } + + /// Converts to "time of week" (`wk, `tow`), + /// which is usually how GNSS receivers describe a timestamp. + /// `wk` is a rolling week conter into that time scale, + /// `tow` is the number of seconds since closest Sunday midnight into that week. + pub fn to_timeofweek(&self) -> (u32, u64) { + // fractionnal days in this time scale + let days = match self.time_scale { + TimeScale::GPST => self.to_gpst_days(), + TimeScale::GST => self.to_gst_days(), + TimeScale::BDT => self.to_bdt_days(), + TimeScale::TT => self.to_tt_days(), + TimeScale::TAI => self.to_tai_days(), + TimeScale::UTC => self.to_utc_days(), + TimeScale::TDB => self.to_tdb_seconds() / SECONDS_PER_DAY, + TimeScale::ET => self.to_et_seconds() / SECONDS_PER_DAY, + }; + let wk = (days / 7.0) as u32; + let residuals = (Duration::from_f64(days + 7.0, Unit::Day) - Duration::from_f64(days, Unit::Day)) + .truncated_nanoseconds(); + (wk, residuals as u64)// express residuals in ns + } + + /// Returns weekday counter in TAI timescale, + /// 0: Monday,..., 6: Sunday + pub fn weekday_tai(&self) -> u8 { + // we're helped here, because J1900 was a monday :) + (self.to_tai_days() as u64).rem_euclid(7) as u8 + } + +/* needs timescale starting point offset + /// Returns weekday counter in GST timescale, + /// 0: Monday,..., 6: Sunday + pub fn weekday_gst(&self) -> u8 { + self.in_time_scale(TimeScale::TAI).weekday_tai() + } + + pub fn weekday_gpst(&self) -> u8 { + self.in_time_scale(TimeScale::GPST).weekday_tai() + } + + pub fn weekday_bdt(&self) -> u8 { + self.in_time_scale(TimeScale::BDT).weekday_tai() + } + + pub fn weekday_utc(&self) -> u8 { + self.in_time_scale(TimeScale::UTC).weekday_tai() + } +*/ // Python helpers diff --git a/src/timescale.rs b/src/timescale.rs index 5583998..2d83f39 100644 --- a/src/timescale.rs +++ b/src/timescale.rs @@ -19,6 +19,15 @@ use core::str::FromStr; use crate::{Duration, Epoch, Errors, ParsingErrors, SECONDS_PER_DAY}; +/* +/// TAI reference Monday: first Monday encountered in the TAI timescale, +/// from our reference point (or Epoch), which is defined as J1900 in TAI. +/// This is used as a reference point for weekday determination +pub const TAI_REF_MONDAY: Epoch = Epoch { + centuries: 0, // funny enough, Day 1 in 1900 tai was a monday + nanoseconds: 0, +}; */ + /// 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 { centuries: 0, diff --git a/tests/epoch.rs b/tests/epoch.rs index 1a7d51d..dcdd8d7 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -1337,3 +1337,42 @@ fn test_minmax() { assert_eq!(e1, e1.max(e0)); assert_eq!(e1, e0.max(e1)); } + +#[test] +fn test_weekday() { + // J1900 was a monday + let j1900 = Epoch::from_gregorian_tai_at_midnight(1900, 01, 01); + assert_eq!(j1900.weekday_tai(), 0); + // 1 nanosec into TAI: still a monday + let j1900_fractionnal = Epoch::from_gregorian_tai(1900, 01, 01, 0, 0, 0, 1); + assert_eq!(j1900.weekday_tai(), 0); + // some portion of that day: still a mon day + let j1900_fractionnal = Epoch::from_gregorian_tai(1900, 01, 01, 10, 00, 00, 123); + assert_eq!(j1900.weekday_tai(), 0); + // Day +1: tuesday + let j1901 = j1900 + Duration::from_days(1.0); + assert_eq!(j1901.weekday_tai(), 1); + // 1 ns into tuesday, still a tuesday + let j1901 = j1901 + Duration::from_nanoseconds(1.0); + assert_eq!(j1901.weekday_tai(), 1); + // 6 days into TAI was a sunday + let e = j1900 + Duration::from_days(6.0); + assert_eq!(e.weekday_tai(), 6); + // 6 days + some residuales, still a sunday + let e = e + Duration::from_nanoseconds(10000.0); + assert_eq!(e.weekday_tai(), 6); + // 7 days into TAI: back to a monday + let e = j1900 + Duration::from_days(7.0); + assert_eq!(e.weekday_tai(), 0); +} + +#[test] +fn test_gnss_timeofweek() { + // GPST + // https://www.labsat.co.uk/index.php/en/gps-time-calculator + // 01/12/2022 00:00:00 => (2238, 345_618_000_000_000) + let epoch = Epoch::from_timeofweek(2238, 345_618_000_000_000, TimeScale::GPST); + assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 01, 00, 00, 00, 00)); + // test reserve op + assert_eq!(epoch.to_timeofweek(), (2238, 345_618_000_000_000)); +} From dd4b7bdd535910cb88d71b80672e7c03275d08c0 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Fri, 2 Dec 2022 14:07:21 +0100 Subject: [PATCH 02/22] epoch.rs: introduce start of week Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 43 ++++++++++++++++++++++++++----------------- tests/epoch.rs | 19 +++++++++++++++++-- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 1c842d5..09ac6de 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -2261,29 +2261,38 @@ impl Epoch { /// 0: Monday,..., 6: Sunday pub fn weekday_tai(&self) -> u8 { // we're helped here, because J1900 was a monday :) - (self.to_tai_days() as u64).rem_euclid(7) as u8 + (self.to_tai_days() as u64).rem_euclid(7) as u8 } -/* needs timescale starting point offset - /// Returns weekday counter in GST timescale, - /// 0: Monday,..., 6: Sunday - pub fn weekday_gst(&self) -> u8 { - self.in_time_scale(TimeScale::TAI).weekday_tai() - } - - pub fn weekday_gpst(&self) -> u8 { - self.in_time_scale(TimeScale::GPST).weekday_tai() - } - - pub fn weekday_bdt(&self) -> u8 { - self.in_time_scale(TimeScale::BDT).weekday_tai() +/* + /// Returns weekday counter in current time scale + pub fn weekday(&self) -> u8 { + match self.time_scale { + TimeScale::TAI => self.weekday_tai(), + TimeScale::UTC => self.weekday_utc(), + TimeScale::TT => self.weekday_tt(), + TimeScale::TDB => self.weekday_tdb(), + TimeScale::ET => self.weekday_et(), + TimeScale::GPST => self.weekday_gpst(), + TimeScale::GST => self.weekday_gst(), + TimeScale::BDT => self.weekday_bdt(), + } } - +*/ + /// Returns weekday counter in UTC timescale, + /// 0: Monday,..., 6: Sunday pub fn weekday_utc(&self) -> u8 { - self.in_time_scale(TimeScale::UTC).weekday_tai() + Self::from_tai_duration(self.duration_since_j1900_tai - self.leap_seconds(true).unwrap_or(0.0) * Unit::Second) + .weekday_tai() } -*/ + /// Returns closest UTC Sunday midnight (ie., start of week) from self + pub fn closest_utc_start_of_week(&self) -> Self { + let days = Duration::from_days(self.weekday_utc() as f64); + let (y, m, d, _, _, _, _) = (*self - days).to_gregorian_utc(); + Self::from_gregorian_utc_at_midnight(y, m, d) + } + // Python helpers #[cfg(feature = "python")] diff --git a/tests/epoch.rs b/tests/epoch.rs index dcdd8d7..8265ffd 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -1347,8 +1347,8 @@ fn test_weekday() { let j1900_fractionnal = Epoch::from_gregorian_tai(1900, 01, 01, 0, 0, 0, 1); assert_eq!(j1900.weekday_tai(), 0); // some portion of that day: still a mon day - let j1900_fractionnal = Epoch::from_gregorian_tai(1900, 01, 01, 10, 00, 00, 123); - assert_eq!(j1900.weekday_tai(), 0); + let j1900_10h_123_ns = Epoch::from_gregorian_tai(1900, 01, 01, 10, 00, 00, 123); + assert_eq!(j1900_10h_123_ns.weekday_tai(), 0); // Day +1: tuesday let j1901 = j1900 + Duration::from_days(1.0); assert_eq!(j1901.weekday_tai(), 1); @@ -1364,6 +1364,21 @@ fn test_weekday() { // 7 days into TAI: back to a monday let e = j1900 + Duration::from_days(7.0); assert_eq!(e.weekday_tai(), 0); + // 2022/12/01 was a thursday + let epoch = Epoch::from_gregorian_utc(2022, 12, 01, 00, 00, 00, 0); + assert_eq!(epoch.weekday_utc(), 3); + // 2022/11/28 was a monday + let epoch = Epoch::from_gregorian_utc(2022, 11, 28, 00, 00, 00, 0); + assert_eq!(epoch.weekday_utc(), 0); +} + +#[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); + // 2022/11/28 was the related start of week + assert_eq!(epoch.closest_utc_start_of_week(), + Epoch::from_gregorian_utc_at_midnight(2022, 11, 28)); } #[test] From ad53c5f8d7d4bdcea76560caacca7779570a407d Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Fri, 2 Dec 2022 15:59:51 +0100 Subject: [PATCH 03/22] timeofweek (utc) and start of week (utc) Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 57 +++++++++++++++++++++++++++--------------------- src/timescale.rs | 9 -------- tests/epoch.rs | 32 ++++++++++++++++++++------- 3 files changed, 56 insertions(+), 42 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 09ac6de..9f2fee0 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -2227,19 +2227,27 @@ impl Epoch { } /// Builds an Epoch from given `wk` week counter into the desired Time scale. - /// `ns` is the amount of nanoseconds into that week, starting on Sunday midnight of that week. + /// `ns` is the amount of nanoseconds into that week from closest Sunday Midnight of that week. + /// This is usually how GNSS receivers describe a GNSS time scale epoch #[cfg(feature = "python")] #[staticmethod] pub fn from_timeofweek(wk: u32, ns: u64, ts: TimeScale) -> Self { let week = Duration::from_seconds(wk as f64 * SECONDS_PER_DAY * 7.0); Self::from_duration(week + (ns as f64) * Unit::Nanosecond, ts) } + + #[cfg(feature = "python")] + #[staticmethod] + pub fn from_timeofweek_utc(wk: u32, ns: u64) -> Self { + let week = Duration::from_seconds(wk as f64 * SECONDS_PER_DAY * 7.0); + Self::from_utc_duration(week + (ns as f64) * Unit::Nanosecond) + } /// Converts to "time of week" (`wk, `tow`), /// which is usually how GNSS receivers describe a timestamp. /// `wk` is a rolling week conter into that time scale, /// `tow` is the number of seconds since closest Sunday midnight into that week. - pub fn to_timeofweek(&self) -> (u32, u64) { + pub fn to_timeofweek_utc(&self) -> (u32, u64) { // fractionnal days in this time scale let days = match self.time_scale { TimeScale::GPST => self.to_gpst_days(), @@ -2251,36 +2259,33 @@ impl Epoch { TimeScale::TDB => self.to_tdb_seconds() / SECONDS_PER_DAY, TimeScale::ET => self.to_et_seconds() / SECONDS_PER_DAY, }; + // wk: rolling week counter into timescale let wk = (days / 7.0) as u32; - let residuals = (Duration::from_f64(days + 7.0, Unit::Day) - Duration::from_f64(days, Unit::Day)) - .truncated_nanoseconds(); - (wk, residuals as u64)// express residuals in ns + // tow: number of nanoseconds between self and closest sunday midnight / start of week + let start_of_week = self.closest_utc_start_of_week(); + let dw = *self - start_of_week; // difference in weekdays [0..6] + let (_, d, h, m, s, ms, us, ns) = dw.decompose(); + let tow = Duration::from_days(d as f64) + + Duration::from_hours(h as f64) + + Duration::from_seconds((m * 60) as f64) + + Duration::from_seconds(s as f64) + + Duration::from_milliseconds(ms as f64) + + Duration::from_microseconds(us as f64) + + Duration::from_nanoseconds(ns as f64); + let (_, mut tow) = tow.to_parts(); + //tow += self.leap_seconds(true).unwrap_or(0.0) as u64 * 1_000_000_000; + (wk, tow) } - + /// Returns weekday counter in TAI timescale, - /// 0: Monday,..., 6: Sunday + /// 0: Monday, ..., 6: Sunday pub fn weekday_tai(&self) -> u8 { // we're helped here, because J1900 was a monday :) (self.to_tai_days() as u64).rem_euclid(7) as u8 } -/* - /// Returns weekday counter in current time scale - pub fn weekday(&self) -> u8 { - match self.time_scale { - TimeScale::TAI => self.weekday_tai(), - TimeScale::UTC => self.weekday_utc(), - TimeScale::TT => self.weekday_tt(), - TimeScale::TDB => self.weekday_tdb(), - TimeScale::ET => self.weekday_et(), - TimeScale::GPST => self.weekday_gpst(), - TimeScale::GST => self.weekday_gst(), - TimeScale::BDT => self.weekday_bdt(), - } - } -*/ /// Returns weekday counter in UTC timescale, - /// 0: Monday,..., 6: Sunday + /// 0: Monday, ..., 6: Sunday pub fn weekday_utc(&self) -> u8 { Self::from_tai_duration(self.duration_since_j1900_tai - self.leap_seconds(true).unwrap_or(0.0) * Unit::Second) .weekday_tai() @@ -2288,8 +2293,10 @@ impl Epoch { /// Returns closest UTC Sunday midnight (ie., start of week) from self pub fn closest_utc_start_of_week(&self) -> Self { - let days = Duration::from_days(self.weekday_utc() as f64); - let (y, m, d, _, _, _, _) = (*self - days).to_gregorian_utc(); + let weekday = self.weekday_utc(); + let days = Duration::from_days(weekday as f64); // duration into that week + // -1: 0 is monday, we want Sunday midnight + let (y, m, d, _, _, _, _) = (*self - days - Duration::from_days(1.0)).to_gregorian_utc(); Self::from_gregorian_utc_at_midnight(y, m, d) } diff --git a/src/timescale.rs b/src/timescale.rs index 2d83f39..5583998 100644 --- a/src/timescale.rs +++ b/src/timescale.rs @@ -19,15 +19,6 @@ use core::str::FromStr; use crate::{Duration, Epoch, Errors, ParsingErrors, SECONDS_PER_DAY}; -/* -/// TAI reference Monday: first Monday encountered in the TAI timescale, -/// from our reference point (or Epoch), which is defined as J1900 in TAI. -/// This is used as a reference point for weekday determination -pub const TAI_REF_MONDAY: Epoch = Epoch { - centuries: 0, // funny enough, Day 1 in 1900 tai was a monday - nanoseconds: 0, -}; */ - /// 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 { centuries: 0, diff --git a/tests/epoch.rs b/tests/epoch.rs index 8265ffd..bf75713 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -1344,8 +1344,8 @@ fn test_weekday() { let j1900 = Epoch::from_gregorian_tai_at_midnight(1900, 01, 01); assert_eq!(j1900.weekday_tai(), 0); // 1 nanosec into TAI: still a monday - let j1900_fractionnal = Epoch::from_gregorian_tai(1900, 01, 01, 0, 0, 0, 1); - assert_eq!(j1900.weekday_tai(), 0); + let j1900_1ns = Epoch::from_gregorian_tai(1900, 01, 01, 0, 0, 0, 1); + assert_eq!(j1900_1ns.weekday_tai(), 0); // 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_tai(), 0); @@ -1376,18 +1376,34 @@ fn test_weekday() { 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); - // 2022/11/28 was the related start of week + // 2022/11/27 was the related sunday / start of week + assert_eq!(epoch.closest_utc_start_of_week(), + Epoch::from_gregorian_utc_at_midnight(2022, 11, 27)); + + let epoch = Epoch::from_gregorian_utc(2022, 09, 15, 01, 01, 01, 01); + assert_eq!(epoch.weekday_utc(), 3); assert_eq!(epoch.closest_utc_start_of_week(), - Epoch::from_gregorian_utc_at_midnight(2022, 11, 28)); + Epoch::from_gregorian_utc_at_midnight(2022, 09, 11)); } #[test] -fn test_gnss_timeofweek() { +fn test_timeofweek() { // GPST // https://www.labsat.co.uk/index.php/en/gps-time-calculator - // 01/12/2022 00:00:00 => (2238, 345_618_000_000_000) + // 01/12/2022 00:00:00 <=> (2238, 345_618_000_000_000) let epoch = Epoch::from_timeofweek(2238, 345_618_000_000_000, TimeScale::GPST); assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 01, 00, 00, 00, 00)); - // test reserve op - assert_eq!(epoch.to_timeofweek(), (2238, 345_618_000_000_000)); + assert_eq!(epoch.to_timeofweek_utc(), (2238, 345_618_000_000_000)); + + // add 1 nanos + let epoch = Epoch::from_timeofweek(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_timeofweek(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_timeofweek(2238, 487_657_000_000_010, TimeScale::GPST); + assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 02, 15, 27, 19, 10)); } From f38b1c54e2bbb593f0165f3aa83da9e87d803be4 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Fri, 2 Dec 2022 16:02:56 +0100 Subject: [PATCH 04/22] epoch.rs: improve doc Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/epoch.rs b/src/epoch.rs index 9f2fee0..82c2878 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -2227,7 +2227,7 @@ impl Epoch { } /// Builds an Epoch from given `wk` week counter into the desired Time scale. - /// `ns` is the amount of nanoseconds into that week from closest Sunday Midnight of that week. + /// `ns` is the amount of nanoseconds into that week from closest Sunday Midnight. /// This is usually how GNSS receivers describe a GNSS time scale epoch #[cfg(feature = "python")] #[staticmethod] From 92534ba4ab79b0f91d725738d707cb9f10de8a00 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Fri, 2 Dec 2022 16:24:27 +0100 Subject: [PATCH 05/22] utc: account for leap seconds Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/epoch.rs b/src/epoch.rs index 82c2878..ae2315b 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -2273,7 +2273,7 @@ impl Epoch { + Duration::from_microseconds(us as f64) + Duration::from_nanoseconds(ns as f64); let (_, mut tow) = tow.to_parts(); - //tow += self.leap_seconds(true).unwrap_or(0.0) as u64 * 1_000_000_000; + tow += self.leap_seconds(true).unwrap_or(0.0) as u64 * 1_000_000_000; (wk, tow) } From 6d42274a3a49d32dbd00145d0e39bd4a0cd3e5c5 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Fri, 2 Dec 2022 16:30:41 +0100 Subject: [PATCH 06/22] cargo fmt Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 30 ++++++++++++++++-------------- tests/epoch.rs | 22 +++++++++++++--------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index ae2315b..a57107b 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -13,7 +13,7 @@ use crate::parser::Token; use crate::{ Errors, TimeScale, BDT_REF_EPOCH, DAYS_PER_YEAR_NLD, ET_EPOCH_S, GPST_REF_EPOCH, GST_REF_EPOCH, J1900_OFFSET, J2000_TO_J1900_DURATION, MJD_OFFSET, NANOSECONDS_PER_MICROSECOND, - SECONDS_PER_DAY, NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_SECOND_U32, UNIX_REF_EPOCH, + NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_SECOND_U32, SECONDS_PER_DAY, UNIX_REF_EPOCH, }; use core::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}; use core::fmt; @@ -2225,20 +2225,20 @@ impl Epoch { me.time_scale = new_time_scale; me } - - /// Builds an Epoch from given `wk` week counter into the desired Time scale. + + /// Builds an Epoch from given `wk` week counter into the desired Time scale. /// `ns` is the amount of nanoseconds into that week from closest Sunday Midnight. /// This is usually how GNSS receivers describe a GNSS time scale epoch #[cfg(feature = "python")] #[staticmethod] - pub fn from_timeofweek(wk: u32, ns: u64, ts: TimeScale) -> Self { + pub fn from_timeofweek(wk: u32, ns: u64, ts: TimeScale) -> Self { let week = Duration::from_seconds(wk as f64 * SECONDS_PER_DAY * 7.0); Self::from_duration(week + (ns as f64) * Unit::Nanosecond, ts) } - + #[cfg(feature = "python")] #[staticmethod] - pub fn from_timeofweek_utc(wk: u32, ns: u64) -> Self { + pub fn from_timeofweek_utc(wk: u32, ns: u64) -> Self { let week = Duration::from_seconds(wk as f64 * SECONDS_PER_DAY * 7.0); Self::from_utc_duration(week + (ns as f64) * Unit::Nanosecond) } @@ -2264,7 +2264,7 @@ impl Epoch { // tow: number of nanoseconds between self and closest sunday midnight / start of week let start_of_week = self.closest_utc_start_of_week(); let dw = *self - start_of_week; // difference in weekdays [0..6] - let (_, d, h, m, s, ms, us, ns) = dw.decompose(); + let (_, d, h, m, s, ms, us, ns) = dw.decompose(); let tow = Duration::from_days(d as f64) + Duration::from_hours(h as f64) + Duration::from_seconds((m * 60) as f64) @@ -2276,30 +2276,32 @@ impl Epoch { tow += self.leap_seconds(true).unwrap_or(0.0) as u64 * 1_000_000_000; (wk, tow) } - + /// Returns weekday counter in TAI timescale, /// 0: Monday, ..., 6: Sunday pub fn weekday_tai(&self) -> u8 { - // we're helped here, because J1900 was a monday :) + // we're helped here, because J1900 was a monday :) (self.to_tai_days() as u64).rem_euclid(7) as u8 } /// Returns weekday counter in UTC timescale, /// 0: Monday, ..., 6: Sunday pub fn weekday_utc(&self) -> u8 { - Self::from_tai_duration(self.duration_since_j1900_tai - self.leap_seconds(true).unwrap_or(0.0) * Unit::Second) - .weekday_tai() + Self::from_tai_duration( + self.duration_since_j1900_tai - self.leap_seconds(true).unwrap_or(0.0) * Unit::Second, + ) + .weekday_tai() } /// Returns closest UTC Sunday midnight (ie., start of week) from self pub fn closest_utc_start_of_week(&self) -> Self { - let weekday = self.weekday_utc(); + let weekday = self.weekday_utc(); let days = Duration::from_days(weekday as f64); // duration into that week - // -1: 0 is monday, we want Sunday midnight + // -1: 0 is monday, we want Sunday midnight let (y, m, d, _, _, _, _) = (*self - days - Duration::from_days(1.0)).to_gregorian_utc(); Self::from_gregorian_utc_at_midnight(y, m, d) } - + // Python helpers #[cfg(feature = "python")] diff --git a/tests/epoch.rs b/tests/epoch.rs index bf75713..86241e6 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -1367,7 +1367,7 @@ fn test_weekday() { // 2022/12/01 was a thursday let epoch = Epoch::from_gregorian_utc(2022, 12, 01, 00, 00, 00, 0); assert_eq!(epoch.weekday_utc(), 3); - // 2022/11/28 was a monday + // 2022/11/28 was a monday let epoch = Epoch::from_gregorian_utc(2022, 11, 28, 00, 00, 00, 0); assert_eq!(epoch.weekday_utc(), 0); } @@ -1377,13 +1377,17 @@ 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); // 2022/11/27 was the related sunday / start of week - assert_eq!(epoch.closest_utc_start_of_week(), - Epoch::from_gregorian_utc_at_midnight(2022, 11, 27)); + assert_eq!( + epoch.closest_utc_start_of_week(), + Epoch::from_gregorian_utc_at_midnight(2022, 11, 27) + ); let epoch = Epoch::from_gregorian_utc(2022, 09, 15, 01, 01, 01, 01); assert_eq!(epoch.weekday_utc(), 3); - assert_eq!(epoch.closest_utc_start_of_week(), - Epoch::from_gregorian_utc_at_midnight(2022, 09, 11)); + assert_eq!( + epoch.closest_utc_start_of_week(), + Epoch::from_gregorian_utc_at_midnight(2022, 09, 11) + ); } #[test] @@ -1394,15 +1398,15 @@ fn test_timeofweek() { let epoch = Epoch::from_timeofweek(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_timeofweek_utc(), (2238, 345_618_000_000_000)); - + // add 1 nanos let epoch = Epoch::from_timeofweek(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 + + // add 1/2 day let epoch = Epoch::from_timeofweek(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_timeofweek(2238, 487_657_000_000_010, TimeScale::GPST); assert_eq!(epoch.to_gregorian_utc(), (2022, 12, 02, 15, 27, 19, 10)); From 97c30e8654478ccb9910f34a66289cf4aa7fb44f Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Fri, 2 Dec 2022 18:57:08 +0100 Subject: [PATCH 07/22] introduce weekday enum Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 51 +++++++++++++++++---------------- src/lib.rs | 5 ++++ src/weekday.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++ tests/epoch.rs | 35 ++++++++++++++--------- 4 files changed, 128 insertions(+), 39 deletions(-) create mode 100644 src/weekday.rs diff --git a/src/epoch.rs b/src/epoch.rs index a57107b..3dc4de6 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::*; @@ -1186,6 +1187,21 @@ impl Epoch { ) } } + + /// Builds an Epoch from given `wk` week counter into the desired Time scale. + /// `ns` is the amount of nanoseconds into that week from closest Sunday Midnight. + /// This is usually how GNSS receivers describe a GNSS time scale epoch + #[must_use] + pub fn from_timeofweek(wk: u32, ns: u64, ts: TimeScale) -> Self { + let week = Duration::from_seconds(wk as f64 * SECONDS_PER_DAY * 7.0); + Self::from_duration(week + (ns as f64) * Unit::Nanosecond, ts) + } + + #[must_use] + pub fn from_timeofweek_utc(wk: u32, ns: u64) -> Self { + let week = Duration::from_seconds(wk as f64 * SECONDS_PER_DAY * 7.0); + Self::from_utc_duration(week + (ns as f64) * Unit::Nanosecond) + } } #[cfg_attr(feature = "python", pymethods)] @@ -2226,23 +2242,6 @@ impl Epoch { me } - /// Builds an Epoch from given `wk` week counter into the desired Time scale. - /// `ns` is the amount of nanoseconds into that week from closest Sunday Midnight. - /// This is usually how GNSS receivers describe a GNSS time scale epoch - #[cfg(feature = "python")] - #[staticmethod] - pub fn from_timeofweek(wk: u32, ns: u64, ts: TimeScale) -> Self { - let week = Duration::from_seconds(wk as f64 * SECONDS_PER_DAY * 7.0); - Self::from_duration(week + (ns as f64) * Unit::Nanosecond, ts) - } - - #[cfg(feature = "python")] - #[staticmethod] - pub fn from_timeofweek_utc(wk: u32, ns: u64) -> Self { - let week = Duration::from_seconds(wk as f64 * SECONDS_PER_DAY * 7.0); - Self::from_utc_duration(week + (ns as f64) * Unit::Nanosecond) - } - /// Converts to "time of week" (`wk, `tow`), /// which is usually how GNSS receivers describe a timestamp. /// `wk` is a rolling week conter into that time scale, @@ -2256,11 +2255,11 @@ impl Epoch { TimeScale::TT => self.to_tt_days(), TimeScale::TAI => self.to_tai_days(), TimeScale::UTC => self.to_utc_days(), - TimeScale::TDB => self.to_tdb_seconds() / SECONDS_PER_DAY, - TimeScale::ET => self.to_et_seconds() / SECONDS_PER_DAY, + TimeScale::TDB => self.to_tdb_duration().to_unit(Unit::Day), + TimeScale::ET => self.to_et_duration().to_unit(Unit::Day), }; // wk: rolling week counter into timescale - let wk = (days / 7.0) as u32; + let wk = (days / 7.0).floor() as u32; // tow: number of nanoseconds between self and closest sunday midnight / start of week let start_of_week = self.closest_utc_start_of_week(); let dw = *self - start_of_week; // difference in weekdays [0..6] @@ -2279,14 +2278,14 @@ impl Epoch { /// Returns weekday counter in TAI timescale, /// 0: Monday, ..., 6: Sunday - pub fn weekday_tai(&self) -> u8 { - // we're helped here, because J1900 was a monday :) - (self.to_tai_days() as u64).rem_euclid(7) as u8 + pub fn weekday_tai(&self) -> Weekday { + // J1900 was a monday :) + ((self.to_tai_days() as u64).rem_euclid(7) as u8).into() } /// Returns weekday counter in UTC timescale, /// 0: Monday, ..., 6: Sunday - pub fn weekday_utc(&self) -> u8 { + pub fn weekday_utc(&self) -> Weekday { Self::from_tai_duration( self.duration_since_j1900_tai - self.leap_seconds(true).unwrap_or(0.0) * Unit::Second, ) @@ -2296,8 +2295,8 @@ impl Epoch { /// Returns closest UTC Sunday midnight (ie., start of week) from self pub fn closest_utc_start_of_week(&self) -> Self { let weekday = self.weekday_utc(); - let days = Duration::from_days(weekday as f64); // duration into that week - // -1: 0 is monday, we want Sunday midnight + let days = Duration::from_days((weekday as u8).into()); // duration into that week + // -1: 0 is monday, we want Sunday midnight let (y, m, d, _, _, _, _) = (*self - days - Duration::from_days(1.0)).to_gregorian_utc(); Self::from_gregorian_utc_at_midnight(y, m, d) } diff --git a/src/lib.rs b/src/lib.rs index 4db2407..8096772 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,6 +78,9 @@ pub use timeunits::*; mod timeseries; pub use timeseries::*; +mod weekday; +pub use weekday::*; + /// This module defines all of the deprecated methods. mod deprecated; @@ -132,6 +135,8 @@ pub enum ParsingErrors { UnknownFormat, UnknownOrMissingUnit, UnsupportedTimeSystem, + /// Non recognized Weekday description + ParseWeekdayError, } impl fmt::Display for Errors { diff --git a/src/weekday.rs b/src/weekday.rs new file mode 100644 index 0000000..40f9b00 --- /dev/null +++ b/src/weekday.rs @@ -0,0 +1,76 @@ +/* + * 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::ParsingErrors; +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)] +#[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 From for Weekday { + fn from(u: u8) -> Self { + match u { + 0 => Self::Monday, + 1 => Self::Tuesday, + 2 => Self::Wednesday, + 3 => Self::Thursday, + 4 => Self::Friday, + 5 => Self::Saturday, + _ => Self::Sunday, + } + } +} + +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 { + let val = s.trim().to_lowercase(); + match val.as_str() { + "monday" => Ok(Self::Monday), + "tuesday" => Ok(Self::Tuesday), + "wednesday" => Ok(Self::Wednesday), + "thursday" => Ok(Self::Thursday), + "friday" => Ok(Self::Friday), + "saturday" => Ok(Self::Saturday), + "sunday" => Ok(Self::Sunday), + _ => Err(ParsingErrors::ParseWeekdayError), + } + } +} diff --git a/tests/epoch.rs b/tests/epoch.rs index 86241e6..681395a 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -2,7 +2,7 @@ extern crate core; use hifitime::{ - is_gregorian_valid, Duration, Epoch, TimeScale, TimeUnits, Unit, BDT_REF_EPOCH, + is_gregorian_valid, Duration, Epoch, TimeScale, TimeUnits, Unit, Weekday, 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, }; @@ -1342,52 +1342,61 @@ fn test_minmax() { fn test_weekday() { // J1900 was a monday let j1900 = Epoch::from_gregorian_tai_at_midnight(1900, 01, 01); - assert_eq!(j1900.weekday_tai(), 0); + assert_eq!(j1900.weekday_tai(), 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_tai(), 0); + assert_eq!(j1900_1ns.weekday_tai(), 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_tai(), 0); + assert_eq!(j1900_10h_123_ns.weekday_tai(), Weekday::Monday); // Day +1: tuesday let j1901 = j1900 + Duration::from_days(1.0); - assert_eq!(j1901.weekday_tai(), 1); + assert_eq!(j1901.weekday_tai(), Weekday::Tuesday); // 1 ns into tuesday, still a tuesday let j1901 = j1901 + Duration::from_nanoseconds(1.0); - assert_eq!(j1901.weekday_tai(), 1); + assert_eq!(j1901.weekday_tai(), Weekday::Tuesday); // 6 days into TAI was a sunday let e = j1900 + Duration::from_days(6.0); - assert_eq!(e.weekday_tai(), 6); - // 6 days + some residuales, still a sunday + assert_eq!(e.weekday_tai(), Weekday::Sunday); + // 6 days + some tiny offset, still a sunday let e = e + Duration::from_nanoseconds(10000.0); - assert_eq!(e.weekday_tai(), 6); + assert_eq!(e.weekday_tai(), Weekday::Sunday); // 7 days into TAI: back to a monday let e = j1900 + Duration::from_days(7.0); - assert_eq!(e.weekday_tai(), 0); + assert_eq!(e.weekday_tai(), Weekday::Monday); // 2022/12/01 was a thursday let epoch = Epoch::from_gregorian_utc(2022, 12, 01, 00, 00, 00, 0); - assert_eq!(epoch.weekday_utc(), 3); + assert_eq!(epoch.weekday_utc(), Weekday::Thursday); // 2022/11/28 was a monday let epoch = Epoch::from_gregorian_utc(2022, 11, 28, 00, 00, 00, 0); - assert_eq!(epoch.weekday_utc(), 0); + assert_eq!(epoch.weekday_utc(), Weekday::Monday); } #[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.closest_utc_start_of_week(), Epoch::from_gregorian_utc_at_midnight(2022, 11, 27) ); + assert_eq!( + epoch.closest_utc_start_of_week().weekday_utc(), + Weekday::Sunday + ); let epoch = Epoch::from_gregorian_utc(2022, 09, 15, 01, 01, 01, 01); - assert_eq!(epoch.weekday_utc(), 3); + assert_eq!(epoch.weekday_utc(), Weekday::Thursday); assert_eq!( epoch.closest_utc_start_of_week(), Epoch::from_gregorian_utc_at_midnight(2022, 09, 11) ); + assert_eq!( + epoch.closest_utc_start_of_week().weekday_utc(), + Weekday::Sunday + ); } #[test] From a2dd4fc1653f6db1385d6613fe04ebc83b7cdf57 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Fri, 2 Dec 2022 19:10:39 +0100 Subject: [PATCH 08/22] introduce weekday enum Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 3dc4de6..814d664 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -2276,15 +2276,13 @@ impl Epoch { (wk, tow) } - /// Returns weekday counter in TAI timescale, - /// 0: Monday, ..., 6: Sunday + /// Returns weekday in TAI timescale pub fn weekday_tai(&self) -> Weekday { // J1900 was a monday :) ((self.to_tai_days() as u64).rem_euclid(7) as u8).into() } - /// Returns weekday counter in UTC timescale, - /// 0: Monday, ..., 6: Sunday + /// Returns weekday in UTC timescale pub fn weekday_utc(&self) -> Weekday { Self::from_tai_duration( self.duration_since_j1900_tai - self.leap_seconds(true).unwrap_or(0.0) * Unit::Second, From 528ec085aa8737f101d67b86eefe2630ac1f2a89 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Fri, 2 Dec 2022 19:43:54 +0100 Subject: [PATCH 09/22] weekday: basic arithmetics and ops --- src/weekday.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++++- tests/weekday.rs | 21 +++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 tests/weekday.rs diff --git a/src/weekday.rs b/src/weekday.rs index 40f9b00..f7dbfc4 100644 --- a/src/weekday.rs +++ b/src/weekday.rs @@ -9,6 +9,7 @@ */ use crate::ParsingErrors; +use core::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}; use core::str::FromStr; #[cfg(feature = "python")] @@ -18,6 +19,7 @@ use pyo3::prelude::*; 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 { @@ -30,9 +32,20 @@ pub enum Weekday { Sunday = 6, } +impl Default for Weekday { + fn default() -> Self { + Self::Monday + } +} + +impl Weekday { + /// Max: last weekday <=> `Sunday` + pub const MAX: Self = Self::Sunday; +} + impl From for Weekday { fn from(u: u8) -> Self { - match u { + match u.rem_euclid(Self::MAX.into()) { 0 => Self::Monday, 1 => Self::Tuesday, 2 => Self::Wednesday, @@ -74,3 +87,55 @@ impl FromStr for Weekday { } } } + +impl Add for Weekday { + type Output = Self; + fn add(self, rhs: Self) -> Self { + Self::from(self as u8 + rhs as u8) + } +} + +impl Sub for Weekday { + type Output = Self; + fn sub(self, rhs: Self) -> Self { + Self::from(self as u8 - rhs as u8) + } +} + +impl Add for Weekday { + type Output = Self; + fn add(self, rhs: u8) -> Self { + Self::from(self as u8 + rhs) + } +} + +impl Sub for Weekday { + type Output = Self; + fn sub(self, rhs: u8) -> Self { + Self::from(self as u8 - rhs) + } +} + +impl AddAssign for Weekday { + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl SubAssign for Weekday { + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +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; + } +} diff --git a/tests/weekday.rs b/tests/weekday.rs new file mode 100644 index 0000000..432c339 --- /dev/null +++ b/tests/weekday.rs @@ -0,0 +1,21 @@ +#[cfg(feature = "std")] +extern crate core; + +use hifitime::Weekday; + +#[cfg(feature = "std")] +use core::f64::EPSILON; +#[cfg(not(feature = "std"))] +use std::f64::EPSILON; + +#[test] +fn test_basic_ops() { + assert_eq!(Weekday::default(), Weekday::Monday); + let weekday = Weekday::default(); + for i in 1..24 { + // test (+) wrapping + let add = weekday + i; + let expected: Weekday = i.rem_euclid(Weekday::MAX.into()).into(); + assert_eq!(add, expected); + } +} From cd1748f5310d99e94416841d97e17557dd175913 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Fri, 2 Dec 2022 20:17:03 +0100 Subject: [PATCH 10/22] weekday: wrapping arithmetics Signed-off-by: Guillaume W. Bres --- src/weekday.rs | 14 ++++++++++---- tests/weekday.rs | 40 ++++++++++++++++++++++++++++++---------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/weekday.rs b/src/weekday.rs index f7dbfc4..4358898 100644 --- a/src/weekday.rs +++ b/src/weekday.rs @@ -9,7 +9,7 @@ */ use crate::ParsingErrors; -use core::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}; +use core::ops::{Add, AddAssign, Sub, SubAssign}; use core::str::FromStr; #[cfg(feature = "python")] @@ -40,12 +40,12 @@ impl Default for Weekday { impl Weekday { /// Max: last weekday <=> `Sunday` - pub const MAX: Self = Self::Sunday; + pub const MAX: u8 = 7; } impl From for Weekday { fn from(u: u8) -> Self { - match u.rem_euclid(Self::MAX.into()) { + match u.rem_euclid(Self::MAX) { 0 => Self::Monday, 1 => Self::Tuesday, 2 => Self::Wednesday, @@ -57,6 +57,12 @@ impl From for Weekday { } } +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 { @@ -112,7 +118,7 @@ impl Add for Weekday { impl Sub for Weekday { type Output = Self; fn sub(self, rhs: u8) -> Self { - Self::from(self as u8 - rhs) + Self::from(self as i8 - rhs as i8) } } diff --git a/tests/weekday.rs b/tests/weekday.rs index 432c339..995f22f 100644 --- a/tests/weekday.rs +++ b/tests/weekday.rs @@ -3,19 +3,39 @@ extern crate core; use hifitime::Weekday; -#[cfg(feature = "std")] -use core::f64::EPSILON; -#[cfg(not(feature = "std"))] -use std::f64::EPSILON; - #[test] fn test_basic_ops() { assert_eq!(Weekday::default(), Weekday::Monday); - let weekday = Weekday::default(); - for i in 1..24 { - // test (+) wrapping - let add = weekday + i; + + let monday = Weekday::default(); + for i in 0..24 { + // wrapping + let add = monday + i; let expected: Weekday = i.rem_euclid(Weekday::MAX.into()).into(); - assert_eq!(add, expected); + assert_eq!( + add, expected, + "test failed, expecting {:?} got {:?} for {:02} conversion", + expected, add, i + ); } + + 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); } From 306bc08b268d852bf1e44d28ce0459f78a779311 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Fri, 2 Dec 2022 17:20:57 -0700 Subject: [PATCH 11/22] Initial review of the Weekday addition This is good! Signed-off-by: Christopher Rabotin --- src/epoch.rs | 28 ++++++++++++++++-------- src/timescale.rs | 8 ++++++- src/weekday.rs | 27 ++++++++++++++++++++--- tests/epoch.rs | 57 +++++++++++++++++++++++++++++++++++++----------- tests/weekday.rs | 10 --------- 5 files changed, 94 insertions(+), 36 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 814d664..aad70eb 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -2247,7 +2247,7 @@ impl Epoch { /// `wk` is a rolling week conter into that time scale, /// `tow` is the number of seconds since closest Sunday midnight into that week. pub fn to_timeofweek_utc(&self) -> (u32, u64) { - // fractionnal days in this time scale + // fractional days in this time scale let days = match self.time_scale { TimeScale::GPST => self.to_gpst_days(), TimeScale::GST => self.to_gst_days(), @@ -2276,18 +2276,28 @@ impl Epoch { (wk, tow) } - /// Returns weekday in TAI timescale - pub fn weekday_tai(&self) -> Weekday { - // J1900 was a monday :) - ((self.to_tai_days() as u64).rem_euclid(7) as u8).into() + /// 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 { + (self + .to_duration_in_time_scale(time_scale) + .to_unit(Unit::Day) + .rem_euclid(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::from_tai_duration( - self.duration_since_j1900_tai - self.leap_seconds(true).unwrap_or(0.0) * Unit::Second, - ) - .weekday_tai() + self.weekday_in_time_scale(TimeScale::UTC) } /// Returns closest UTC Sunday midnight (ie., start of week) from self diff --git a/src/timescale.rs b/src/timescale.rs index 5583998..7ecfbc0 100644 --- a/src/timescale.rs +++ b/src/timescale.rs @@ -17,7 +17,13 @@ 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_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 { diff --git a/src/weekday.rs b/src/weekday.rs index 4358898..cd0848d 100644 --- a/src/weekday.rs +++ b/src/weekday.rs @@ -39,8 +39,10 @@ impl Default for Weekday { } impl Weekday { - /// Max: last weekday <=> `Sunday` - pub const MAX: u8 = 7; + /// 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; } impl From for Weekday { @@ -52,7 +54,8 @@ impl From for Weekday { 3 => Self::Thursday, 4 => Self::Friday, 5 => Self::Saturday, - _ => Self::Sunday, + 6 => Self::Sunday, + _ => Self::default(), // Defaults back to default for other values. } } } @@ -145,3 +148,21 @@ impl SubAssign for Weekday { *self = *self - rhs; } } + +#[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 + ); + } +} diff --git a/tests/epoch.rs b/tests/epoch.rs index 681395a..685f3fb 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -3,8 +3,9 @@ extern crate core; use hifitime::{ is_gregorian_valid, Duration, Epoch, TimeScale, TimeUnits, Unit, Weekday, 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, + 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")] @@ -1340,36 +1341,66 @@ fn test_minmax() { #[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 = Epoch::from_gregorian_tai_at_midnight(1900, 01, 01); - assert_eq!(j1900.weekday_tai(), Weekday::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_tai(), Weekday::Monday); + 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_tai(), Weekday::Monday); + 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_tai(), Weekday::Tuesday); + 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_tai(), Weekday::Tuesday); + 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_tai(), Weekday::Sunday); + 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_tai(), Weekday::Sunday); + 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_tai(), Weekday::Monday); + assert_eq!(e.weekday(), Weekday::Monday); + permutate_time_scale(e, Weekday::Monday); // 2022/12/01 was a thursday - let epoch = Epoch::from_gregorian_utc(2022, 12, 01, 00, 00, 00, 0); + 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(2022, 11, 28, 00, 00, 00, 0); + 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] diff --git a/tests/weekday.rs b/tests/weekday.rs index 995f22f..fd2bb2a 100644 --- a/tests/weekday.rs +++ b/tests/weekday.rs @@ -8,16 +8,6 @@ fn test_basic_ops() { assert_eq!(Weekday::default(), Weekday::Monday); let monday = Weekday::default(); - for i in 0..24 { - // wrapping - let add = monday + i; - let expected: Weekday = i.rem_euclid(Weekday::MAX.into()).into(); - assert_eq!( - add, expected, - "test failed, expecting {:?} got {:?} for {:02} conversion", - expected, add, i - ); - } assert_eq!(monday - 1, Weekday::Sunday); assert_eq!(monday - 2, Weekday::Saturday); From 036f922cddb8de78057c9b3b0e9dc6e7a91f286e Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Fri, 2 Dec 2022 18:56:59 -0700 Subject: [PATCH 12/22] Implement weekday computations Also add `with_hms` and `with_hms_strict` to set the time of a given Epoch Closes #183 Signed-off-by: Christopher Rabotin --- src/epoch.rs | 109 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 +- src/weekday.rs | 40 ++++++++--------- tests/epoch.rs | 23 ++++++++++ tests/weekday.rs | 107 +++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 260 insertions(+), 21 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index aad70eb..b3fb6a4 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -2300,6 +2300,115 @@ impl Epoch { 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 + } + } + + /// 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 + } + } + + /// 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, + ) + } + /// Returns closest UTC Sunday midnight (ie., start of week) from self pub fn closest_utc_start_of_week(&self) -> Self { let weekday = self.weekday_utc(); diff --git a/src/lib.rs b/src/lib.rs index 8096772..f9ce401 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,7 +88,7 @@ mod deprecated; pub mod prelude { pub use crate::{ deprecated::TimeSystem, Duration, Epoch, Errors, Freq, Frequencies, TimeScale, TimeSeries, - TimeUnits, Unit, + TimeUnits, Unit, Weekday, }; } diff --git a/src/weekday.rs b/src/weekday.rs index cd0848d..40830aa 100644 --- a/src/weekday.rs +++ b/src/weekday.rs @@ -8,7 +8,8 @@ * Documentation: https://nyxspace.com/ */ -use crate::ParsingErrors; +use crate::{Duration, ParsingErrors, Unit}; +use core::fmt; use core::ops::{Add, AddAssign, Sub, SubAssign}; use core::str::FromStr; @@ -100,40 +101,35 @@ impl FromStr for Weekday { impl Add for Weekday { type Output = Self; fn add(self, rhs: Self) -> Self { - Self::from(self as u8 + rhs as u8) + Self::from(u8::from(self) + u8::from(rhs)) } } impl Sub for Weekday { - type Output = Self; - fn sub(self, rhs: Self) -> Self { - Self::from(self as u8 - rhs as u8) + 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(self as u8 + rhs) + Self::from(u8::from(self) + rhs) } } impl Sub for Weekday { type Output = Self; fn sub(self, rhs: u8) -> Self { - Self::from(self as i8 - rhs as i8) - } -} - -impl AddAssign for Weekday { - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } -} - -impl SubAssign for Weekday { - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; + // 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 - u8::from(rhs) as i8) } } @@ -149,6 +145,12 @@ impl SubAssign for Weekday { } } +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); diff --git a/tests/epoch.rs b/tests/epoch.rs index 685f3fb..b3f1ca6 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -1403,6 +1403,29 @@ fn test_weekday() { 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 diff --git a/tests/weekday.rs b/tests/weekday.rs index fd2bb2a..4d6932d 100644 --- a/tests/weekday.rs +++ b/tests/weekday.rs @@ -1,7 +1,7 @@ #[cfg(feature = "std")] extern crate core; -use hifitime::Weekday; +use hifitime::{Duration, Epoch, Unit, Weekday}; #[test] fn test_basic_ops() { @@ -29,3 +29,108 @@ fn test_basic_ops() { 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] +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) + ); +} + +#[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) + ); +} From 5d9034d59770fa57596ba585fbc8e57b3c7badbd Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Fri, 2 Dec 2022 19:09:09 -0700 Subject: [PATCH 13/22] Fixed time of week calculation Needs more testing for sure Signed-off-by: Christopher Rabotin --- src/epoch.rs | 70 ++++++++++++++++++++------------------------------ tests/epoch.rs | 22 +++++++++------- 2 files changed, 41 insertions(+), 51 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index b3fb6a4..528804e 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -1192,14 +1192,14 @@ impl Epoch { /// `ns` is the amount of nanoseconds into that week from closest Sunday Midnight. /// This is usually how GNSS receivers describe a GNSS time scale epoch #[must_use] - pub fn from_timeofweek(wk: u32, ns: u64, ts: TimeScale) -> Self { - let week = Duration::from_seconds(wk as f64 * SECONDS_PER_DAY * 7.0); + pub fn from_time_of_week(wk: u32, ns: u64, ts: TimeScale) -> Self { + let week = Duration::from_seconds(wk as f64 * SECONDS_PER_DAY * Weekday::DAYS_PER_WEEK); Self::from_duration(week + (ns as f64) * Unit::Nanosecond, ts) } #[must_use] - pub fn from_timeofweek_utc(wk: u32, ns: u64) -> Self { - let week = Duration::from_seconds(wk as f64 * SECONDS_PER_DAY * 7.0); + pub fn from_time_of_week_utc(wk: u32, ns: u64) -> Self { + let week = Duration::from_seconds(wk as f64 * SECONDS_PER_DAY * Weekday::DAYS_PER_WEEK); Self::from_utc_duration(week + (ns as f64) * Unit::Nanosecond) } } @@ -2242,38 +2242,17 @@ impl Epoch { me } - /// Converts to "time of week" (`wk, `tow`), - /// which is usually how GNSS receivers describe a timestamp. - /// `wk` is a rolling week conter into that time scale, - /// `tow` is the number of seconds since closest Sunday midnight into that week. - pub fn to_timeofweek_utc(&self) -> (u32, u64) { + /// Converts this epoch into the time of week, represented as a rolling week counter into that time scale and the number of seconds since closest Sunday midnight into that week. + /// This is usually how GNSS receivers describe a timestamp. + pub fn to_time_of_week_utc(&self) -> (u32, u64) { // fractional days in this time scale - let days = match self.time_scale { - TimeScale::GPST => self.to_gpst_days(), - TimeScale::GST => self.to_gst_days(), - TimeScale::BDT => self.to_bdt_days(), - TimeScale::TT => self.to_tt_days(), - TimeScale::TAI => self.to_tai_days(), - TimeScale::UTC => self.to_utc_days(), - TimeScale::TDB => self.to_tdb_duration().to_unit(Unit::Day), - TimeScale::ET => self.to_et_duration().to_unit(Unit::Day), - }; + let days = self.to_duration().to_unit(Unit::Day); // wk: rolling week counter into timescale - let wk = (days / 7.0).floor() as u32; - // tow: number of nanoseconds between self and closest sunday midnight / start of week - let start_of_week = self.closest_utc_start_of_week(); + let wk = (days / Weekday::DAYS_PER_WEEK).floor() as u32; + // tow: number of nanoseconds between self and previous sunday midnight / start of week + let start_of_week = self.previous_weekday_at_midnight(Weekday::Sunday); let dw = *self - start_of_week; // difference in weekdays [0..6] - let (_, d, h, m, s, ms, us, ns) = dw.decompose(); - let tow = Duration::from_days(d as f64) - + Duration::from_hours(h as f64) - + Duration::from_seconds((m * 60) as f64) - + Duration::from_seconds(s as f64) - + Duration::from_milliseconds(ms as f64) - + Duration::from_microseconds(us as f64) - + Duration::from_nanoseconds(ns as f64); - let (_, mut tow) = tow.to_parts(); - tow += self.leap_seconds(true).unwrap_or(0.0) as u64 * 1_000_000_000; - (wk, tow) + (wk, dw.nanoseconds) } /// Returns the weekday in provided time scale **ASSUMING** that the reference epoch of that time scale is a Monday. @@ -2324,6 +2303,14 @@ impl Epoch { } } + 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. /// /// ``` @@ -2347,6 +2334,14 @@ impl Epoch { } } + 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 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 @@ -2409,15 +2404,6 @@ impl Epoch { ) } - /// Returns closest UTC Sunday midnight (ie., start of week) from self - pub fn closest_utc_start_of_week(&self) -> Self { - let weekday = self.weekday_utc(); - let days = Duration::from_days((weekday as u8).into()); // duration into that week - // -1: 0 is monday, we want Sunday midnight - let (y, m, d, _, _, _, _) = (*self - days - Duration::from_days(1.0)).to_gregorian_utc(); - Self::from_gregorian_utc_at_midnight(y, m, d) - } - // Python helpers #[cfg(feature = "python")] diff --git a/tests/epoch.rs b/tests/epoch.rs index b3f1ca6..91911f3 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -1433,22 +1433,26 @@ fn test_start_of_week() { assert_eq!(epoch.weekday_utc(), Weekday::Thursday); // 2022/11/27 was the related sunday / start of week assert_eq!( - epoch.closest_utc_start_of_week(), + epoch.previous_weekday_at_midnight(Weekday::Sunday), Epoch::from_gregorian_utc_at_midnight(2022, 11, 27) ); assert_eq!( - epoch.closest_utc_start_of_week().weekday_utc(), + 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.closest_utc_start_of_week(), + epoch.previous_weekday_at_midnight(Weekday::Sunday), Epoch::from_gregorian_utc_at_midnight(2022, 09, 11) ); assert_eq!( - epoch.closest_utc_start_of_week().weekday_utc(), + epoch + .previous_weekday_at_midnight(Weekday::Sunday) + .weekday_utc(), Weekday::Sunday ); } @@ -1458,19 +1462,19 @@ fn test_timeofweek() { // GPST // https://www.labsat.co.uk/index.php/en/gps-time-calculator // 01/12/2022 00:00:00 <=> (2238, 345_618_000_000_000) - let epoch = Epoch::from_timeofweek(2238, 345_618_000_000_000, TimeScale::GPST); + 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_timeofweek_utc(), (2238, 345_618_000_000_000)); + assert_eq!(epoch.to_time_of_week_utc(), (2238, 345_618_000_000_000)); // add 1 nanos - let epoch = Epoch::from_timeofweek(2238, 345_618_000_000_001, TimeScale::GPST); + 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_timeofweek(2238, 475_218_000_000_000, TimeScale::GPST); + 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_timeofweek(2238, 487_657_000_000_010, TimeScale::GPST); + 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)); } From a6b77190b8f099559013b58ee04296b8d2593b34 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Fri, 2 Dec 2022 23:38:04 -0700 Subject: [PATCH 14/22] Fix no-std build Signed-off-by: Christopher Rabotin --- src/epoch.rs | 2 +- src/weekday.rs | 20 ++++++++++---------- tests/weekday.rs | 4 ++++ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 528804e..2433ffe 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -37,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; diff --git a/src/weekday.rs b/src/weekday.rs index 40830aa..0f3a160 100644 --- a/src/weekday.rs +++ b/src/weekday.rs @@ -84,15 +84,14 @@ impl From for u8 { impl FromStr for Weekday { type Err = ParsingErrors; fn from_str(s: &str) -> Result { - let val = s.trim().to_lowercase(); - match val.as_str() { - "monday" => Ok(Self::Monday), - "tuesday" => Ok(Self::Tuesday), - "wednesday" => Ok(Self::Wednesday), - "thursday" => Ok(Self::Thursday), - "friday" => Ok(Self::Friday), - "saturday" => Ok(Self::Saturday), - "sunday" => Ok(Self::Sunday), + 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), } } @@ -129,7 +128,7 @@ 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 - u8::from(rhs) as i8) + Self::from(u8::from(self) as i8 - rhs as i8) } } @@ -166,5 +165,6 @@ fn test_wrapping() { "expecting {:?} got {:?} for {:02} conversion", expected, add, i ); + // Test FromStr } } diff --git a/tests/weekday.rs b/tests/weekday.rs index 4d6932d..838caa0 100644 --- a/tests/weekday.rs +++ b/tests/weekday.rs @@ -1,6 +1,8 @@ #[cfg(feature = "std")] extern crate core; +use core::str::FromStr; + use hifitime::{Duration, Epoch, Unit, Weekday}; #[test] @@ -65,6 +67,8 @@ fn test_weekday_differences() { } else { assert_eq!(neg_delta, i64::from(day_num % 7 - 2) * Unit::Day); } + // Test FromStr + assert_eq!(Weekday::from_str(&format!("{day}")).unwrap(), day); } } From 0dc5c1c922041b12ea957314477e6f19f018e28e Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 3 Dec 2022 00:41:03 -0700 Subject: [PATCH 15/22] Expand test suite for weekday computation Found bug in time_of_week initialization? Signed-off-by: Christopher Rabotin --- src/epoch.rs | 30 ++++++++---- tests/epoch.rs | 8 +++- tests/weekday.rs | 121 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 147 insertions(+), 12 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 2433ffe..42a6ef8 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -1199,8 +1199,7 @@ impl Epoch { #[must_use] pub fn from_time_of_week_utc(wk: u32, ns: u64) -> Self { - let week = Duration::from_seconds(wk as f64 * SECONDS_PER_DAY * Weekday::DAYS_PER_WEEK); - Self::from_utc_duration(week + (ns as f64) * Unit::Nanosecond) + Self::from_time_of_week(wk, ns, TimeScale::UTC) } } @@ -2244,7 +2243,7 @@ impl Epoch { /// Converts this epoch into the time of week, represented as a rolling week counter into that time scale and the number of seconds since closest Sunday midnight into that week. /// This is usually how GNSS receivers describe a timestamp. - pub fn to_time_of_week_utc(&self) -> (u32, u64) { + pub fn to_time_of_week(&self) -> (u32, u64) { // fractional days in this time scale let days = self.to_duration().to_unit(Unit::Day); // wk: rolling week counter into timescale @@ -2259,12 +2258,25 @@ impl Epoch { /// 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 { - (self - .to_duration_in_time_scale(time_scale) - .to_unit(Unit::Day) - .rem_euclid(Weekday::DAYS_PER_WEEK) - .floor() as u8) - .into() + #[cfg(feature = "std")] + { + (self + .to_duration_in_time_scale(time_scale) + .to_unit(Unit::Day) + .rem_euclid(Weekday::DAYS_PER_WEEK) + .floor() as u8) + .into() + } + #[cfg(not(feature = "std"))] + { + // The rem_euclid call of num_traits requires a pointer not a value. + (self + .to_duration_in_time_scale(time_scale) + .to_unit(Unit::Day) + .rem_euclid(&Weekday::DAYS_PER_WEEK) + .floor() as u8) + .into() + } } /// Returns weekday (uses the TAI representation for this calculation). diff --git a/tests/epoch.rs b/tests/epoch.rs index 91911f3..f8abfdd 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -1458,13 +1458,17 @@ fn test_start_of_week() { } #[test] -fn test_timeofweek() { +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) 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_utc(), (2238, 345_618_000_000_000)); + assert_eq!(epoch.to_time_of_week(), (2238, 345_618_000_000_000)); + + let epoch_utc = epoch.in_time_scale(TimeScale::UTC); + let (utc_wk, utc_tow) = epoch_utc.to_time_of_week(); + assert_eq!(Epoch::from_time_of_week_utc(utc_wk, utc_tow), epoch_utc); // add 1 nanos let epoch = Epoch::from_time_of_week(2238, 345_618_000_000_001, TimeScale::GPST); diff --git a/tests/weekday.rs b/tests/weekday.rs index 838caa0..8efa4b5 100644 --- a/tests/weekday.rs +++ b/tests/weekday.rs @@ -3,7 +3,7 @@ extern crate core; use core::str::FromStr; -use hifitime::{Duration, Epoch, Unit, Weekday}; +use hifitime::{Duration, Epoch, TimeUnits, Unit, Weekday}; #[test] fn test_basic_ops() { @@ -104,6 +104,65 @@ fn test_next() { 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] @@ -137,4 +196,64 @@ fn test_previous() { 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) + ); } From 0d6ca37d08b89212de03711bb3dcf90f879c109f Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 3 Dec 2022 00:57:18 -0700 Subject: [PATCH 16/22] Add day_of_year and duration_in_year This is the first step in #182 Signed-off-by: Christopher Rabotin --- src/epoch.rs | 12 ++++++++++++ tests/epoch.rs | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/src/epoch.rs b/src/epoch.rs index 42a6ef8..8db5ec9 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -2354,6 +2354,18 @@ impl Epoch { 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 diff --git a/tests/epoch.rs b/tests/epoch.rs index f8abfdd..744cee5 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -1016,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); From fa246f5d9041640a22b2a4c497844548744ce1cc Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 3 Dec 2022 11:16:21 +0100 Subject: [PATCH 17/22] epoch::to_time_of_week: restrict first sunday to the timescale Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 34 ++++++++++++++++++++++++++++++---- tests/epoch.rs | 48 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 8db5ec9..1096e7c 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -12,8 +12,9 @@ use crate::duration::{Duration, Unit}; use crate::parser::Token; use crate::{ Errors, TimeScale, BDT_REF_EPOCH, DAYS_PER_YEAR_NLD, ET_EPOCH_S, GPST_REF_EPOCH, GST_REF_EPOCH, - J1900_OFFSET, J2000_TO_J1900_DURATION, MJD_OFFSET, NANOSECONDS_PER_MICROSECOND, - NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_SECOND_U32, SECONDS_PER_DAY, UNIX_REF_EPOCH, + J1900_OFFSET, J1900_REF_EPOCH, J2000_TO_J1900_DURATION, MJD_OFFSET, + NANOSECONDS_PER_MICROSECOND, NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_SECOND_U32, + SECONDS_PER_DAY, UNIX_REF_EPOCH, }; use core::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}; use core::fmt; @@ -2241,7 +2242,7 @@ impl Epoch { me } - /// Converts this epoch into the time of week, represented as a rolling week counter into that time scale and the number of seconds since closest Sunday midnight into that week. + /// 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) { // fractional days in this time scale @@ -2249,7 +2250,32 @@ impl Epoch { // wk: rolling week counter into timescale let wk = (days / Weekday::DAYS_PER_WEEK).floor() as u32; // tow: number of nanoseconds between self and previous sunday midnight / start of week - let start_of_week = self.previous_weekday_at_midnight(Weekday::Sunday); + let mut start_of_week = self.previous_weekday_at_midnight(Weekday::Sunday); + // restrict start of week/sunday to the time scale starting day + // std::cmp::min( start_of_week, self.time_scale.ref_epoch) + match self.time_scale { + TimeScale::GPST => { + if start_of_week < GPST_REF_EPOCH { + start_of_week = GPST_REF_EPOCH; + } + } + TimeScale::BDT => { + if start_of_week < BDT_REF_EPOCH { + start_of_week = BDT_REF_EPOCH; + } + } + TimeScale::GST => { + if start_of_week < GST_REF_EPOCH { + start_of_week = GST_REF_EPOCH; + } + } + TimeScale::TAI => { + if start_of_week < J1900_REF_EPOCH { + start_of_week = J1900_REF_EPOCH; + } + } + _ => {} + } let dw = *self - start_of_week; // difference in weekdays [0..6] (wk, dw.nanoseconds) } diff --git a/tests/epoch.rs b/tests/epoch.rs index 744cee5..26e699a 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -1467,23 +1467,47 @@ 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::UTC); - let (utc_wk, utc_tow) = epoch_utc.to_time_of_week(); - assert_eq!(Epoch::from_time_of_week_utc(utc_wk, utc_tow), epoch_utc); + /* + let epoch_utc = epoch.in_time_scale(TimeScale::UTC); + let (utc_wk, utc_tow) = epoch_utc.to_time_of_week(); + assert_eq!(Epoch::from_time_of_week_utc(utc_wk, utc_tow), 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 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 + 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)); + // 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)); + */ } From 84094a12eb4f7503066bfc7bd5bfd9ce825b171b Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 3 Dec 2022 13:19:13 +0100 Subject: [PATCH 18/22] timescale: introduce ref_epoch for easy referencing Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 30 +++++------------------------- src/timescale.rs | 9 +++++++++ 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 1096e7c..f827d99 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -12,7 +12,7 @@ use crate::duration::{Duration, Unit}; use crate::parser::Token; use crate::{ Errors, TimeScale, BDT_REF_EPOCH, DAYS_PER_YEAR_NLD, ET_EPOCH_S, GPST_REF_EPOCH, GST_REF_EPOCH, - J1900_OFFSET, J1900_REF_EPOCH, J2000_TO_J1900_DURATION, MJD_OFFSET, + J1900_OFFSET, J2000_TO_J1900_DURATION, MJD_OFFSET, NANOSECONDS_PER_MICROSECOND, NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_SECOND_U32, SECONDS_PER_DAY, UNIX_REF_EPOCH, }; @@ -2251,30 +2251,10 @@ impl Epoch { let wk = (days / Weekday::DAYS_PER_WEEK).floor() as u32; // tow: number of nanoseconds between self and previous sunday midnight / start of week let mut start_of_week = self.previous_weekday_at_midnight(Weekday::Sunday); - // restrict start of week/sunday to the time scale starting day - // std::cmp::min( start_of_week, self.time_scale.ref_epoch) - match self.time_scale { - TimeScale::GPST => { - if start_of_week < GPST_REF_EPOCH { - start_of_week = GPST_REF_EPOCH; - } - } - TimeScale::BDT => { - if start_of_week < BDT_REF_EPOCH { - start_of_week = BDT_REF_EPOCH; - } - } - TimeScale::GST => { - if start_of_week < GST_REF_EPOCH { - start_of_week = GST_REF_EPOCH; - } - } - TimeScale::TAI => { - if start_of_week < J1900_REF_EPOCH { - start_of_week = J1900_REF_EPOCH; - } - } - _ => {} + 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, dw.nanoseconds) diff --git a/src/timescale.rs b/src/timescale.rs index 7ecfbc0..8972eb6 100644 --- a/src/timescale.rs +++ b/src/timescale.rs @@ -105,6 +105,15 @@ 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(crate) const fn ref_epoch(&self) -> Epoch { + match self { + Self::GPST => GPST_REF_EPOCH, + Self::GST => GST_REF_EPOCH, + Self::BDT => BDT_REF_EPOCH, + _ => J1900_REF_EPOCH, + } + } } impl fmt::Display for TimeScale { From c407c9f57772bcdfb29c6e8eb25cbaa2fff7933c Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 3 Dec 2022 13:32:31 +0100 Subject: [PATCH 19/22] cargo fmt Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index f827d99..3f97fec 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -12,9 +12,8 @@ use crate::duration::{Duration, Unit}; use crate::parser::Token; use crate::{ Errors, TimeScale, BDT_REF_EPOCH, DAYS_PER_YEAR_NLD, ET_EPOCH_S, GPST_REF_EPOCH, GST_REF_EPOCH, - J1900_OFFSET, J2000_TO_J1900_DURATION, MJD_OFFSET, - NANOSECONDS_PER_MICROSECOND, NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_SECOND_U32, - SECONDS_PER_DAY, UNIX_REF_EPOCH, + J1900_OFFSET, J2000_TO_J1900_DURATION, MJD_OFFSET, NANOSECONDS_PER_MICROSECOND, + NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_SECOND_U32, SECONDS_PER_DAY, UNIX_REF_EPOCH, }; use core::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}; use core::fmt; From b17f171f9657a4c214760b666d5f8a57277fb1d2 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 3 Dec 2022 13:56:08 +0100 Subject: [PATCH 20/22] docs, tests, reciprocal test Signed-off-by: Guillaume W. Bres --- src/epoch.rs | 18 +++++++++--------- tests/epoch.rs | 30 +++++++++++++----------------- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 3f97fec..7309b99 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -1188,18 +1188,18 @@ impl Epoch { } } - /// Builds an Epoch from given `wk` week counter into the desired Time scale. - /// `ns` is the amount of nanoseconds into that week from closest Sunday Midnight. - /// This is usually how GNSS receivers describe a GNSS time scale 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(wk: u32, ns: u64, ts: TimeScale) -> Self { - let week = Duration::from_seconds(wk as f64 * SECONDS_PER_DAY * Weekday::DAYS_PER_WEEK); + pub fn from_time_of_week(week: u32, ns: u64, ts: TimeScale) -> Self { + let week = Duration::from_seconds(week as f64 * SECONDS_PER_DAY * Weekday::DAYS_PER_WEEK); Self::from_duration(week + (ns as f64) * Unit::Nanosecond, ts) } #[must_use] - pub fn from_time_of_week_utc(wk: u32, ns: u64) -> Self { - Self::from_time_of_week(wk, ns, TimeScale::UTC) + /// 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, ns: u64) -> Self { + Self::from_time_of_week(week, ns, TimeScale::UTC) } } @@ -2244,9 +2244,9 @@ impl Epoch { /// 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) { - // fractional days in this time scale - let days = self.to_duration().to_unit(Unit::Day); // wk: rolling week counter into timescale + // fractional days in this time scale + let days = self.to_duration().to_unit(Unit::Day); let wk = (days / Weekday::DAYS_PER_WEEK).floor() as u32; // tow: number of nanoseconds between self and previous sunday midnight / start of week let mut start_of_week = self.previous_weekday_at_midnight(Weekday::Sunday); diff --git a/tests/epoch.rs b/tests/epoch.rs index 26e699a..cc3dc4f 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -1473,11 +1473,10 @@ fn test_time_of_week() { 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::UTC); - let (utc_wk, utc_tow) = epoch_utc.to_time_of_week(); - assert_eq!(Epoch::from_time_of_week_utc(utc_wk, utc_tow), epoch_utc); - */ + let epoch_utc = epoch.in_time_scale(TimeScale::UTC); + let (utc_wk, utc_tow) = epoch_utc.to_time_of_week(); + assert_eq!(Epoch::from_time_of_week_utc(utc_wk, utc_tow), 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)); @@ -1496,18 +1495,15 @@ fn test_time_of_week() { ); 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 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 + 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)); - */ + // 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)); } From e5604f14f0c14f96318af23caabd5612ee47dc38 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 3 Dec 2022 14:10:26 +0100 Subject: [PATCH 21/22] docs, fmt, more tests Signed-off-by: Guillaume W. Bres --- src/timescale.rs | 2 +- tests/epoch.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/timescale.rs b/src/timescale.rs index 8972eb6..d9a0cb3 100644 --- a/src/timescale.rs +++ b/src/timescale.rs @@ -106,7 +106,7 @@ impl TimeScale { matches!(self, Self::GPST | Self::GST | Self::BDT) } /// Returns Reference Epoch (t(0)) for given timescale - pub(crate) const fn ref_epoch(&self) -> Epoch { + pub const fn ref_epoch(&self) -> Epoch { match self { Self::GPST => GPST_REF_EPOCH, Self::GST => GST_REF_EPOCH, diff --git a/tests/epoch.rs b/tests/epoch.rs index cc3dc4f..3a6ad9d 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -1506,4 +1506,44 @@ fn test_time_of_week() { // 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) + ); } From 704fbff07ba9b3a369b2b77a4c47a942e585c223 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Mon, 5 Dec 2022 23:30:46 -0700 Subject: [PATCH 22/22] Hot fix for #187 Signed-off-by: Christopher Rabotin --- src/epoch.rs | 58 ++++++++++++++++++++++++------------------------ src/lib.rs | 18 +++++++++++++++ src/timescale.rs | 11 +++++++-- src/weekday.rs | 2 ++ tests/epoch.rs | 7 ++++-- 5 files changed, 63 insertions(+), 33 deletions(-) diff --git a/src/epoch.rs b/src/epoch.rs index 7309b99..f09b2ee 100644 --- a/src/epoch.rs +++ b/src/epoch.rs @@ -13,7 +13,7 @@ use crate::parser::Token; use crate::{ Errors, TimeScale, BDT_REF_EPOCH, DAYS_PER_YEAR_NLD, ET_EPOCH_S, GPST_REF_EPOCH, GST_REF_EPOCH, J1900_OFFSET, J2000_TO_J1900_DURATION, MJD_OFFSET, NANOSECONDS_PER_MICROSECOND, - NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_SECOND_U32, SECONDS_PER_DAY, UNIX_REF_EPOCH, + NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_SECOND_U32, UNIX_REF_EPOCH, }; use core::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}; use core::fmt; @@ -1191,15 +1191,20 @@ 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, ns: u64, ts: TimeScale) -> Self { - let week = Duration::from_seconds(week as f64 * SECONDS_PER_DAY * Weekday::DAYS_PER_WEEK); - Self::from_duration(week + (ns as f64) * Unit::Nanosecond, ts) + 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, ns: u64) -> Self { - Self::from_time_of_week(week, ns, TimeScale::UTC) + pub fn from_time_of_week_utc(week: u32, nanoseconds: u64) -> Self { + Self::from_time_of_week(week, nanoseconds, TimeScale::UTC) } } @@ -2173,6 +2178,7 @@ impl Epoch { Self::compute_gregorian(self.to_tai_duration()) } + #[must_use] /// Floors this epoch to the closest provided duration /// /// # Example @@ -2195,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 @@ -2218,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 @@ -2234,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; @@ -2241,14 +2250,16 @@ 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 days = self.to_duration().to_unit(Unit::Day); - let wk = (days / Weekday::DAYS_PER_WEEK).floor() as u32; - // tow: number of nanoseconds between self and previous sunday midnight / start of week + 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 @@ -2256,32 +2267,21 @@ impl Epoch { start_of_week = ref_epoch; } let dw = *self - start_of_week; // difference in weekdays [0..6] - (wk, dw.nanoseconds) + (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 { - #[cfg(feature = "std")] - { - (self - .to_duration_in_time_scale(time_scale) - .to_unit(Unit::Day) - .rem_euclid(Weekday::DAYS_PER_WEEK) - .floor() as u8) - .into() - } - #[cfg(not(feature = "std"))] - { - // The rem_euclid call of num_traits requires a pointer not a value. - (self - .to_duration_in_time_scale(time_scale) - .to_unit(Unit::Day) - .rem_euclid(&Weekday::DAYS_PER_WEEK) - .floor() as u8) - .into() - } + (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). diff --git a/src/lib.rs b/src/lib.rs index f9ce401..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; diff --git a/src/timescale.rs b/src/timescale.rs index d9a0cb3..b4673ec 100644 --- a/src/timescale.rs +++ b/src/timescale.rs @@ -17,7 +17,10 @@ use serde_derive::{Deserialize, Serialize}; use core::fmt; use core::str::FromStr; -use crate::{Duration, Epoch, Errors, ParsingErrors, J2000_TO_J1900_DURATION, 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); @@ -105,13 +108,17 @@ 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, - _ => J1900_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, } } } diff --git a/src/weekday.rs b/src/weekday.rs index 0f3a160..9c1f7ae 100644 --- a/src/weekday.rs +++ b/src/weekday.rs @@ -44,6 +44,8 @@ impl Weekday { 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 { diff --git a/tests/epoch.rs b/tests/epoch.rs index 3a6ad9d..d5b6a3c 100644 --- a/tests/epoch.rs +++ b/tests/epoch.rs @@ -1473,9 +1473,12 @@ fn test_time_of_week() { 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::UTC); + 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(utc_wk, utc_tow), epoch_utc); + 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);