From 59c7ece1264c8ce16cc5c90a414c54aeb2dbb8d5 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 18 Mar 2024 15:14:40 +0100 Subject: [PATCH] Major refactoring of the time API --- CHANGELOG.md | 32 ++ Cargo.toml | 13 +- README.md | 2 + src/ecss/tm.rs | 8 +- src/time/ascii.rs | 78 ++--- src/time/cds.rs | 662 +++++++++++++++++++----------------- src/time/cuc.rs | 836 ++++++++++++++++++++++++++++------------------ src/time/mod.rs | 381 +++++++++++++-------- 8 files changed, 1202 insertions(+), 810 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6578ca4..4300d4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,41 @@ and this project adheres to [Semantic Versioning](http://semver.org/). # [unreleased] +Major API changes for the time API. If you are using the time API, it is strongly recommended +to check all the API changes in the **Changed** chapter. + +## Fixed + +- CUC timestamp was fixed to include leap second corrections because it is based on the TAI + time reference. The default CUC time object do not implement `CcsdsTimeProvider` anymore + because the trait methods require cached leap second information. This task is now performed + by the `cuc::CucTimeWithLeapSecs` which implements the trait. + ## Added - `From<$EcssEnum$TY> from $TY` for the ECSS enum type definitions. +- Added basic support conversions to the `time` library. Introduce new `chrono` and `timelib` + feature gate. +- Added `CcsdsTimeProvider::timelib_date_time`. + +## Changed + +- Renamed `CcsdsTimeProvider::date_time` to `CcsdsTimeProvider::chrono_date_time` +- Renamed `CcsdsTimeCodes` to `CcsdsTimeCode` +- Renamed `cds::TimeProvider` to `cds::CdsTime` +- Renamed `cuc::TimeProviderCcsdsEpoch` to `cuc::CucTime` +- `UnixTimestamp` renamed to `UnixTime` +- `UnixTime` seconds are now private and can be retrieved using the `secs` member method. +- `UnixTime::new` renamed to `UnixTime::new_checked`. +- `UnixTime` now has a nanosecond subsecond precision. The `new` constructor now expects + nanoseconds as the second argument. +- Added new `UnixTime::new_subsec_millis` and `UnixTime::new_subsec_millis_checked` API + to still allow creating a timestamp with only millisecond subsecond resolution. +- `CcsdsTimeProvider` now has a new `subsec_nanos` method in addition to a default + implementation for the `subsec_millis` method. +- `CcsdsTimeProvider::date_time` renamed to `CcsdsTimeProvider::chrono_date_time`. +- Added `UnixTime::MIN`, `UnixTime::MAX` and `UnixTime::EPOCH`. +- Added `UnixTime::timelib_date_time`. # [v0.11.0-rc.0] 2024-03-04 diff --git a/Cargo.toml b/Cargo.toml index c1273d4..c0d6a8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,22 +34,31 @@ optional = true default-features = false features = ["derive"] +[dependencies.time] +version = "0.3" +default-features = false +optional = true + [dependencies.chrono] version = "0.4" default-features = false +optional = true [dependencies.num-traits] version = "0.2" default-features = false -[dev-dependencies.postcard] -version = "1" +[dev-dependencies] +postcard = "1" +chrono = "0.4" [features] default = ["std"] std = ["chrono/std", "chrono/clock", "alloc", "thiserror"] serde = ["dep:serde", "chrono/serde"] alloc = ["postcard/alloc", "chrono/alloc"] +chrono = ["dep:chrono"] +timelib = ["dep:time"] [package.metadata.docs.rs] all-features = true diff --git a/README.md b/README.md index e2551ca..c136b6a 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ deserializing them with an appropriate `serde` provider like ## Optional Features - [`serde`](https://serde.rs/): Adds `serde` support for most types by adding `Serialize` and `Deserialize` `derive`s + - [`chrono`](https://crates.io/crates/chrono): Add basic support for the `chrono` time library. + - [`timelib`](https://crates.io/crates/time): Add basic support for the `time` time library. # Examples diff --git a/src/ecss/tm.rs b/src/ecss/tm.rs index fedfb0e..837a6dc 100644 --- a/src/ecss/tm.rs +++ b/src/ecss/tm.rs @@ -1097,7 +1097,7 @@ mod tests { use super::*; use crate::ecss::PusVersion::PusC; - use crate::time::cds::TimeProvider; + use crate::time::cds::CdsTime; #[cfg(feature = "serde")] use crate::time::CcsdsTimeProvider; use crate::SpHeader; @@ -1136,7 +1136,7 @@ mod tests { #[test] fn test_basic_simple_api() { let mut sph = SpHeader::tm_unseg(0x123, 0x234, 0).unwrap(); - let time_provider = TimeProvider::new_with_u16_days(0, 0); + let time_provider = CdsTime::new_with_u16_days(0, 0); let mut stamp_buf: [u8; 8] = [0; 8]; let pus_tm = PusTmCreator::new_simple(&mut sph, 17, 2, &time_provider, &mut stamp_buf, None, true) @@ -1534,7 +1534,7 @@ mod tests { #[cfg(feature = "serde")] fn test_serialization_creator_serde() { let mut sph = SpHeader::tm_unseg(0x123, 0x234, 0).unwrap(); - let time_provider = TimeProvider::new_with_u16_days(0, 0); + let time_provider = CdsTime::new_with_u16_days(0, 0); let mut stamp_buf: [u8; 8] = [0; 8]; let pus_tm = PusTmCreator::new_simple(&mut sph, 17, 2, &time_provider, &mut stamp_buf, None, true) @@ -1549,7 +1549,7 @@ mod tests { #[cfg(feature = "serde")] fn test_serialization_reader_serde() { let mut sph = SpHeader::tm_unseg(0x123, 0x234, 0).unwrap(); - let time_provider = TimeProvider::new_with_u16_days(0, 0); + let time_provider = CdsTime::new_with_u16_days(0, 0); let mut stamp_buf: [u8; 8] = [0; 8]; let pus_tm = PusTmCreator::new_simple(&mut sph, 17, 2, &time_provider, &mut stamp_buf, None, true) diff --git a/src/time/ascii.rs b/src/time/ascii.rs index ad0a210..c749c49 100644 --- a/src/time/ascii.rs +++ b/src/time/ascii.rs @@ -2,11 +2,8 @@ //! [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) section 3.5 . //! See [chrono::DateTime::format] for a usage example of the generated //! [chrono::format::DelayedFormat] structs. -#[cfg(feature = "alloc")] -use chrono::{ - format::{DelayedFormat, StrftimeItems}, - DateTime, Utc, -}; +#[cfg(all(feature = "alloc", feature = "chrono"))] +pub use alloc_mod_chrono::*; /// Tuple of format string and formatted size for time code A. /// @@ -34,36 +31,41 @@ pub const FMT_STR_CODE_B_WITH_SIZE: (&str, usize) = ("%Y-%jT%T%.3f", 21); /// Three digits are used for the decimal fraction and a terminator is added at the end. pub const FMT_STR_CODE_B_TERMINATED_WITH_SIZE: (&str, usize) = ("%Y-%jT%T%.3fZ", 22); -/// Generates a time code formatter using the [FMT_STR_CODE_A_WITH_SIZE] format. -#[cfg(feature = "alloc")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] -pub fn generate_time_code_a(date: &DateTime) -> DelayedFormat> { - date.format(FMT_STR_CODE_A_WITH_SIZE.0) -} +#[cfg(all(feature = "alloc", feature = "chrono"))] +pub mod alloc_mod_chrono { + use super::*; + use chrono::{ + format::{DelayedFormat, StrftimeItems}, + DateTime, Utc, + }; -/// Generates a time code formatter using the [FMT_STR_CODE_A_TERMINATED_WITH_SIZE] format. -#[cfg(feature = "alloc")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] -pub fn generate_time_code_a_terminated( - date: &DateTime, -) -> DelayedFormat> { - date.format(FMT_STR_CODE_A_TERMINATED_WITH_SIZE.0) -} + /// Generates a time code formatter using the [FMT_STR_CODE_A_WITH_SIZE] format. + #[cfg_attr(doc_cfg, doc(cfg(all(feature = "alloc", feature = "chrono"))))] + pub fn generate_time_code_a(date: &DateTime) -> DelayedFormat> { + date.format(FMT_STR_CODE_A_WITH_SIZE.0) + } -/// Generates a time code formatter using the [FMT_STR_CODE_B_WITH_SIZE] format. -#[cfg(feature = "alloc")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] -pub fn generate_time_code_b(date: &DateTime) -> DelayedFormat> { - date.format(FMT_STR_CODE_B_WITH_SIZE.0) -} + /// Generates a time code formatter using the [FMT_STR_CODE_A_TERMINATED_WITH_SIZE] format. + #[cfg_attr(doc_cfg, doc(cfg(all(feature = "alloc", feature = "chrono"))))] + pub fn generate_time_code_a_terminated( + date: &DateTime, + ) -> DelayedFormat> { + date.format(FMT_STR_CODE_A_TERMINATED_WITH_SIZE.0) + } -/// Generates a time code formatter using the [FMT_STR_CODE_B_TERMINATED_WITH_SIZE] format. -#[cfg(feature = "alloc")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] -pub fn generate_time_code_b_terminated( - date: &DateTime, -) -> DelayedFormat> { - date.format(FMT_STR_CODE_B_TERMINATED_WITH_SIZE.0) + /// Generates a time code formatter using the [FMT_STR_CODE_B_WITH_SIZE] format. + #[cfg_attr(doc_cfg, doc(cfg(all(feature = "alloc", feature = "chrono"))))] + pub fn generate_time_code_b(date: &DateTime) -> DelayedFormat> { + date.format(FMT_STR_CODE_B_WITH_SIZE.0) + } + + /// Generates a time code formatter using the [FMT_STR_CODE_B_TERMINATED_WITH_SIZE] format. + #[cfg_attr(doc_cfg, doc(cfg(all(feature = "alloc", feature = "chrono"))))] + pub fn generate_time_code_b_terminated( + date: &DateTime, + ) -> DelayedFormat> { + date.format(FMT_STR_CODE_B_TERMINATED_WITH_SIZE.0) + } } #[cfg(test)] @@ -77,7 +79,7 @@ mod tests { let date = Utc::now(); let stamp_formatter = generate_time_code_a(&date); let stamp = format!("{}", stamp_formatter); - let t_sep = stamp.find("T"); + let t_sep = stamp.find('T'); assert!(t_sep.is_some()); assert_eq!(t_sep.unwrap(), 10); assert_eq!(stamp.len(), FMT_STR_CODE_A_WITH_SIZE.1); @@ -88,10 +90,10 @@ mod tests { let date = Utc::now(); let stamp_formatter = generate_time_code_a_terminated(&date); let stamp = format!("{}", stamp_formatter); - let t_sep = stamp.find("T"); + let t_sep = stamp.find('T'); assert!(t_sep.is_some()); assert_eq!(t_sep.unwrap(), 10); - let z_terminator = stamp.find("Z"); + let z_terminator = stamp.find('Z'); assert!(z_terminator.is_some()); assert_eq!( z_terminator.unwrap(), @@ -105,7 +107,7 @@ mod tests { let date = Utc::now(); let stamp_formatter = generate_time_code_b(&date); let stamp = format!("{}", stamp_formatter); - let t_sep = stamp.find("T"); + let t_sep = stamp.find('T'); assert!(t_sep.is_some()); assert_eq!(t_sep.unwrap(), 8); assert_eq!(stamp.len(), FMT_STR_CODE_B_WITH_SIZE.1); @@ -116,10 +118,10 @@ mod tests { let date = Utc::now(); let stamp_formatter = generate_time_code_b_terminated(&date); let stamp = format!("{}", stamp_formatter); - let t_sep = stamp.find("T"); + let t_sep = stamp.find('T'); assert!(t_sep.is_some()); assert_eq!(t_sep.unwrap(), 8); - let z_terminator = stamp.find("Z"); + let z_terminator = stamp.find('Z'); assert!(z_terminator.is_some()); assert_eq!( z_terminator.unwrap(), diff --git a/src/time/cds.rs b/src/time/cds.rs index 52bf7cf..120d27a 100644 --- a/src/time/cds.rs +++ b/src/time/cds.rs @@ -1,24 +1,45 @@ //! Module to generate or read CCSDS Day Segmented (CDS) timestamps as specified in //! [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) section 3.3 . //! -//! The core data structure to do this is the [TimeProvider] struct and the +//! The core data structure to do this is the [CdsTime] struct and the //! [get_dyn_time_provider_from_bytes] function to retrieve correct instances of the //! struct from a bytestream. -use super::*; use crate::private::Sealed; -#[cfg(feature = "alloc")] -use alloc::boxed::Box; -use chrono::Datelike; -#[cfg(feature = "alloc")] -use core::any::Any; +use crate::ByteConversionError; use core::cmp::Ordering; -use core::fmt::Debug; +use core::fmt::{Debug, Display, Formatter}; use core::ops::{Add, AddAssign}; use core::time::Duration; + use delegate::delegate; +#[cfg(feature = "std")] +use super::StdTimestampError; +#[cfg(feature = "std")] +use std::error::Error; +#[cfg(feature = "std")] +use std::time::{SystemTime, SystemTimeError}; + +#[cfg(feature = "chrono")] +use chrono::Datelike; + +#[cfg(feature = "alloc")] +use super::ccsds_time_code_from_p_field; +#[cfg(feature = "alloc")] +use alloc::boxed::Box; +#[cfg(feature = "alloc")] +use core::any::Any; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use super::{ + ccsds_to_unix_days, unix_to_ccsds_days, CcsdsTimeCode, CcsdsTimeProvider, TimeReader, + TimeWriter, TimestampError, UnixTime, MS_PER_DAY, SECONDS_PER_DAY, +}; + /// Base value for the preamble field for a time field parser to determine the time field type. -pub const P_FIELD_BASE: u8 = (CcsdsTimeCodes::Cds as u8) << 4; +pub const P_FIELD_BASE: u8 = (CcsdsTimeCode::Cds as u8) << 4; pub const MIN_CDS_FIELD_LEN: usize = 7; pub const MAX_DAYS_24_BITS: u32 = 2_u32.pow(24) - 1; @@ -37,7 +58,7 @@ pub trait ProvidesDaysLength: Sealed + Clone { + Into; } -/// Type level token to be used as a generic parameter to [TimeProvider]. +/// Type level token to be used as a generic parameter to [CdsTime]. #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] pub struct DaysLen16Bits {} @@ -46,7 +67,7 @@ impl ProvidesDaysLength for DaysLen16Bits { type FieldType = u16; } -/// Type level token to be used as a generic parameter to [TimeProvider]. +/// Type level token to be used as a generic parameter to [CdsTime]. #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] pub struct DaysLen24Bits {} impl Sealed for DaysLen24Bits {} @@ -135,40 +156,39 @@ pub fn precision_from_pfield(pfield: u8) -> SubmillisPrecision { /// /// ``` /// use core::time::Duration; -/// use spacepackets::time::cds::{TimeProvider, length_of_day_segment_from_pfield, LengthOfDaySegment}; -/// use spacepackets::time::{TimeWriter, CcsdsTimeCodes, CcsdsTimeProvider}; +/// use spacepackets::time::cds::{CdsTime, length_of_day_segment_from_pfield, LengthOfDaySegment}; +/// use spacepackets::time::{TimeWriter, CcsdsTimeCode, CcsdsTimeProvider}; /// -/// let timestamp_now = TimeProvider::from_now_with_u16_days().unwrap(); +/// let timestamp_now = CdsTime::from_now_with_u16_days().unwrap(); /// let mut raw_stamp = [0; 7]; /// { /// let written = timestamp_now.write_to_bytes(&mut raw_stamp).unwrap(); -/// assert_eq!((raw_stamp[0] >> 4) & 0b111, CcsdsTimeCodes::Cds as u8); +/// assert_eq!((raw_stamp[0] >> 4) & 0b111, CcsdsTimeCode::Cds as u8); /// assert_eq!(written, 7); /// } /// { /// assert_eq!(length_of_day_segment_from_pfield(raw_stamp[0]), LengthOfDaySegment::Short16Bits); -/// let read_result = TimeProvider::from_bytes_with_u16_days(&raw_stamp); +/// let read_result = CdsTime::from_bytes_with_u16_days(&raw_stamp); /// assert!(read_result.is_ok()); /// let stamp_deserialized = read_result.unwrap(); /// assert_eq!(stamp_deserialized.len_as_bytes(), 7); /// } /// // It is possible to add a Duration offset to a timestamp provider. Add 5 minutes offset here /// let offset = Duration::from_secs(60 * 5); -/// let former_unix_seconds = timestamp_now.unix_seconds(); +/// let former_unix_seconds = timestamp_now.unix_secs(); /// let timestamp_in_5_minutes = timestamp_now + offset; -/// assert_eq!(timestamp_in_5_minutes.unix_seconds(), former_unix_seconds + 5 * 60); +/// assert_eq!(timestamp_in_5_minutes.unix_secs(), former_unix_seconds + 5 * 60); /// ``` #[derive(Debug, Copy, Clone, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct TimeProvider { +pub struct CdsTime { pfield: u8, ccsds_days: DaysLen::FieldType, ms_of_day: u32, - // submillis_precision: SubmillisPrecision, submillis: u32, /// This is not strictly necessary but still cached because it significantly simplifies the /// calculation of [`DateTime`]. - unix_stamp: UnixTimestamp, + unix_time: UnixTime, } /// Common properties for all CDS time providers. @@ -194,36 +214,47 @@ trait CdsConverter: CdsCommon { struct ConversionFromUnix { ccsds_days: u32, ms_of_day: u32, + submilis_prec: SubmillisPrecision, + submillis: u32, /// This is a side-product of the calculation of the CCSDS days. It is useful for /// re-calculating the datetime at a later point and therefore supplied as well. unix_days_seconds: i64, } impl ConversionFromUnix { - fn new(unix_seconds: i64, subsec_millis: u32) -> Result { + fn new( + unix_seconds: i64, + subsec_nanos: u32, + precision: SubmillisPrecision, + ) -> Result { let (unix_days, secs_of_day) = calc_unix_days_and_secs_of_day(unix_seconds); let ccsds_days = unix_to_ccsds_days(unix_days); - if ccsds_days == 0 && (secs_of_day > 0 || subsec_millis > 0) || ccsds_days < 0 { - let millis = if unix_seconds < 0 { - unix_seconds * 1000 - subsec_millis as i64 - } else { - unix_seconds * 1000 + subsec_millis as i64 - }; + if ccsds_days == 0 && (secs_of_day > 0 || subsec_nanos > 0) || ccsds_days < 0 { return Err(TimestampError::DateBeforeCcsdsEpoch( - Utc.timestamp_millis_opt(millis).unwrap(), + UnixTime::new_checked(unix_seconds, subsec_nanos) + .expect("unix timestamp creation failed"), )); } + let ms_of_day = secs_of_day * 1000 + subsec_nanos / 10_u32.pow(6); + + let submillis = match precision { + SubmillisPrecision::Microseconds => (subsec_nanos / 1_000) % 1000, + SubmillisPrecision::Picoseconds => (subsec_nanos % 10_u32.pow(6)) * 1000, + _ => 0, + }; Ok(Self { ccsds_days: unix_to_ccsds_days(unix_days) as u32, - ms_of_day: secs_of_day * 1000 + subsec_millis, + ms_of_day, unix_days_seconds: unix_days * SECONDS_PER_DAY as i64, + submilis_prec: precision, + submillis, }) } } impl CdsCommon for ConversionFromUnix { fn submillis_precision(&self) -> SubmillisPrecision { - SubmillisPrecision::Absent + self.submilis_prec } fn ms_of_day(&self) -> u32 { @@ -235,7 +266,7 @@ impl CdsCommon for ConversionFromUnix { } fn submillis(&self) -> u32 { - 0 + self.submillis } } @@ -245,13 +276,13 @@ impl CdsConverter for ConversionFromUnix { } } /// Helper struct which generates fields for the CDS time provider from a datetime. -struct ConversionFromDatetime { +struct ConversionFromChronoDatetime { unix_conversion: ConversionFromUnix, submillis_prec: SubmillisPrecision, submillis: u32, } -impl CdsCommon for ConversionFromDatetime { +impl CdsCommon for ConversionFromChronoDatetime { fn submillis_precision(&self) -> SubmillisPrecision { self.submillis_prec } @@ -268,7 +299,7 @@ impl CdsCommon for ConversionFromDatetime { } } -impl CdsConverter for ConversionFromDatetime { +impl CdsConverter for ConversionFromChronoDatetime { delegate! {to self.unix_conversion { fn unix_days_seconds(&self) -> i64; }} } @@ -285,27 +316,36 @@ fn calc_unix_days_and_secs_of_day(unix_seconds: i64) -> (i64, u32) { (unix_days, secs_of_day as u32) } -impl ConversionFromDatetime { - fn new(dt: &DateTime) -> Result { +#[cfg(feature = "chrono")] +impl ConversionFromChronoDatetime { + fn new(dt: &chrono::DateTime) -> Result { Self::new_generic(dt, SubmillisPrecision::Absent) } - fn new_with_submillis_us_prec(dt: &DateTime) -> Result { + fn new_with_submillis_us_prec( + dt: &chrono::DateTime, + ) -> Result { Self::new_generic(dt, SubmillisPrecision::Microseconds) } - fn new_with_submillis_ps_prec(dt: &DateTime) -> Result { + fn new_with_submillis_ps_prec( + dt: &chrono::DateTime, + ) -> Result { Self::new_generic(dt, SubmillisPrecision::Picoseconds) } - fn new_generic(dt: &DateTime, prec: SubmillisPrecision) -> Result { + #[cfg(feature = "chrono")] + fn new_generic( + dt: &chrono::DateTime, + prec: SubmillisPrecision, + ) -> Result { // The CDS timestamp does not support timestamps before the CCSDS epoch. if dt.year() < 1958 { - return Err(TimestampError::DateBeforeCcsdsEpoch(*dt)); + return Err(TimestampError::DateBeforeCcsdsEpoch(UnixTime::from(*dt))); } // The contained values in the conversion should be all positive now let unix_conversion = - ConversionFromUnix::new(dt.timestamp(), dt.timestamp_subsec_millis())?; + ConversionFromUnix::new(dt.timestamp(), dt.timestamp_subsec_nanos(), prec)?; let mut submillis = 0; match prec { SubmillisPrecision::Microseconds => { @@ -350,7 +390,8 @@ impl ConversionFromNow { let epoch = now.as_secs(); // This should always return a value with valid (non-negative) CCSDS days, // so it is okay to unwrap - let unix_conversion = ConversionFromUnix::new(epoch as i64, now.subsec_millis()).unwrap(); + let unix_conversion = + ConversionFromUnix::new(epoch as i64, now.subsec_nanos(), prec).unwrap(); let mut submillis = 0; match prec { @@ -397,27 +438,27 @@ impl CdsConverter for ConversionFromNow { pub trait DynCdsTimeProvider: CcsdsTimeProvider + CdsTimestamp + TimeWriter + Any {} #[cfg(feature = "alloc")] #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] -impl DynCdsTimeProvider for TimeProvider {} +impl DynCdsTimeProvider for CdsTime {} #[cfg(feature = "alloc")] #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] -impl DynCdsTimeProvider for TimeProvider {} +impl DynCdsTimeProvider for CdsTime {} -/// This function returns the correct [TimeProvider] instance from a raw byte array +/// This function returns the correct [CdsTime] instance from a raw byte array /// by checking the length of days field. It also checks the CCSDS time code for correctness. /// /// # Example /// /// ``` /// use spacepackets::time::cds::{ -/// TimeProvider, LengthOfDaySegment, get_dyn_time_provider_from_bytes, SubmillisPrecision, +/// CdsTime, LengthOfDaySegment, get_dyn_time_provider_from_bytes, SubmillisPrecision, /// }; -/// use spacepackets::time::{TimeWriter, CcsdsTimeCodes, CcsdsTimeProvider}; +/// use spacepackets::time::{TimeWriter, CcsdsTimeCode, CcsdsTimeProvider}; /// -/// let timestamp_now = TimeProvider::new_with_u16_days(24, 24); +/// let timestamp_now = CdsTime::new_with_u16_days(24, 24); /// let mut raw_stamp = [0; 7]; /// { /// let written = timestamp_now.write_to_bytes(&mut raw_stamp).unwrap(); -/// assert_eq!((raw_stamp[0] >> 4) & 0b111, CcsdsTimeCodes::Cds as u8); +/// assert_eq!((raw_stamp[0] >> 4) & 0b111, CcsdsTimeCode::Cds as u8); /// assert_eq!(written, 7); /// } /// { @@ -436,25 +477,25 @@ pub fn get_dyn_time_provider_from_bytes( let time_code = ccsds_time_code_from_p_field(buf[0]); if let Err(e) = time_code { return Err(TimestampError::InvalidTimeCode { - expected: CcsdsTimeCodes::Cds, + expected: CcsdsTimeCode::Cds, found: e, }); } let time_code = time_code.unwrap(); - if time_code != CcsdsTimeCodes::Cds { + if time_code != CcsdsTimeCode::Cds { return Err(TimestampError::InvalidTimeCode { - expected: CcsdsTimeCodes::Cds, + expected: CcsdsTimeCode::Cds, found: time_code as u8, }); } if length_of_day_segment_from_pfield(buf[0]) == LengthOfDaySegment::Short16Bits { - Ok(Box::new(TimeProvider::from_bytes_with_u16_days(buf)?)) + Ok(Box::new(CdsTime::from_bytes_with_u16_days(buf)?)) } else { - Ok(Box::new(TimeProvider::from_bytes_with_u24_days(buf)?)) + Ok(Box::new(CdsTime::from_bytes_with_u24_days(buf)?)) } } -impl CdsCommon for TimeProvider { +impl CdsCommon for CdsTime { fn submillis_precision(&self) -> SubmillisPrecision { precision_from_pfield(self.pfield) } @@ -472,7 +513,7 @@ impl CdsCommon for TimeProvider TimeProvider { +impl CdsTime { /// Please note that a precision value of 0 will be converted to [None] (no precision). pub fn set_submillis(&mut self, prec: SubmillisPrecision, value: u32) -> bool { self.pfield &= !(0b11); @@ -531,19 +572,19 @@ impl TimeProvider { )); } let pfield = buf[0]; - match CcsdsTimeCodes::try_from(pfield >> 4 & 0b111) { + match CcsdsTimeCode::try_from(pfield >> 4 & 0b111) { Ok(cds_type) => match cds_type { - CcsdsTimeCodes::Cds => (), + CcsdsTimeCode::Cds => (), _ => { return Err(TimestampError::InvalidTimeCode { - expected: CcsdsTimeCodes::Cds, + expected: CcsdsTimeCode::Cds, found: cds_type as u8, }) } }, _ => { return Err(TimestampError::InvalidTimeCode { - expected: CcsdsTimeCodes::Cds, + expected: CcsdsTimeCode::Cds, found: pfield >> 4 & 0b111, }); } @@ -596,20 +637,11 @@ impl TimeProvider { } else { unix_days_seconds += seconds_of_day; } - self.unix_stamp = UnixTimestamp::const_new(unix_days_seconds, (ms_of_day % 1000) as u16); - } - - fn calc_date_time(&self, ns_since_last_second: u32) -> Option> { - assert!( - ns_since_last_second < 10_u32.pow(9), - "Invalid MS since last second" - ); - if let LocalResult::Single(val) = - Utc.timestamp_opt(self.unix_stamp.unix_seconds, ns_since_last_second) - { - return Some(val); + let mut subsec_nanos = (ms_of_day % 1000) * 10_u32.pow(6); + if let Some(precision) = self.precision_as_ns() { + subsec_nanos += precision; } - None + self.unix_time = UnixTime::new(unix_days_seconds, subsec_nanos); } fn length_check(&self, buf: &[u8], len_as_bytes: usize) -> Result<(), TimestampError> { @@ -636,7 +668,7 @@ impl TimeProvider { pfield: Self::generate_p_field(days_len, SubmillisPrecision::Absent), ccsds_days, ms_of_day, - unix_stamp: Default::default(), + unix_time: Default::default(), submillis: 0, }; let unix_days_seconds = ccsds_to_unix_days(i64::from(ccsds_days)) * SECONDS_PER_DAY as i64; @@ -644,36 +676,40 @@ impl TimeProvider { Ok(provider) } + #[cfg(feature = "chrono")] fn from_dt_generic( - dt: &DateTime, + dt: &chrono::DateTime, days_len: LengthOfDaySegment, ) -> Result { - let conv_from_dt = ConversionFromDatetime::new(dt)?; + let conv_from_dt = ConversionFromChronoDatetime::new(dt)?; Self::generic_from_conversion(days_len, conv_from_dt) } + #[cfg(feature = "chrono")] fn from_dt_generic_us_prec( - dt: &DateTime, + dt: &chrono::DateTime, days_len: LengthOfDaySegment, ) -> Result { - let conv_from_dt = ConversionFromDatetime::new_with_submillis_us_prec(dt)?; + let conv_from_dt = ConversionFromChronoDatetime::new_with_submillis_us_prec(dt)?; Self::generic_from_conversion(days_len, conv_from_dt) } + #[cfg(feature = "chrono")] fn from_dt_generic_ps_prec( - dt: &DateTime, + dt: &chrono::DateTime, days_len: LengthOfDaySegment, ) -> Result { - let conv_from_dt = ConversionFromDatetime::new_with_submillis_ps_prec(dt)?; + let conv_from_dt = ConversionFromChronoDatetime::new_with_submillis_ps_prec(dt)?; Self::generic_from_conversion(days_len, conv_from_dt) } fn from_unix_generic( - unix_stamp: &UnixTimestamp, + unix_stamp: &UnixTime, days_len: LengthOfDaySegment, + submillis_prec: SubmillisPrecision, ) -> Result { let conv_from_dt = - ConversionFromUnix::new(unix_stamp.unix_seconds, unix_stamp.subsecond_millis as u32)?; + ConversionFromUnix::new(unix_stamp.secs, unix_stamp.subsec_nanos, submillis_prec)?; Self::generic_from_conversion(days_len, conv_from_dt) } @@ -712,7 +748,7 @@ impl TimeProvider { pfield: Self::generate_p_field(days_len, converter.submillis_precision()), ccsds_days, ms_of_day: converter.ms_of_day(), - unix_stamp: Default::default(), + unix_time: Default::default(), submillis: converter.submillis(), }; provider.setup(converter.unix_days_seconds(), converter.ms_of_day()); @@ -765,7 +801,7 @@ impl TimeProvider { } } -impl TimeProvider { +impl CdsTime { /// Generate a new timestamp provider with the days field width set to 24 bits pub fn new_with_u24_days(ccsds_days: u32, ms_of_day: u32) -> Result { if ccsds_days > MAX_DAYS_24_BITS { @@ -781,14 +817,17 @@ impl TimeProvider { Self::from_now_generic(LengthOfDaySegment::Long24Bits) } - /// Create a provider from a [`DateTime`] struct. + /// Create a provider from a [`chrono::DateTime`] struct. /// /// ## Errors /// /// This function will return [TimestampError::DateBeforeCcsdsEpoch] or /// [TimestampError::Cds] if the time is before the CCSDS epoch (1958-01-01T00:00:00+00:00) /// or the CCSDS days value exceeds the allowed bit width (24 bits). - pub fn from_dt_with_u24_days(dt: &DateTime) -> Result { + #[cfg(feature = "chrono")] + pub fn from_dt_with_u24_days( + dt: &chrono::DateTime, + ) -> Result { Self::from_dt_generic(dt, LengthOfDaySegment::Long24Bits) } @@ -799,19 +838,26 @@ impl TimeProvider { /// This function will return [TimestampError::DateBeforeCcsdsEpoch] or /// [TimestampError::Cds] if the time is before the CCSDS epoch (1958-01-01T00:00:00+00:00) /// or the CCSDS days value exceeds the allowed bit width (24 bits). - pub fn from_unix_secs_with_u24_days( - unix_stamp: &UnixTimestamp, + pub fn from_unix_stamp_with_u24_days( + unix_stamp: &UnixTime, + submillis_prec: SubmillisPrecision, ) -> Result { - Self::from_unix_generic(unix_stamp, LengthOfDaySegment::Long24Bits) + Self::from_unix_generic(unix_stamp, LengthOfDaySegment::Long24Bits, submillis_prec) } /// Like [Self::from_dt_with_u24_days] but with microsecond sub-millisecond precision. - pub fn from_dt_with_u24_days_us_precision(dt: &DateTime) -> Result { + #[cfg(feature = "chrono")] + pub fn from_dt_with_u24_days_us_precision( + dt: &chrono::DateTime, + ) -> Result { Self::from_dt_generic_us_prec(dt, LengthOfDaySegment::Long24Bits) } /// Like [Self::from_dt_with_u24_days] but with picoseconds sub-millisecond precision. - pub fn from_dt_with_u24_days_ps_precision(dt: &DateTime) -> Result { + #[cfg(feature = "chrono")] + pub fn from_dt_with_u24_days_ps_precision( + dt: &chrono::DateTime, + ) -> Result { Self::from_dt_generic_ps_prec(dt, LengthOfDaySegment::Long24Bits) } @@ -856,19 +902,22 @@ impl TimeProvider { } } -impl TimeProvider { +impl CdsTime { /// Generate a new timestamp provider with the days field width set to 16 bits pub fn new_with_u16_days(ccsds_days: u16, ms_of_day: u32) -> Self { // This should never fail, type system ensures CCSDS can not be negative or too large Self::generic_new(LengthOfDaySegment::Short16Bits, ccsds_days, ms_of_day).unwrap() } - /// Create a provider from a [`DateTime`] struct. + /// Create a provider from a [`chrono::DateTime`] struct. /// /// This function will return a [TimestampError::DateBeforeCcsdsEpoch] or a /// [TimestampError::Cds] if the time is before the CCSDS epoch (01-01-1958 00:00:00) or /// the CCSDS days value exceeds the allowed bit width (16 bits). - pub fn from_dt_with_u16_days(dt: &DateTime) -> Result { + #[cfg(feature = "chrono")] + pub fn from_dt_with_u16_days( + dt: &chrono::DateTime, + ) -> Result { Self::from_dt_generic(dt, LengthOfDaySegment::Short16Bits) } @@ -886,19 +935,26 @@ impl TimeProvider { /// This function will return [TimestampError::DateBeforeCcsdsEpoch] or /// [TimestampError::Cds] if the time is before the CCSDS epoch (1958-01-01T00:00:00+00:00) /// or the CCSDS days value exceeds the allowed bit width (24 bits). - pub fn from_unix_secs_with_u16_days( - unix_stamp: &UnixTimestamp, + pub fn from_unix_stamp_with_u16_days( + unix_stamp: &UnixTime, + submillis_prec: SubmillisPrecision, ) -> Result { - Self::from_unix_generic(unix_stamp, LengthOfDaySegment::Short16Bits) + Self::from_unix_generic(unix_stamp, LengthOfDaySegment::Short16Bits, submillis_prec) } /// Like [Self::from_dt_with_u16_days] but with microsecond sub-millisecond precision. - pub fn from_dt_with_u16_days_us_precision(dt: &DateTime) -> Result { + #[cfg(feature = "chrono")] + pub fn from_dt_with_u16_days_us_precision( + dt: &chrono::DateTime, + ) -> Result { Self::from_dt_generic_us_prec(dt, LengthOfDaySegment::Short16Bits) } /// Like [Self::from_dt_with_u16_days] but with picoseconds sub-millisecond precision. - pub fn from_dt_with_u16_days_ps_precision(dt: &DateTime) -> Result { + #[cfg(feature = "chrono")] + pub fn from_dt_with_u16_days_ps_precision( + dt: &chrono::DateTime, + ) -> Result { Self::from_dt_generic_ps_prec(dt, LengthOfDaySegment::Short16Bits) } @@ -944,7 +1000,7 @@ impl TimeProvider { } fn add_for_max_ccsds_days_val( - time_provider: &TimeProvider, + time_provider: &CdsTime, max_days_val: u32, duration: Duration, ) -> (u32, u32, u32) { @@ -1018,13 +1074,13 @@ fn add_for_max_ccsds_days_val( (next_ccsds_days, next_ms_of_day, submillis) } -impl CdsTimestamp for TimeProvider { +impl CdsTimestamp for CdsTime { fn len_of_day_seg(&self) -> LengthOfDaySegment { LengthOfDaySegment::Short16Bits } } -impl CdsTimestamp for TimeProvider { +impl CdsTimestamp for CdsTime { fn len_of_day_seg(&self) -> LengthOfDaySegment { LengthOfDaySegment::Long24Bits } @@ -1033,7 +1089,7 @@ impl CdsTimestamp for TimeProvider { /// Allows adding an duration in form of an offset. Please note that the CCSDS days will rollover /// when they overflow, because addition needs to be infallible. The user needs to check for a /// days overflow when this is a possibility and might be a problem. -impl Add for TimeProvider { +impl Add for CdsTime { type Output = Self; fn add(self, duration: Duration) -> Self::Output { @@ -1045,8 +1101,8 @@ impl Add for TimeProvider { } } -impl Add for &TimeProvider { - type Output = TimeProvider; +impl Add for &CdsTime { + type Output = CdsTime; fn add(self, duration: Duration) -> Self::Output { let (next_ccsds_days, next_ms_of_day, precision) = @@ -1060,7 +1116,7 @@ impl Add for &TimeProvider { /// Allows adding an duration in form of an offset. Please note that the CCSDS days will rollover /// when they overflow, because addition needs to be infallible. The user needs to check for a /// days overflow when this is a possibility and might be a problem. -impl Add for TimeProvider { +impl Add for CdsTime { type Output = Self; fn add(self, duration: Duration) -> Self::Output { @@ -1072,8 +1128,8 @@ impl Add for TimeProvider { } } -impl Add for &TimeProvider { - type Output = TimeProvider; +impl Add for &CdsTime { + type Output = CdsTime; fn add(self, duration: Duration) -> Self::Output { let (next_ccsds_days, next_ms_of_day, precision) = add_for_max_ccsds_days_val(self, MAX_DAYS_24_BITS, duration); @@ -1087,7 +1143,7 @@ impl Add for &TimeProvider { /// Allows adding an duration in form of an offset. Please note that the CCSDS days will rollover /// when they overflow, because addition needs to be infallible. The user needs to check for a /// days overflow when this is a possibility and might be a problem. -impl AddAssign for TimeProvider { +impl AddAssign for CdsTime { fn add_assign(&mut self, duration: Duration) { let (next_ccsds_days, next_ms_of_day, submillis) = add_for_max_ccsds_days_val(self, u16::MAX as u32, duration); @@ -1100,7 +1156,7 @@ impl AddAssign for TimeProvider { /// Allows adding an duration in form of an offset. Please note that the CCSDS days will rollover /// when they overflow, because addition needs to be infallible. The user needs to check for a /// days overflow when this is a possibility and might be a problem. -impl AddAssign for TimeProvider { +impl AddAssign for CdsTime { fn add_assign(&mut self, duration: Duration) { let (next_ccsds_days, next_ms_of_day, submillis) = add_for_max_ccsds_days_val(self, MAX_DAYS_24_BITS, duration); @@ -1110,25 +1166,27 @@ impl AddAssign for TimeProvider { } } -impl TryFrom> for TimeProvider { +#[cfg(feature = "chrono")] +impl TryFrom> for CdsTime { type Error = TimestampError; - fn try_from(dt: DateTime) -> Result { - let conversion = ConversionFromDatetime::new(&dt)?; + fn try_from(dt: chrono::DateTime) -> Result { + let conversion = ConversionFromChronoDatetime::new(&dt)?; Self::generic_from_conversion(LengthOfDaySegment::Short16Bits, conversion) } } -impl TryFrom> for TimeProvider { +#[cfg(feature = "chrono")] +impl TryFrom> for CdsTime { type Error = TimestampError; - fn try_from(dt: DateTime) -> Result { - let conversion = ConversionFromDatetime::new(&dt)?; + fn try_from(dt: chrono::DateTime) -> Result { + let conversion = ConversionFromChronoDatetime::new(&dt)?; Self::generic_from_conversion(LengthOfDaySegment::Long24Bits, conversion) } } -impl CcsdsTimeProvider for TimeProvider { +impl CcsdsTimeProvider for CdsTime { fn len_as_bytes(&self) -> usize { Self::calc_stamp_len(self.pfield) } @@ -1137,52 +1195,38 @@ impl CcsdsTimeProvider for TimeProvider CcsdsTimeCodes { - CcsdsTimeCodes::Cds + fn ccdsd_time_code(&self) -> CcsdsTimeCode { + CcsdsTimeCode::Cds } #[inline] - fn unix_seconds(&self) -> i64 { - self.unix_stamp.unix_seconds + fn unix_secs(&self) -> i64 { + self.unix_time.secs } #[inline] - fn subsecond_millis(&self) -> u16 { - self.unix_stamp.subsecond_millis + fn subsec_nanos(&self) -> u32 { + self.unix_time.subsec_nanos } #[inline] - fn unix_stamp(&self) -> UnixTimestamp { - self.unix_stamp - } - - fn date_time(&self) -> Option> { - let mut ns_since_last_sec = (self.ms_of_day % 1000) * 10_u32.pow(6); - match self.submillis_precision() { - SubmillisPrecision::Microseconds => { - ns_since_last_sec += self.submillis() * 1000; - } - SubmillisPrecision::Picoseconds => { - ns_since_last_sec += self.submillis() / 1000; - } - _ => (), - } - self.calc_date_time(ns_since_last_sec) + fn unix_time(&self) -> UnixTime { + self.unix_time } } -impl TimeReader for TimeProvider { +impl TimeReader for CdsTime { fn from_bytes(buf: &[u8]) -> Result { Self::from_bytes_with_u16_days(buf) } } -impl TimeReader for TimeProvider { +impl TimeReader for CdsTime { fn from_bytes(buf: &[u8]) -> Result { Self::from_bytes_with_u24_days(buf) } } -impl TimeWriter for TimeProvider { +impl TimeWriter for CdsTime { fn write_to_bytes(&self, buf: &mut [u8]) -> Result { self.length_check(buf, self.len_as_bytes())?; buf[0] = self.pfield; @@ -1205,7 +1249,7 @@ impl TimeWriter for TimeProvider { } } -impl TimeWriter for TimeProvider { +impl TimeWriter for CdsTime { fn write_to_bytes(&self, buf: &mut [u8]) -> Result { self.length_check(buf, self.len_as_bytes())?; buf[0] = self.pfield; @@ -1229,7 +1273,7 @@ impl TimeWriter for TimeProvider { } } -impl PartialEq for TimeProvider { +impl PartialEq for CdsTime { fn eq(&self, other: &Self) -> bool { if self.ccsds_days == other.ccsds_days && self.ms_of_day == other.ms_of_day @@ -1241,7 +1285,7 @@ impl PartialEq for TimeProvider PartialOrd for TimeProvider { +impl PartialOrd for CdsTime { fn partial_cmp(&self, other: &Self) -> Option { if self == other { return Some(Ordering::Equal); @@ -1269,14 +1313,14 @@ impl PartialOrd for TimeProvider Ord for TimeProvider { +impl Ord for CdsTime { fn cmp(&self, other: &Self) -> Ordering { PartialOrd::partial_cmp(self, other).unwrap() } } -impl From> for TimeProvider { - fn from(value: TimeProvider) -> Self { +impl From> for CdsTime { + fn from(value: CdsTime) -> Self { // This function only fails if the days value exceeds 24 bits, which is not possible here, // so it is okay to unwrap. Self::new_with_u24_days(value.ccsds_days_as_u32(), value.ms_of_day()).unwrap() @@ -1284,9 +1328,9 @@ impl From> for TimeProvider { } /// This conversion can fail if the days value exceeds 16 bits. -impl TryFrom> for TimeProvider { +impl TryFrom> for CdsTime { type Error = CdsError; - fn try_from(value: TimeProvider) -> Result { + fn try_from(value: CdsTime) -> Result { let ccsds_days = value.ccsds_days_as_u32(); if ccsds_days > u16::MAX as u32 { return Err(CdsError::InvalidCcsdsDays(ccsds_days as i64)); @@ -1302,6 +1346,7 @@ impl TryFrom> for TimeProvider { mod tests { use super::*; use crate::time::TimestampError::{ByteConversion, InvalidTimeCode}; + use crate::time::{UnixTime, DAYS_CCSDS_TO_UNIX, MS_PER_DAY}; use crate::ByteConversionError::{FromSliceTooSmall, ToSliceTooSmall}; use alloc::string::ToString; use chrono::{Datelike, NaiveDate, Timelike}; @@ -1311,25 +1356,25 @@ mod tests { #[test] fn test_time_stamp_zero_args() { - let time_stamper = TimeProvider::new_with_u16_days(0, 0); - let unix_stamp = time_stamper.unix_stamp(); + let time_stamper = CdsTime::new_with_u16_days(0, 0); + let unix_stamp = time_stamper.unix_time(); assert_eq!( - unix_stamp.unix_seconds, + unix_stamp.secs, (DAYS_CCSDS_TO_UNIX * SECONDS_PER_DAY as i32) as i64 ); - let subsecond_millis = unix_stamp.subsecond_millis; + let subsecond_millis = unix_stamp.subsec_nanos; assert_eq!(subsecond_millis, 0); assert_eq!( time_stamper.submillis_precision(), SubmillisPrecision::Absent ); - assert_eq!(time_stamper.subsecond_millis(), 0); - assert_eq!(time_stamper.ccdsd_time_code(), CcsdsTimeCodes::Cds); + assert_eq!(time_stamper.subsec_nanos(), 0); + assert_eq!(time_stamper.ccdsd_time_code(), CcsdsTimeCode::Cds); assert_eq!( time_stamper.p_field(), - (1, [(CcsdsTimeCodes::Cds as u8) << 4, 0]) + (1, [(CcsdsTimeCode::Cds as u8) << 4, 0]) ); - let date_time = time_stamper.date_time().unwrap(); + let date_time = time_stamper.chrono_date_time().unwrap(); assert_eq!(date_time.year(), 1958); assert_eq!(date_time.month(), 1); assert_eq!(date_time.day(), 1); @@ -1340,28 +1385,30 @@ mod tests { #[test] fn test_time_stamp_unix_epoch() { - let time_stamper = TimeProvider::new_with_u16_days((-DAYS_CCSDS_TO_UNIX) as u16, 0); - assert_eq!(time_stamper.unix_stamp().unix_seconds, 0); + let time_stamper = CdsTime::new_with_u16_days((-DAYS_CCSDS_TO_UNIX) as u16, 0); + assert_eq!(time_stamper.unix_time().secs, 0); assert_eq!( time_stamper.submillis_precision(), SubmillisPrecision::Absent ); - let date_time = time_stamper.date_time().unwrap(); + let date_time = time_stamper.chrono_date_time().unwrap(); assert_eq!(date_time.year(), 1970); assert_eq!(date_time.month(), 1); assert_eq!(date_time.day(), 1); assert_eq!(date_time.hour(), 0); assert_eq!(date_time.minute(), 0); assert_eq!(date_time.second(), 0); - let time_stamper = TimeProvider::new_with_u16_days((-DAYS_CCSDS_TO_UNIX) as u16, 40); - assert_eq!(time_stamper.subsecond_millis(), 40); - let time_stamper = TimeProvider::new_with_u16_days((-DAYS_CCSDS_TO_UNIX) as u16, 1040); - assert_eq!(time_stamper.subsecond_millis(), 40); + let time_stamper = CdsTime::new_with_u16_days((-DAYS_CCSDS_TO_UNIX) as u16, 40); + assert_eq!(time_stamper.subsec_nanos(), 40 * 10_u32.pow(6)); + assert_eq!(time_stamper.subsec_millis(), 40); + let time_stamper = CdsTime::new_with_u16_days((-DAYS_CCSDS_TO_UNIX) as u16, 1040); + assert_eq!(time_stamper.subsec_nanos(), 40 * 10_u32.pow(6)); + assert_eq!(time_stamper.subsec_millis(), 40); } #[test] fn test_large_days_field_write() { - let time_stamper = TimeProvider::new_with_u24_days(0x108020_u32, 0x10203040); + let time_stamper = CdsTime::new_with_u24_days(0x108020_u32, 0x10203040); assert!(time_stamper.is_ok()); let time_stamper = time_stamper.unwrap(); assert_eq!(time_stamper.len_as_bytes(), 8); @@ -1380,13 +1427,13 @@ mod tests { #[test] fn test_large_days_field_read() { - let time_stamper = TimeProvider::new_with_u24_days(0x108020_u32, 0); + let time_stamper = CdsTime::new_with_u24_days(0x108020_u32, 0); assert!(time_stamper.is_ok()); let time_stamper = time_stamper.unwrap(); let mut buf = [0; 16]; let written = time_stamper.write_to_bytes(&mut buf); assert!(written.is_ok()); - let provider = TimeProvider::::from_bytes(&buf); + let provider = CdsTime::::from_bytes(&buf); assert!(provider.is_ok()); let provider = provider.unwrap(); assert_eq!(provider.ccsds_days(), 0x108020); @@ -1395,13 +1442,13 @@ mod tests { #[test] fn test_large_days_field_read_invalid_ctor() { - let time_stamper = TimeProvider::new_with_u24_days(0x108020, 0); + let time_stamper = CdsTime::new_with_u24_days(0x108020, 0); assert!(time_stamper.is_ok()); let time_stamper = time_stamper.unwrap(); let mut buf = [0; 16]; let written = time_stamper.write_to_bytes(&mut buf); assert!(written.is_ok()); - let faulty_ctor = TimeProvider::::from_bytes(&buf); + let faulty_ctor = CdsTime::::from_bytes(&buf); assert!(faulty_ctor.is_err()); let error = faulty_ctor.unwrap_err(); if let TimestampError::Cds(CdsError::InvalidCtorForDaysOfLenInPreamble(len_of_day)) = error @@ -1415,15 +1462,15 @@ mod tests { #[test] fn test_write() { let mut buf = [0; 16]; - let time_stamper_0 = TimeProvider::new_with_u16_days(0, 0); - let unix_stamp = time_stamper_0.unix_stamp(); + let time_stamper_0 = CdsTime::new_with_u16_days(0, 0); + let unix_stamp = time_stamper_0.unix_time(); assert_eq!( - unix_stamp.unix_seconds, + unix_stamp.secs, (DAYS_CCSDS_TO_UNIX * SECONDS_PER_DAY as i32).into() ); let mut res = time_stamper_0.write_to_bytes(&mut buf); assert!(res.is_ok()); - assert_eq!(buf[0], (CcsdsTimeCodes::Cds as u8) << 4); + assert_eq!(buf[0], (CcsdsTimeCode::Cds as u8) << 4); assert_eq!( u16::from_be_bytes(buf[1..3].try_into().expect("Byte conversion failed")), 0 @@ -1432,10 +1479,10 @@ mod tests { u32::from_be_bytes(buf[3..7].try_into().expect("Byte conversion failed")), 0 ); - let time_stamper_1 = TimeProvider::new_with_u16_days(u16::MAX - 1, u32::MAX - 1); + let time_stamper_1 = CdsTime::new_with_u16_days(u16::MAX - 1, u32::MAX - 1); res = time_stamper_1.write_to_bytes(&mut buf); assert!(res.is_ok()); - assert_eq!(buf[0], (CcsdsTimeCodes::Cds as u8) << 4); + assert_eq!(buf[0], (CcsdsTimeCode::Cds as u8) << 4); assert_eq!( u16::from_be_bytes(buf[1..3].try_into().expect("Byte conversion failed")), u16::MAX - 1 @@ -1449,7 +1496,7 @@ mod tests { #[test] fn test_faulty_write_buf_too_small() { let mut buf = [0; 7]; - let time_stamper = TimeProvider::new_with_u16_days(u16::MAX - 1, u32::MAX - 1); + let time_stamper = CdsTime::new_with_u16_days(u16::MAX - 1, u32::MAX - 1); for i in 0..6 { let res = time_stamper.write_to_bytes(&mut buf[0..i]); assert!(res.is_err()); @@ -1475,7 +1522,7 @@ mod tests { fn test_faulty_read_buf_too_small() { let buf = [0; 7]; for i in 0..6 { - let res = TimeProvider::::from_bytes(&buf[0..i]); + let res = CdsTime::::from_bytes(&buf[0..i]); assert!(res.is_err()); let err = res.unwrap_err(); match err { @@ -1496,15 +1543,15 @@ mod tests { #[test] fn test_faulty_invalid_pfield() { let mut buf = [0; 16]; - let time_stamper_0 = TimeProvider::new_with_u16_days(0, 0); + let time_stamper_0 = CdsTime::new_with_u16_days(0, 0); let res = time_stamper_0.write_to_bytes(&mut buf); assert!(res.is_ok()); buf[0] = 0; - let res = TimeProvider::::from_bytes(&buf); + let res = CdsTime::::from_bytes(&buf); assert!(res.is_err()); let err = res.unwrap_err(); if let InvalidTimeCode { expected, found } = err { - assert_eq!(expected, CcsdsTimeCodes::Cds); + assert_eq!(expected, CcsdsTimeCode::Cds); assert_eq!(found, 0); assert_eq!( err.to_string(), @@ -1516,10 +1563,10 @@ mod tests { #[test] fn test_reading() { let mut buf = [0; 16]; - let time_stamper = TimeProvider::new_with_u16_days(u16::MAX - 1, u32::MAX - 1); + let time_stamper = CdsTime::new_with_u16_days(u16::MAX - 1, u32::MAX - 1); let res = time_stamper.write_to_bytes(&mut buf); assert!(res.is_ok()); - assert_eq!(buf[0], (CcsdsTimeCodes::Cds as u8) << 4); + assert_eq!(buf[0], (CcsdsTimeCode::Cds as u8) << 4); assert_eq!( u16::from_be_bytes(buf[1..3].try_into().expect("Byte conversion failed")), u16::MAX - 1 @@ -1529,17 +1576,17 @@ mod tests { u32::MAX - 1 ); - let read_stamp: TimeProvider = - TimeProvider::from_bytes(&buf).expect("Reading timestamp failed"); + let read_stamp: CdsTime = + CdsTime::from_bytes(&buf).expect("Reading timestamp failed"); assert_eq!(read_stamp.ccsds_days(), u16::MAX - 1); assert_eq!(read_stamp.ms_of_day(), u32::MAX - 1); } fn generic_now_test( - timestamp_now: TimeProvider, - compare_stamp: DateTime, + timestamp_now: CdsTime, + compare_stamp: chrono::DateTime, ) { - let dt = timestamp_now.date_time().unwrap(); + let dt = timestamp_now.chrono_date_time().unwrap(); if compare_stamp.year() > dt.year() { assert_eq!(compare_stamp.year() - dt.year(), 1); } else { @@ -1562,42 +1609,42 @@ mod tests { #[test] fn test_time_now() { - let timestamp_now = TimeProvider::from_now_with_u16_days().unwrap(); - let compare_stamp = Utc::now(); + let timestamp_now = CdsTime::from_now_with_u16_days().unwrap(); + let compare_stamp = chrono::Utc::now(); generic_now_test(timestamp_now, compare_stamp); } #[test] fn test_time_now_us_prec() { - let timestamp_now = TimeProvider::from_now_with_u16_days_us_precision().unwrap(); - let compare_stamp = Utc::now(); + let timestamp_now = CdsTime::from_now_with_u16_days_us_precision().unwrap(); + let compare_stamp = chrono::Utc::now(); generic_now_test(timestamp_now, compare_stamp); } #[test] fn test_time_now_ps_prec() { - let timestamp_now = TimeProvider::from_now_with_u16_days_ps_precision().unwrap(); - let compare_stamp = Utc::now(); + let timestamp_now = CdsTime::from_now_with_u16_days_ps_precision().unwrap(); + let compare_stamp = chrono::Utc::now(); generic_now_test(timestamp_now, compare_stamp); } #[test] fn test_time_now_ps_prec_u16_days() { - let timestamp_now = TimeProvider::from_now_with_u16_days_ps_precision().unwrap(); - let compare_stamp = Utc::now(); + let timestamp_now = CdsTime::from_now_with_u16_days_ps_precision().unwrap(); + let compare_stamp = chrono::Utc::now(); generic_now_test(timestamp_now, compare_stamp); } #[test] fn test_time_now_ps_prec_u24_days() { - let timestamp_now = TimeProvider::from_now_with_u24_days_ps_precision().unwrap(); - let compare_stamp = Utc::now(); + let timestamp_now = CdsTime::from_now_with_u24_days_ps_precision().unwrap(); + let compare_stamp = chrono::Utc::now(); generic_now_test(timestamp_now, compare_stamp); } #[test] fn test_submillis_precision_micros() { - let mut time_stamper = TimeProvider::new_with_u16_days(0, 0); + let mut time_stamper = CdsTime::new_with_u16_days(0, 0); time_stamper.set_submillis(SubmillisPrecision::Microseconds, 500); assert_eq!( time_stamper.submillis_precision(), @@ -1615,7 +1662,7 @@ mod tests { #[test] fn test_submillis_precision_picos() { - let mut time_stamper = TimeProvider::new_with_u16_days(0, 0); + let mut time_stamper = CdsTime::new_with_u16_days(0, 0); time_stamper.set_submillis(SubmillisPrecision::Picoseconds, 5e8 as u32); assert_eq!( time_stamper.submillis_precision(), @@ -1633,14 +1680,14 @@ mod tests { #[test] fn read_stamp_with_ps_submillis_precision() { - let mut time_stamper = TimeProvider::new_with_u16_days(0, 0); + let mut time_stamper = CdsTime::new_with_u16_days(0, 0); time_stamper.set_submillis(SubmillisPrecision::Picoseconds, 5e8 as u32); let mut write_buf: [u8; 16] = [0; 16]; let written = time_stamper .write_to_bytes(&mut write_buf) .expect("Writing timestamp failed"); assert_eq!(written, 11); - let stamp_deserialized = TimeProvider::::from_bytes(&write_buf); + let stamp_deserialized = CdsTime::::from_bytes(&write_buf); assert!(stamp_deserialized.is_ok()); let stamp_deserialized = stamp_deserialized.unwrap(); assert_eq!(stamp_deserialized.len_as_bytes(), 11); @@ -1653,14 +1700,14 @@ mod tests { #[test] fn read_stamp_with_us_submillis_precision() { - let mut time_stamper = TimeProvider::new_with_u16_days(0, 0); + let mut time_stamper = CdsTime::new_with_u16_days(0, 0); time_stamper.set_submillis(SubmillisPrecision::Microseconds, 500); let mut write_buf: [u8; 16] = [0; 16]; let written = time_stamper .write_to_bytes(&mut write_buf) .expect("Writing timestamp failed"); assert_eq!(written, 9); - let stamp_deserialized = TimeProvider::::from_bytes(&write_buf); + let stamp_deserialized = CdsTime::::from_bytes(&write_buf); assert!(stamp_deserialized.is_ok()); let stamp_deserialized = stamp_deserialized.unwrap(); assert_eq!(stamp_deserialized.len_as_bytes(), 9); @@ -1673,7 +1720,7 @@ mod tests { #[test] fn read_u24_stamp_with_us_submillis_precision() { - let mut time_stamper = TimeProvider::new_with_u24_days(u16::MAX as u32 + 1, 0).unwrap(); + let mut time_stamper = CdsTime::new_with_u24_days(u16::MAX as u32 + 1, 0).unwrap(); time_stamper.set_submillis(SubmillisPrecision::Microseconds, 500); let mut write_buf: [u8; 16] = [0; 16]; let written = time_stamper @@ -1681,7 +1728,7 @@ mod tests { .expect("Writing timestamp failed"); // 1 byte pfield + 3 bytes days + 4 bytes ms of day + 2 bytes us precision assert_eq!(written, 10); - let stamp_deserialized = TimeProvider::from_bytes_with_u24_days(&write_buf); + let stamp_deserialized = CdsTime::from_bytes_with_u24_days(&write_buf); assert!(stamp_deserialized.is_ok()); let stamp_deserialized = stamp_deserialized.unwrap(); assert_eq!(stamp_deserialized.len_as_bytes(), 10); @@ -1695,7 +1742,7 @@ mod tests { #[test] fn read_u24_stamp_with_ps_submillis_precision() { - let mut time_stamper = TimeProvider::new_with_u24_days(u16::MAX as u32 + 1, 0).unwrap(); + let mut time_stamper = CdsTime::new_with_u24_days(u16::MAX as u32 + 1, 0).unwrap(); time_stamper.set_submillis(SubmillisPrecision::Picoseconds, 5e8 as u32); let mut write_buf: [u8; 16] = [0; 16]; let written = time_stamper @@ -1703,7 +1750,7 @@ mod tests { .expect("Writing timestamp failed"); // 1 byte pfield + 3 bytes days + 4 bytes ms of day + 4 bytes us precision assert_eq!(written, 12); - let stamp_deserialized = TimeProvider::from_bytes_with_u24_days(&write_buf); + let stamp_deserialized = CdsTime::from_bytes_with_u24_days(&write_buf); assert!(stamp_deserialized.is_ok()); let stamp_deserialized = stamp_deserialized.unwrap(); assert_eq!(stamp_deserialized.len_as_bytes(), 12); @@ -1715,19 +1762,19 @@ mod tests { assert_eq!(stamp_deserialized.submillis(), 5e8 as u32); } - fn generic_dt_case_0_no_prec(subsec_millis: u32) -> DateTime { + fn generic_dt_case_0_no_prec(subsec_millis: u32) -> chrono::DateTime { NaiveDate::from_ymd_opt(2023, 1, 14) .unwrap() .and_hms_milli_opt(16, 49, 30, subsec_millis) .unwrap() - .and_local_timezone(Utc) + .and_local_timezone(chrono::Utc) .unwrap() } fn generic_check_dt_case_0( - time_provider: &TimeProvider, + time_provider: &CdsTime, subsec_millis: u32, - datetime_utc: DateTime, + datetime_utc: chrono::DateTime, ) { // https://www.timeanddate.com/date/durationresult.html?d1=01&m1=01&y1=1958&d2=14&m2=01&y2=2023 // Leap years need to be accounted for as well. @@ -1736,16 +1783,16 @@ mod tests { time_provider.ms_of_day, 30 * 1000 + 49 * 60 * 1000 + 16 * 60 * 60 * 1000 + subsec_millis ); - assert_eq!(time_provider.date_time().unwrap(), datetime_utc); + assert_eq!(time_provider.chrono_date_time().unwrap(), datetime_utc); } #[test] fn test_creation_from_dt_u16_days() { let subsec_millis = 250; let datetime_utc = generic_dt_case_0_no_prec(subsec_millis); - let time_provider = TimeProvider::from_dt_with_u16_days(&datetime_utc).unwrap(); + let time_provider = CdsTime::from_dt_with_u16_days(&datetime_utc).unwrap(); generic_check_dt_case_0(&time_provider, subsec_millis, datetime_utc); - let time_provider_2: TimeProvider = + let time_provider_2: CdsTime = datetime_utc.try_into().expect("conversion failed"); // Test the TryInto trait impl assert_eq!(time_provider, time_provider_2); @@ -1754,29 +1801,29 @@ mod tests { fn test_creation_from_dt_u24_days() { let subsec_millis = 250; let datetime_utc = generic_dt_case_0_no_prec(subsec_millis); - let time_provider = TimeProvider::from_dt_with_u24_days(&datetime_utc).unwrap(); + let time_provider = CdsTime::from_dt_with_u24_days(&datetime_utc).unwrap(); generic_check_dt_case_0(&time_provider, subsec_millis, datetime_utc); - let time_provider_2: TimeProvider = + let time_provider_2: CdsTime = datetime_utc.try_into().expect("conversion failed"); // Test the TryInto trait impl assert_eq!(time_provider, time_provider_2); } - fn generic_dt_case_1_us_prec(subsec_millis: u32) -> DateTime { + fn generic_dt_case_1_us_prec(subsec_millis: u32) -> chrono::DateTime { // 250 ms + 500 us let subsec_micros = subsec_millis * 1000 + 500; NaiveDate::from_ymd_opt(2023, 1, 14) .unwrap() .and_hms_micro_opt(16, 49, 30, subsec_micros) .unwrap() - .and_local_timezone(Utc) + .and_local_timezone(chrono::Utc) .unwrap() } fn generic_check_dt_case_1_us_prec( - time_provider: &TimeProvider, + time_provider: &CdsTime, subsec_millis: u32, - datetime_utc: DateTime, + datetime_utc: chrono::DateTime, ) { // https://www.timeanddate.com/date/durationresult.html?d1=01&m1=01&y1=1958&d2=14&m2=01&y2=2023 // Leap years need to be accounted for as well. @@ -1790,15 +1837,14 @@ mod tests { SubmillisPrecision::Microseconds ); assert_eq!(time_provider.submillis(), 500); - assert_eq!(time_provider.date_time().unwrap(), datetime_utc); + assert_eq!(time_provider.chrono_date_time().unwrap(), datetime_utc); } #[test] fn test_creation_from_dt_u16_days_us_prec() { let subsec_millis = 250; let datetime_utc = generic_dt_case_1_us_prec(subsec_millis); - let time_provider = - TimeProvider::from_dt_with_u16_days_us_precision(&datetime_utc).unwrap(); + let time_provider = CdsTime::from_dt_with_u16_days_us_precision(&datetime_utc).unwrap(); generic_check_dt_case_1_us_prec(&time_provider, subsec_millis, datetime_utc); } @@ -1806,12 +1852,11 @@ mod tests { fn test_creation_from_dt_u24_days_us_prec() { let subsec_millis = 250; let datetime_utc = generic_dt_case_1_us_prec(subsec_millis); - let time_provider = - TimeProvider::from_dt_with_u24_days_us_precision(&datetime_utc).unwrap(); + let time_provider = CdsTime::from_dt_with_u24_days_us_precision(&datetime_utc).unwrap(); generic_check_dt_case_1_us_prec(&time_provider, subsec_millis, datetime_utc); } - fn generic_dt_case_2_ps_prec(subsec_millis: u32) -> (DateTime, u32) { + fn generic_dt_case_2_ps_prec(subsec_millis: u32) -> (chrono::DateTime, u32) { // 250 ms + 500 us let subsec_nanos = subsec_millis * 1000 * 1000 + 500 * 1000; let submilli_nanos = subsec_nanos % 10_u32.pow(6); @@ -1820,17 +1865,17 @@ mod tests { .unwrap() .and_hms_nano_opt(16, 49, 30, subsec_nanos) .unwrap() - .and_local_timezone(Utc) + .and_local_timezone(chrono::Utc) .unwrap(), submilli_nanos, ) } fn generic_check_dt_case_2_ps_prec( - time_provider: &TimeProvider, + time_provider: &CdsTime, subsec_millis: u32, submilli_nanos: u32, - datetime_utc: DateTime, + datetime_utc: chrono::DateTime, ) { // https://www.timeanddate.com/date/durationresult.html?d1=01&m1=01&y1=1958&d2=14&m2=01&y2=2023 // Leap years need to be accounted for as well. @@ -1844,15 +1889,14 @@ mod tests { SubmillisPrecision::Picoseconds ); assert_eq!(time_provider.submillis(), submilli_nanos * 1000); - assert_eq!(time_provider.date_time().unwrap(), datetime_utc); + assert_eq!(time_provider.chrono_date_time().unwrap(), datetime_utc); } #[test] fn test_creation_from_dt_u16_days_ps_prec() { let subsec_millis = 250; let (datetime_utc, submilli_nanos) = generic_dt_case_2_ps_prec(subsec_millis); - let time_provider = - TimeProvider::from_dt_with_u16_days_ps_precision(&datetime_utc).unwrap(); + let time_provider = CdsTime::from_dt_with_u16_days_ps_precision(&datetime_utc).unwrap(); generic_check_dt_case_2_ps_prec( &time_provider, subsec_millis, @@ -1865,8 +1909,7 @@ mod tests { fn test_creation_from_dt_u24_days_ps_prec() { let subsec_millis = 250; let (datetime_utc, submilli_nanos) = generic_dt_case_2_ps_prec(subsec_millis); - let time_provider = - TimeProvider::from_dt_with_u24_days_ps_precision(&datetime_utc).unwrap(); + let time_provider = CdsTime::from_dt_with_u24_days_ps_precision(&datetime_utc).unwrap(); generic_check_dt_case_2_ps_prec( &time_provider, subsec_millis, @@ -1879,10 +1922,10 @@ mod tests { fn test_creation_from_unix_stamp_0_u16_days() { let unix_secs = 0; let subsec_millis = 0; - let time_provider = TimeProvider::from_unix_secs_with_u16_days(&UnixTimestamp::const_new( - unix_secs, - subsec_millis, - )) + let time_provider = CdsTime::from_unix_stamp_with_u16_days( + &UnixTime::new(unix_secs, subsec_millis), + SubmillisPrecision::Absent, + ) .expect("creating provider from unix stamp failed"); assert_eq!(time_provider.ccsds_days, -DAYS_CCSDS_TO_UNIX as u16) } @@ -1891,10 +1934,10 @@ mod tests { fn test_creation_from_unix_stamp_0_u24_days() { let unix_secs = 0; let subsec_millis = 0; - let time_provider = TimeProvider::from_unix_secs_with_u24_days(&UnixTimestamp::const_new( - unix_secs, - subsec_millis, - )) + let time_provider = CdsTime::from_unix_stamp_with_u24_days( + &UnixTime::new(unix_secs, subsec_millis), + SubmillisPrecision::Absent, + ) .expect("creating provider from unix stamp failed"); assert_eq!(time_provider.ccsds_days, (-DAYS_CCSDS_TO_UNIX) as u32) } @@ -1906,10 +1949,13 @@ mod tests { .unwrap() .and_hms_milli_opt(16, 49, 30, subsec_millis) .unwrap() - .and_local_timezone(Utc) + .and_local_timezone(chrono::Utc) .unwrap(); - let time_provider = TimeProvider::from_unix_secs_with_u16_days(&datetime_utc.into()) - .expect("creating provider from unix stamp failed"); + let time_provider = CdsTime::from_unix_stamp_with_u16_days( + &datetime_utc.into(), + SubmillisPrecision::Absent, + ) + .expect("creating provider from unix stamp failed"); // https://www.timeanddate.com/date/durationresult.html?d1=01&m1=01&y1=1958&d2=14&m2=01&y2=2023 // Leap years need to be accounted for as well. assert_eq!(time_provider.ccsds_days, 23754); @@ -1917,7 +1963,7 @@ mod tests { time_provider.ms_of_day, 30 * 1000 + 49 * 60 * 1000 + 16 * 60 * 60 * 1000 + subsec_millis ); - let dt_back = time_provider.date_time().unwrap(); + let dt_back = time_provider.chrono_date_time().unwrap(); assert_eq!(datetime_utc, dt_back); } @@ -1925,10 +1971,10 @@ mod tests { fn test_creation_0_ccsds_days() { let unix_secs = DAYS_CCSDS_TO_UNIX as i64 * SECONDS_PER_DAY as i64; let subsec_millis = 0; - let time_provider = TimeProvider::from_unix_secs_with_u16_days(&UnixTimestamp::const_new( - unix_secs, - subsec_millis, - )) + let time_provider = CdsTime::from_unix_stamp_with_u16_days( + &UnixTime::new(unix_secs, subsec_millis), + SubmillisPrecision::Absent, + ) .expect("creating provider from unix stamp failed"); assert_eq!(time_provider.ccsds_days, 0) } @@ -1937,10 +1983,10 @@ mod tests { fn test_invalid_creation_from_unix_stamp_days_too_large() { let invalid_unix_secs: i64 = (u16::MAX as i64 + 1) * SECONDS_PER_DAY as i64; let subsec_millis = 0; - match TimeProvider::from_unix_secs_with_u16_days(&UnixTimestamp::const_new( - invalid_unix_secs, - subsec_millis, - )) { + match CdsTime::from_unix_stamp_with_u16_days( + &UnixTime::new(invalid_unix_secs, subsec_millis), + SubmillisPrecision::Absent, + ) { Ok(_) => { panic!("creation should not succeed") } @@ -1964,21 +2010,26 @@ mod tests { // precisely 31-12-1957 23:59:55 let unix_secs = DAYS_CCSDS_TO_UNIX * SECONDS_PER_DAY as i32 - 5; let subsec_millis = 0; - match TimeProvider::from_unix_secs_with_u16_days(&UnixTimestamp::const_new( - unix_secs as i64, - subsec_millis, - )) { + match CdsTime::from_unix_stamp_with_u16_days( + &UnixTime::new(unix_secs as i64, subsec_millis), + SubmillisPrecision::Absent, + ) { Ok(_) => { panic!("creation should not succeed") } Err(e) => { if let TimestampError::DateBeforeCcsdsEpoch(dt) = e { - assert_eq!(dt.year(), 1957); - assert_eq!(dt.month(), 12); - assert_eq!(dt.day(), 31); - assert_eq!(dt.hour(), 23); - assert_eq!(dt.minute(), 59); - assert_eq!(dt.second(), 55); + let dt = dt.chrono_date_time(); + if let chrono::LocalResult::Single(dt) = dt { + assert_eq!(dt.year(), 1957); + assert_eq!(dt.month(), 12); + assert_eq!(dt.day(), 31); + assert_eq!(dt.hour(), 23); + assert_eq!(dt.minute(), 59); + assert_eq!(dt.second(), 55); + } else { + panic!("unexpected error {}", e) + } } else { panic!("unexpected error {}", e) } @@ -1988,7 +2039,7 @@ mod tests { #[test] fn test_addition_u16_days_day_increment() { - let mut provider = TimeProvider::new_with_u16_days(0, MS_PER_DAY - 5 * 1000); + let mut provider = CdsTime::new_with_u16_days(0, MS_PER_DAY - 5 * 1000); let seconds_offset = Duration::from_secs(10); assert_eq!(provider.ccsds_days, 0); assert_eq!(provider.ms_of_day, MS_PER_DAY - 5 * 1000); @@ -1999,7 +2050,7 @@ mod tests { #[test] fn test_addition_u16_days() { - let mut provider = TimeProvider::new_with_u16_days(0, 0); + let mut provider = CdsTime::new_with_u16_days(0, 0); let seconds_offset = Duration::from_secs(5); assert_eq!(provider.ccsds_days, 0); assert_eq!(provider.ms_of_day, 0); @@ -2013,7 +2064,7 @@ mod tests { #[test] fn test_addition_u24_days() { - let mut provider = TimeProvider::new_with_u24_days(u16::MAX as u32, 0).unwrap(); + let mut provider = CdsTime::new_with_u24_days(u16::MAX as u32, 0).unwrap(); let seconds_offset = Duration::from_secs(5); assert_eq!(provider.ccsds_days, u16::MAX as u32); assert_eq!(provider.ms_of_day, 0); @@ -2027,13 +2078,13 @@ mod tests { #[test] fn test_dyn_creation_u24_days() { - let stamp = TimeProvider::new_with_u24_days(u16::MAX as u32 + 1, 24).unwrap(); + let stamp = CdsTime::new_with_u24_days(u16::MAX as u32 + 1, 24).unwrap(); let mut buf: [u8; 32] = [0; 32]; stamp.write_to_bytes(&mut buf).unwrap(); let dyn_provider = get_dyn_time_provider_from_bytes(&buf); assert!(dyn_provider.is_ok()); let dyn_provider = dyn_provider.unwrap(); - assert_eq!(dyn_provider.ccdsd_time_code(), CcsdsTimeCodes::Cds); + assert_eq!(dyn_provider.ccdsd_time_code(), CcsdsTimeCode::Cds); assert_eq!(dyn_provider.ccsds_days_as_u32(), u16::MAX as u32 + 1); assert_eq!(dyn_provider.ms_of_day(), 24); assert_eq!( @@ -2048,7 +2099,7 @@ mod tests { #[test] fn test_addition_with_us_precision_u16_days() { - let mut provider = TimeProvider::new_with_u16_days(0, 0); + let mut provider = CdsTime::new_with_u16_days(0, 0); provider.set_submillis(SubmillisPrecision::Microseconds, 0); let duration = Duration::from_micros(500); provider += duration; @@ -2061,7 +2112,7 @@ mod tests { #[test] fn test_addition_with_us_precision_u16_days_with_subsec_millis() { - let mut provider = TimeProvider::new_with_u16_days(0, 0); + let mut provider = CdsTime::new_with_u16_days(0, 0); provider.set_submillis(SubmillisPrecision::Microseconds, 0); let duration = Duration::from_micros(1200); provider += duration; @@ -2075,7 +2126,7 @@ mod tests { #[test] fn test_addition_with_us_precision_u16_days_carry_over() { - let mut provider = TimeProvider::new_with_u16_days(0, 0); + let mut provider = CdsTime::new_with_u16_days(0, 0); provider.set_submillis(SubmillisPrecision::Microseconds, 800); let duration = Duration::from_micros(400); provider += duration; @@ -2090,7 +2141,7 @@ mod tests { #[test] fn test_addition_with_ps_precision_u16_days() { - let mut provider = TimeProvider::new_with_u16_days(0, 0); + let mut provider = CdsTime::new_with_u16_days(0, 0); provider.set_submillis(SubmillisPrecision::Picoseconds, 0); // 500 us as ns let duration = Duration::from_nanos(500 * 10u32.pow(3) as u64); @@ -2107,7 +2158,7 @@ mod tests { fn test_addition_on_ref() { // This test case also tests the case where there is no submillis precision but subsecond // milliseconds. - let provider_ref = &TimeProvider::new_with_u16_days(2, 500); + let provider_ref = &CdsTime::new_with_u16_days(2, 500); let new_stamp = provider_ref + Duration::from_millis(2 * 24 * 60 * 60 * 1000 + 500); assert_eq!(new_stamp.ccsds_days_as_u32(), 4); assert_eq!(new_stamp.ms_of_day, 1000); @@ -2123,7 +2174,7 @@ mod tests { } #[test] fn test_addition_with_ps_precision_u16_days_with_subsec_millis() { - let mut provider = TimeProvider::new_with_u16_days(0, 0); + let mut provider = CdsTime::new_with_u16_days(0, 0); provider.set_submillis(SubmillisPrecision::Picoseconds, 0); // 1200 us as ns let duration = Duration::from_nanos(1200 * 10u32.pow(3) as u64); @@ -2138,7 +2189,7 @@ mod tests { #[test] fn test_addition_with_ps_precision_u16_days_carryover() { - let mut provider = TimeProvider::new_with_u16_days(0, 0); + let mut provider = CdsTime::new_with_u16_days(0, 0); // 800 us as ps provider.set_submillis(SubmillisPrecision::Picoseconds, 800 * 10_u32.pow(6)); // 400 us as ns @@ -2154,14 +2205,14 @@ mod tests { #[test] fn test_dyn_creation_u16_days_with_precision() { - let mut stamp = TimeProvider::new_with_u16_days(24, 24); + let mut stamp = CdsTime::new_with_u16_days(24, 24); stamp.set_submillis(SubmillisPrecision::Microseconds, 666); let mut buf: [u8; 32] = [0; 32]; stamp.write_to_bytes(&mut buf).unwrap(); let dyn_provider = get_dyn_time_provider_from_bytes(&buf); assert!(dyn_provider.is_ok()); let dyn_provider = dyn_provider.unwrap(); - assert_eq!(dyn_provider.ccdsd_time_code(), CcsdsTimeCodes::Cds); + assert_eq!(dyn_provider.ccdsd_time_code(), CcsdsTimeCode::Cds); assert_eq!(dyn_provider.ccsds_days_as_u32(), 24); assert_eq!(dyn_provider.ms_of_day(), 24); assert_eq!( @@ -2177,7 +2228,7 @@ mod tests { #[test] fn test_new_u24_days_too_large() { - let time_provider = TimeProvider::new_with_u24_days(2_u32.pow(24), 0); + let time_provider = CdsTime::new_with_u24_days(2_u32.pow(24), 0); assert!(time_provider.is_err()); let e = time_provider.unwrap_err(); if let CdsError::InvalidCcsdsDays(days) = e { @@ -2194,21 +2245,21 @@ mod tests { .unwrap() .and_hms_milli_opt(23, 59, 59, 999) .unwrap() - .and_local_timezone(Utc) + .and_local_timezone(chrono::Utc) .unwrap(); - let time_provider = TimeProvider::from_dt_with_u24_days(&datetime_utc); + let time_provider = CdsTime::from_dt_with_u24_days(&datetime_utc); assert!(time_provider.is_err()); if let TimestampError::DateBeforeCcsdsEpoch(dt) = time_provider.unwrap_err() { - assert_eq!(dt, datetime_utc); + assert_eq!(dt, datetime_utc.into()); } } #[test] fn test_eq() { - let stamp0 = TimeProvider::new_with_u16_days(0, 0); + let stamp0 = CdsTime::new_with_u16_days(0, 0); let mut buf: [u8; 7] = [0; 7]; stamp0.write_to_bytes(&mut buf).unwrap(); - let stamp1 = TimeProvider::from_bytes_with_u16_days(&buf).unwrap(); + let stamp1 = CdsTime::from_bytes_with_u16_days(&buf).unwrap(); assert_eq!(stamp0, stamp1); assert!(stamp0 >= stamp1); assert!(stamp1 <= stamp0); @@ -2216,11 +2267,11 @@ mod tests { #[test] fn test_ord() { - let stamp0 = TimeProvider::new_with_u24_days(0, 0).unwrap(); - let stamp1 = TimeProvider::new_with_u24_days(0, 50000).unwrap(); - let mut stamp2 = TimeProvider::new_with_u24_days(0, 50000).unwrap(); + let stamp0 = CdsTime::new_with_u24_days(0, 0).unwrap(); + let stamp1 = CdsTime::new_with_u24_days(0, 50000).unwrap(); + let mut stamp2 = CdsTime::new_with_u24_days(0, 50000).unwrap(); stamp2.set_submillis(SubmillisPrecision::Microseconds, 500); - let stamp3 = TimeProvider::new_with_u24_days(1, 0).unwrap(); + let stamp3 = CdsTime::new_with_u24_days(1, 0).unwrap(); assert!(stamp1 > stamp0); assert!(stamp2 > stamp0); assert!(stamp2 > stamp1); @@ -2231,8 +2282,8 @@ mod tests { #[test] fn test_conversion() { - let mut stamp_small = TimeProvider::new_with_u16_days(u16::MAX, 500); - let stamp_larger: TimeProvider = stamp_small.into(); + let mut stamp_small = CdsTime::new_with_u16_days(u16::MAX, 500); + let stamp_larger: CdsTime = stamp_small.into(); assert_eq!(stamp_larger.ccsds_days_as_u32(), u16::MAX as u32); assert_eq!(stamp_larger.ms_of_day(), 500); stamp_small = stamp_larger.try_into().unwrap(); @@ -2242,25 +2293,25 @@ mod tests { #[test] fn test_update_from_now() { - let mut stamp = TimeProvider::new_with_u16_days(0, 0); + let mut stamp = CdsTime::new_with_u16_days(0, 0); let _ = stamp.update_from_now(); - let dt = stamp.unix_stamp().as_date_time().unwrap(); + let dt = stamp.unix_time().chrono_date_time().unwrap(); assert!(dt.year() > 2020); } #[test] fn test_setting_submillis_precision() { - let mut provider = TimeProvider::new_with_u16_days(0, 15); + let mut provider = CdsTime::new_with_u16_days(0, 15); provider.set_submillis(SubmillisPrecision::Microseconds, 500); } #[test] #[cfg(feature = "serde")] fn test_serialization() { - let stamp_now = TimeProvider::from_now_with_u16_days().expect("Error retrieving time"); + let stamp_now = CdsTime::from_now_with_u16_days().expect("Error retrieving time"); let val = to_allocvec(&stamp_now).expect("Serializing timestamp failed"); assert!(val.len() > 0); - let stamp_deser: TimeProvider = from_bytes(&val).expect("Stamp deserialization failed"); + let stamp_deser: CdsTime = from_bytes(&val).expect("Stamp deserialization failed"); assert_eq!(stamp_deser, stamp_now); } @@ -2277,7 +2328,7 @@ mod tests { #[test] fn test_stamp_to_vec_u16() { - let stamp = TimeProvider::new_with_u16_days(1, 1); + let stamp = CdsTime::new_with_u16_days(1, 1); let stamp_vec = stamp.to_vec().unwrap(); let mut buf: [u8; 7] = [0; 7]; stamp.write_to_bytes(&mut buf).unwrap(); @@ -2286,10 +2337,23 @@ mod tests { #[test] fn test_stamp_to_vec_u24() { - let stamp = TimeProvider::new_with_u24_days(1, 1).unwrap(); + let stamp = CdsTime::new_with_u24_days(1, 1).unwrap(); let stamp_vec = stamp.to_vec().unwrap(); let mut buf: [u8; 10] = [0; 10]; stamp.write_to_bytes(&mut buf).unwrap(); assert_eq!(stamp_vec, buf[..stamp.len_written()]); } + + #[test] + #[cfg(feature = "timelib")] + fn test_timelib_stamp() { + let stamp = CdsTime::new_with_u16_days(0, 0); + let timelib_dt = stamp.timelib_date_time().unwrap(); + assert_eq!(timelib_dt.year(), 1958); + assert_eq!(timelib_dt.month(), time::Month::January); + assert_eq!(timelib_dt.day(), 1); + assert_eq!(timelib_dt.hour(), 0); + assert_eq!(timelib_dt.minute(), 0); + assert_eq!(timelib_dt.second(), 0); + } } diff --git a/src/time/cuc.rs b/src/time/cuc.rs index 4b5d6e8..6671327 100644 --- a/src/time/cuc.rs +++ b/src/time/cuc.rs @@ -1,18 +1,36 @@ //! Module to generate or read CCSDS Unsegmented (CUC) timestamps as specified in //! [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) section 3.2 . //! -//! The core data structure to do this is the [TimeProviderCcsdsEpoch] struct. -use super::*; -use chrono::Datelike; -use core::fmt::Debug; +//! The core data structure to do this is the [CucTime] struct which provides a CUC time object +//! using the CCSDS Epoch. +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use core::fmt::{Debug, Display, Formatter}; use core::ops::{Add, AddAssign}; use core::time::Duration; use core::u64; +use crate::ByteConversionError; + +#[cfg(feature = "std")] +use super::StdTimestampError; +use super::{ + ccsds_epoch_to_unix_epoch, ccsds_time_code_from_p_field, unix_epoch_to_ccsds_epoch, + CcsdsTimeCode, CcsdsTimeProvider, TimeReader, TimeWriter, TimestampError, UnixTime, +}; +#[cfg(feature = "std")] +use std::error::Error; +#[cfg(feature = "std")] +use std::time::SystemTime; + +#[cfg(feature = "chrono")] +use chrono::Datelike; + const MIN_CUC_LEN: usize = 2; /// Base value for the preamble field for a time field parser to determine the time field type. -pub const P_FIELD_BASE: u8 = (CcsdsTimeCodes::CucCcsdsEpoch as u8) << 4; +pub const P_FIELD_BASE: u8 = (CcsdsTimeCode::CucCcsdsEpoch as u8) << 4; /// Maximum length if the preamble field is not extended. pub const MAX_CUC_LEN_SMALL_PREAMBLE: usize = 8; @@ -21,7 +39,7 @@ pub const MAX_CUC_LEN_SMALL_PREAMBLE: usize = 8; pub enum FractionalResolution { /// No fractional part, only second resolution Seconds = 0, - /// 256 fractional parts, resulting in 1/255 ~= 4 ms fractional resolution + /// 255 fractional parts, resulting in 1/255 ~= 4 ms fractional resolution FourMs = 1, /// 65535 fractional parts, resulting in 1/65535 ~= 15 us fractional resolution FifteenUs = 2, @@ -43,18 +61,22 @@ impl TryFrom for FractionalResolution { } } -/// Please note that this function will panic if the fractional value is not smaller than +/// Please note that this function will panic if the fractional counter is not smaller than /// the maximum number of fractions allowed for the particular resolution. /// (e.g. passing 270 when the resolution only allows 255 values). #[inline] pub fn convert_fractional_part_to_ns(fractional_part: FractionalPart) -> u64 { - let div = fractional_res_to_div(fractional_part.0); - assert!(fractional_part.1 < div); - 10_u64.pow(9) * fractional_part.1 as u64 / div as u64 + let div = fractional_res_to_div(fractional_part.resolution); + assert!(fractional_part.counter <= div); + 10_u64.pow(9) * fractional_part.counter as u64 / div as u64 } #[inline(always)] pub const fn fractional_res_to_div(res: FractionalResolution) -> u32 { + // We do not use the full possible range for a given resolution. This is because if we did + // that, the largest value would be equal to the counter being incremented by one. Thus, the + // smallest allowed fractions value is 0 while the largest allowed fractions value is the + // closest fractions value to the next counter increment. 2_u32.pow(8 * res as u32) - 1 } @@ -62,32 +84,22 @@ pub const fn fractional_res_to_div(res: FractionalResolution) -> u32 { /// Please note that this function will panic if the passed nanoseconds exceeds 1 second /// as a nanosecond (10 to the power of 9). Furthermore, it will return [None] if the /// given resolution is [FractionalResolution::Seconds]. -pub fn fractional_part_from_subsec_ns( - res: FractionalResolution, - ns: u64, -) -> Option { +pub fn fractional_part_from_subsec_ns(res: FractionalResolution, ns: u64) -> FractionalPart { if res == FractionalResolution::Seconds { - return None; + return FractionalPart::new_with_seconds_resolution(); } let sec_as_ns = 10_u64.pow(9); if ns > sec_as_ns { panic!("passed nanosecond value larger than 1 second"); } - let resolution = fractional_res_to_div(res) as u64; - // Use integer division because this can reduce code size of really small systems. - // First determine the nanoseconds for the smallest segment given the resolution. - // Then divide by that to find out the fractional part. For the calculation of the smallest - // fraction, we perform a ceiling division. This is because if we would use the default - // flooring division, we would divide by a smaller value, thereby allowing the calculation to - // invalid fractional parts which are too large. For the division of the nanoseconds by the - // smallest fraction, a flooring division is correct. - // The multiplication with 100000 is necessary to avoid precision loss during integer division. - // TODO: Floating point division might actually be faster option, but requires additional - // code on small embedded systems.. - let fractional_part = ns * 100000 / ((sec_as_ns * 100000 + resolution) / resolution); - // Floating point division. - //let fractional_part = (ns as f64 / ((sec_as_ns as f64) / resolution as f64)).floor() as u32; - Some(FractionalPart(res, fractional_part as u32)) + let resolution_divisor = fractional_res_to_div(res) as u64; + // This is probably the cheapest way to calculate the fractional part without using expensive + // floating point division. + let fractional_counter = ns * (resolution_divisor + 1) / sec_as_ns; + FractionalPart { + resolution: res, + counter: fractional_counter as u32, + } } #[derive(Copy, Clone, PartialEq, Eq, Debug)] @@ -103,6 +115,7 @@ pub enum CucError { resolution: FractionalResolution, value: u64, }, + LeapSecondCorrectionError, } impl Display for CucError { @@ -120,6 +133,9 @@ impl Display for CucError { "invalid cuc fractional part {value} for resolution {resolution:?}" ) } + CucError::LeapSecondCorrectionError => { + write!(f, "error while correcting for leap seconds") + } } } } @@ -130,9 +146,57 @@ impl Error for CucError {} #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct WidthCounterPair(u8, u32); + #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct FractionalPart(FractionalResolution, u32); +pub struct FractionalPart { + pub resolution: FractionalResolution, + pub counter: u32, +} + +impl FractionalPart { + pub const fn new(resolution: FractionalResolution, counter: u32) -> Self { + let div = fractional_res_to_div(resolution); + assert!(counter <= div, "invalid counter for resolution"); + Self { + resolution, + counter, + } + } + + /// An empty fractional part for second resolution only. + pub const fn new_with_seconds_resolution() -> Self { + Self::new(FractionalResolution::Seconds, 0) + } + + /// Helper method which simply calls [Self::new_with_seconds_resolution]. + pub const fn new_empty() -> Self { + Self::new_with_seconds_resolution() + } + + pub fn new_checked(resolution: FractionalResolution, counter: u32) -> Option { + let div = fractional_res_to_div(resolution); + if counter > div { + return None; + } + Some(Self { + resolution, + counter, + }) + } + + pub fn resolution(&self) -> FractionalResolution { + self.resolution + } + + pub fn counter(&self) -> u32 { + self.counter + } + + pub fn no_fractional_part(&self) -> bool { + self.resolution == FractionalResolution::Seconds + } +} /// This object is the abstraction for the CCSDS Unsegmented Time Code (CUC) using the CCSDS epoch /// and a small preamble field. @@ -140,30 +204,42 @@ pub struct FractionalPart(FractionalResolution, u32); /// It has the capability to generate and read timestamps as specified in the CCSDS 301.0-B-4 /// section 3.2 . The preamble field only has one byte, which allows a time code representation /// through the year 2094. The time is represented as a simple binary counter starting from the -/// fixed CCSDS epoch (1958-01-01T00:00:00+00:00). It is possible to provide subsecond accuracy -/// using the fractional field with various available [resolutions][FractionalResolution]. +/// fixed CCSDS epoch 1958-01-01T00:00:00+00:00 using the TAI reference time scale. This time +/// code provides the advantage of being truly monotonic. +/// It is possible to provide subsecond accuracy using the fractional field with various available +/// [resolutions][FractionalResolution]. /// /// Having a preamble field of one byte limits the width of the counter /// type (generally seconds) to 4 bytes and the width of the fractions type to 3 bytes. This limits /// the maximum time stamp size to [MAX_CUC_LEN_SMALL_PREAMBLE] (8 bytes). /// +/// Please note that this object does not implement the [CcsdsTimeProvider] trait by itself because +/// leap seconds corrections need to be applied to support the trait methods. Instead, it needs +/// to be converted to a [CucTimeWithLeapSecs] object using the [Self::to_leap_sec_helper] method. +/// +/// This time code is not UTC based. Conversion to UTC based times, for example a UNIX timestamp, +/// can be performed by subtracting the current number of leap seconds. +/// /// # Example /// /// ``` -/// use spacepackets::time::cuc::{FractionalResolution, TimeProviderCcsdsEpoch}; -/// use spacepackets::time::{TimeWriter, CcsdsTimeCodes, TimeReader, CcsdsTimeProvider}; +/// use spacepackets::time::cuc::{FractionalResolution, CucTime}; +/// use spacepackets::time::{TimeWriter, CcsdsTimeCode, TimeReader, CcsdsTimeProvider}; +/// +/// const LEAP_SECONDS: u32 = 37; /// /// // Highest fractional resolution -/// let timestamp_now = TimeProviderCcsdsEpoch::from_now(FractionalResolution::SixtyNs).expect("creating cuc stamp failed"); +/// let timestamp_now = CucTime::from_now(FractionalResolution::SixtyNs, LEAP_SECONDS) +/// .expect("creating cuc stamp failed"); /// let mut raw_stamp = [0; 16]; /// { /// let written = timestamp_now.write_to_bytes(&mut raw_stamp).expect("writing timestamp failed"); -/// assert_eq!((raw_stamp[0] >> 4) & 0b111, CcsdsTimeCodes::CucCcsdsEpoch as u8); +/// assert_eq!((raw_stamp[0] >> 4) & 0b111, CcsdsTimeCode::CucCcsdsEpoch as u8); /// // 1 byte preamble + 4 byte counter + 3 byte fractional part /// assert_eq!(written, 8); /// } /// { -/// let read_result = TimeProviderCcsdsEpoch::from_bytes(&raw_stamp); +/// let read_result = CucTime::from_bytes(&raw_stamp); /// assert!(read_result.is_ok()); /// let stamp_deserialized = read_result.unwrap(); /// assert_eq!(stamp_deserialized, timestamp_now); @@ -171,10 +247,25 @@ pub struct FractionalPart(FractionalResolution, u32); /// ``` #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct TimeProviderCcsdsEpoch { +pub struct CucTime { pfield: u8, counter: WidthCounterPair, - fractions: Option, + fractions: FractionalPart, +} + +/// This object is a wrapper object around the [CucTime] object which also tracks +/// the leap seconds. This is necessary to implement the [CcsdsTimeProvider] trait. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct CucTimeWithLeapSecs { + pub time: CucTime, + pub leap_seconds: u32, +} + +impl CucTimeWithLeapSecs { + pub fn new(time: CucTime, leap_seconds: u32) -> Self { + Self { time, leap_seconds } + } } #[inline] @@ -185,16 +276,20 @@ pub fn pfield_len(pfield: u8) -> usize { 1 } -impl TimeProviderCcsdsEpoch { +impl CucTime { /// Create a time provider with a four byte counter and no fractional part. pub fn new(counter: u32) -> Self { // These values are definitely valid, so it is okay to unwrap here. - Self::new_generic(WidthCounterPair(4, counter), None).unwrap() + Self::new_generic( + WidthCounterPair(4, counter), + FractionalPart::new_with_seconds_resolution(), + ) + .unwrap() } - /// Like [TimeProviderCcsdsEpoch::new] but allow to supply a fractional part as well. + /// Like [CucTime::new] but allow to supply a fractional part as well. pub fn new_with_fractions(counter: u32, fractions: FractionalPart) -> Result { - Self::new_generic(WidthCounterPair(4, counter), Some(fractions)) + Self::new_generic(WidthCounterPair(4, counter), fractions) } /// Fractions with a resolution of ~ 4 ms @@ -202,10 +297,10 @@ impl TimeProviderCcsdsEpoch { // These values are definitely valid, so it is okay to unwrap here. Self::new_generic( WidthCounterPair(4, counter), - Some(FractionalPart( - FractionalResolution::FourMs, - subsec_fractions as u32, - )), + FractionalPart { + resolution: FractionalResolution::FourMs, + counter: subsec_fractions as u32, + }, ) .unwrap() } @@ -215,10 +310,10 @@ impl TimeProviderCcsdsEpoch { // These values are definitely valid, so it is okay to unwrap here. Self::new_generic( WidthCounterPair(4, counter), - Some(FractionalPart( - FractionalResolution::FifteenUs, - subsec_fractions as u32, - )), + FractionalPart { + resolution: FractionalResolution::FifteenUs, + counter: subsec_fractions as u32, + }, ) .unwrap() } @@ -229,56 +324,89 @@ impl TimeProviderCcsdsEpoch { pub fn new_with_fine_fractions(counter: u32, subsec_fractions: u32) -> Result { Self::new_generic( WidthCounterPair(4, counter), - Some(FractionalPart( - FractionalResolution::SixtyNs, - subsec_fractions, - )), + FractionalPart { + resolution: FractionalResolution::SixtyNs, + counter: subsec_fractions, + }, ) } /// This function will return the current time as a CUC timestamp. /// The counter width will always be set to 4 bytes because the normal CCSDS epoch will overflow /// when using less than that. + /// + /// The CUC timestamp uses TAI as the reference time system. Therefore, leap second corrections + /// must be applied on top of the UTC based time retrieved from the system in addition to the + /// conversion to the CCSDS epoch. #[cfg(feature = "std")] #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] - pub fn from_now(fraction_resolution: FractionalResolution) -> Result { + pub fn from_now( + fraction_resolution: FractionalResolution, + leap_seconds: u32, + ) -> Result { let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; - let ccsds_epoch = unix_epoch_to_ccsds_epoch(now.as_secs() as i64); + let mut counter = + u32::try_from(unix_epoch_to_ccsds_epoch(now.as_secs() as i64)).map_err(|_| { + TimestampError::Cuc(CucError::InvalidCounter { + width: 4, + counter: now.as_secs(), + }) + })?; + counter = counter + .checked_add(leap_seconds) + .ok_or(TimestampError::Cuc(CucError::LeapSecondCorrectionError))?; if fraction_resolution == FractionalResolution::Seconds { - return Ok(Self::new(ccsds_epoch as u32)); + return Ok(Self::new(counter)); } let fractions = fractional_part_from_subsec_ns(fraction_resolution, now.subsec_nanos() as u64); - Self::new_with_fractions(ccsds_epoch as u32, fractions.unwrap()) + Self::new_with_fractions(counter, fractions) .map_err(|e| StdTimestampError::Timestamp(e.into())) } /// Updates the current time stamp from the current time. The fractional field width remains /// the same and will be updated accordingly. + /// + /// The CUC timestamp uses TAI as the reference time system. Therefore, leap second corrections + /// must be applied on top of the UTC based time retrieved from the system in addition to the + /// conversion to the CCSDS epoch. #[cfg(feature = "std")] #[cfg_attr(doc_cfg, doc(cfg(feature = "std")))] - pub fn update_from_now(&mut self) -> Result<(), StdTimestampError> { + pub fn update_from_now(&mut self, leap_seconds: u32) -> Result<(), StdTimestampError> { let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; self.counter.1 = unix_epoch_to_ccsds_epoch(now.as_secs() as i64) as u32; - if self.fractions.is_some() { + self.counter.1 = self + .counter + .1 + .checked_add(leap_seconds) + .ok_or(TimestampError::Cuc(CucError::LeapSecondCorrectionError))?; + if let FractionalResolution::Seconds = self.fractions.resolution { self.fractions = fractional_part_from_subsec_ns( - self.fractions.unwrap().0, + self.fractions.resolution, now.subsec_nanos() as u64, ); + return Ok(()); } Ok(()) } - pub fn from_date_time( - dt: &DateTime, + #[cfg(feature = "chrono")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "chrono")))] + pub fn from_chrono_date_time( + dt: &chrono::DateTime, res: FractionalResolution, + leap_seconds: u32, ) -> Result { // Year before CCSDS epoch is invalid. if dt.year() < 1958 { - return Err(TimestampError::DateBeforeCcsdsEpoch(*dt)); + return Err(TimestampError::DateBeforeCcsdsEpoch(UnixTime::from(*dt))); } + let counter = dt + .timestamp() + .checked_add(i64::from(leap_seconds)) + .ok_or(TimestampError::Cuc(CucError::LeapSecondCorrectionError))?; Self::new_generic( - WidthCounterPair(4, dt.timestamp() as u32), + WidthCounterPair(4, counter as u32), fractional_part_from_subsec_ns(res, dt.timestamp_subsec_nanos() as u64), ) .map_err(|e| e.into()) @@ -287,26 +415,39 @@ impl TimeProviderCcsdsEpoch { /// Generates a CUC timestamp from a UNIX timestamp with a width of 4. This width is able /// to accomodate all possible UNIX timestamp values. pub fn from_unix_stamp( - unix_stamp: &UnixTimestamp, + unix_stamp: &UnixTime, res: FractionalResolution, + leap_seconds: u32, ) -> Result { - let ccsds_epoch = unix_epoch_to_ccsds_epoch(unix_stamp.unix_seconds); + let counter = unix_epoch_to_ccsds_epoch(unix_stamp.secs); // Negative CCSDS epoch is invalid. - if ccsds_epoch < 0 { - return Err(TimestampError::DateBeforeCcsdsEpoch( - unix_stamp.as_date_time().unwrap(), - )); + if counter < 0 { + return Err(TimestampError::DateBeforeCcsdsEpoch(*unix_stamp)); } - let fractions = fractional_part_from_subsec_ns( - res, - unix_stamp.subsecond_millis() as u64 * 10_u64.pow(6), - ); - Self::new_generic(WidthCounterPair(4, ccsds_epoch as u32), fractions).map_err(|e| e.into()) + // We already excluded negative values, so the conversion to u64 should always work. + let mut counter = u32::try_from(counter).map_err(|_| CucError::InvalidCounter { + width: 4, + counter: counter as u64, + })?; + counter = counter + .checked_add(leap_seconds) + .ok_or(TimestampError::Cuc(CucError::LeapSecondCorrectionError))?; + let fractions = + fractional_part_from_subsec_ns(res, unix_stamp.subsec_millis() as u64 * 10_u64.pow(6)); + Self::new_generic(WidthCounterPair(4, counter as u32), fractions).map_err(|e| e.into()) } pub fn new_u16_counter(counter: u16) -> Self { // These values are definitely valid, so it is okay to unwrap here. - Self::new_generic(WidthCounterPair(2, counter as u32), None).unwrap() + Self::new_generic( + WidthCounterPair(2, counter as u32), + FractionalPart::new_with_seconds_resolution(), + ) + .unwrap() + } + + pub fn ccsds_time_code(&self) -> CcsdsTimeCode { + CcsdsTimeCode::CucCcsdsEpoch } pub fn width_counter_pair(&self) -> WidthCounterPair { @@ -321,13 +462,18 @@ impl TimeProviderCcsdsEpoch { self.counter.1 } - pub fn width_fractions_pair(&self) -> Option { + /// Subsecond fractional part of the CUC time. + pub fn fractions(&self) -> FractionalPart { self.fractions } + pub fn to_leap_sec_helper(&self, leap_seconds: u32) -> CucTimeWithLeapSecs { + CucTimeWithLeapSecs::new(*self, leap_seconds) + } + pub fn set_fractions(&mut self, fractions: FractionalPart) -> Result<(), CucError> { Self::verify_fractions_value(fractions)?; - self.fractions = Some(fractions); + self.fractions = fractions; self.update_p_field_fractions(); Ok(()) } @@ -336,41 +482,33 @@ impl TimeProviderCcsdsEpoch { /// to 0 if the resolution changes. pub fn set_fractional_resolution(&mut self, res: FractionalResolution) { if res == FractionalResolution::Seconds { - self.fractions = None; + self.fractions = FractionalPart::new_with_seconds_resolution(); } - let mut update_fractions = true; - if let Some(existing_fractions) = self.fractions { - if existing_fractions.0 == res { - update_fractions = false; - } - }; - if update_fractions { - self.fractions = Some(FractionalPart(res, 0)); + if res != self.fractions().resolution() { + self.fractions = FractionalPart::new(res, 0); } } pub fn new_generic( counter: WidthCounterPair, - fractions: Option, + fractions: FractionalPart, ) -> Result { Self::verify_counter_width(counter.0)?; if counter.1 > (2u64.pow(counter.0 as u32 * 8) - 1) as u32 { return Err(CucError::InvalidCounter { width: counter.0, - counter: counter.1 as u64, + counter: counter.1.into(), }); } - if let Some(fractions) = fractions { - Self::verify_fractions_value(fractions)?; - } + Self::verify_fractions_value(fractions)?; Ok(Self { - pfield: Self::build_p_field(counter.0, fractions.map(|v| v.0)), + pfield: Self::build_p_field(counter.0, fractions.resolution), counter, fractions, }) } - fn build_p_field(counter_width: u8, fractions_width: Option) -> u8 { + fn build_p_field(counter_width: u8, resolution: FractionalResolution) -> u8 { let mut pfield = P_FIELD_BASE; if !(1..=4).contains(&counter_width) { // Okay to panic here, this function is private and all input values should @@ -378,25 +516,20 @@ impl TimeProviderCcsdsEpoch { panic!("invalid counter width {} for cuc timestamp", counter_width); } pfield |= (counter_width - 1) << 2; - if let Some(fractions_width) = fractions_width { - if !(1..=3).contains(&(fractions_width as u8)) { + if resolution != FractionalResolution::Seconds { + if !(1..=3).contains(&(resolution as u8)) { // Okay to panic here, this function is private and all input values should // have been sanitized - panic!( - "invalid fractions width {:?} for cuc timestamp", - fractions_width - ); + panic!("invalid fractions width {:?} for cuc timestamp", resolution); } - pfield |= fractions_width as u8; + pfield |= resolution as u8; } pfield } fn update_p_field_fractions(&mut self) { self.pfield &= !(0b11); - if let Some(fractions) = self.fractions { - self.pfield |= fractions.0 as u8; - } + self.pfield |= self.fractions.resolution() as u8; } #[inline] @@ -410,8 +543,15 @@ impl TimeProviderCcsdsEpoch { } #[inline] - fn unix_seconds(&self) -> i64 { + pub fn unix_secs(&self, leap_seconds: u32) -> i64 { ccsds_epoch_to_unix_epoch(self.counter.1 as i64) + .checked_sub(leap_seconds as i64) + .unwrap() + } + + #[inline] + pub fn subsec_millis(&self) -> u16 { + (self.subsec_nanos() / 1_000_000) as u16 } /// This returns the length of the individual components of the CUC timestamp in addition @@ -447,21 +587,30 @@ impl TimeProviderCcsdsEpoch { } fn verify_fractions_value(val: FractionalPart) -> Result<(), CucError> { - if val.1 > 2u32.pow((val.0 as u32) * 8) - 1 { + if val.counter > 2u32.pow((val.resolution as u32) * 8) - 1 { return Err(CucError::InvalidFractions { - resolution: val.0, - value: val.1 as u64, + resolution: val.resolution, + value: val.counter as u64, }); } Ok(()) } + + fn len_as_bytes(&self) -> usize { + Self::len_packed_from_pfield(self.pfield) + } + + fn subsec_nanos(&self) -> u32 { + if self.fractions.resolution() == FractionalResolution::Seconds { + return 0; + } + // Rounding down here is the correct approach. + convert_fractional_part_to_ns(self.fractions) as u32 + } } -impl TimeReader for TimeProviderCcsdsEpoch { - fn from_bytes(buf: &[u8]) -> Result - where - Self: Sized, - { +impl TimeReader for CucTime { + fn from_bytes(buf: &[u8]) -> Result { if buf.len() < MIN_CUC_LEN { return Err(TimestampError::ByteConversion( ByteConversionError::FromSliceTooSmall { @@ -472,16 +621,16 @@ impl TimeReader for TimeProviderCcsdsEpoch { } match ccsds_time_code_from_p_field(buf[0]) { Ok(code) => { - if code != CcsdsTimeCodes::CucCcsdsEpoch { + if code != CcsdsTimeCode::CucCcsdsEpoch { return Err(TimestampError::InvalidTimeCode { - expected: CcsdsTimeCodes::CucCcsdsEpoch, + expected: CcsdsTimeCode::CucCcsdsEpoch, found: code as u8, }); } } Err(raw) => { return Err(TimestampError::InvalidTimeCode { - expected: CcsdsTimeCodes::CucCcsdsEpoch, + expected: CcsdsTimeCode::CucCcsdsEpoch, found: raw, }); } @@ -509,29 +658,29 @@ impl TimeReader for TimeProviderCcsdsEpoch { _ => panic!("unreachable match arm"), }; current_idx += cntr_len as usize; - let mut fractions = None; + let mut fractions = FractionalPart::new_with_seconds_resolution(); if fractions_len > 0 { match fractions_len { 1 => { - fractions = Some(FractionalPart( + fractions = FractionalPart::new( fractions_len.try_into().unwrap(), buf[current_idx] as u32, - )) + ) } 2 => { - fractions = Some(FractionalPart( + fractions = FractionalPart::new( fractions_len.try_into().unwrap(), u16::from_be_bytes(buf[current_idx..current_idx + 2].try_into().unwrap()) as u32, - )) + ) } 3 => { let mut tmp_buf: [u8; 4] = [0; 4]; tmp_buf[1..4].copy_from_slice(&buf[current_idx..current_idx + 3]); - fractions = Some(FractionalPart( + fractions = FractionalPart::new( fractions_len.try_into().unwrap(), u32::from_be_bytes(tmp_buf), - )) + ) } _ => panic!("unreachable match arm"), } @@ -541,7 +690,7 @@ impl TimeReader for TimeProviderCcsdsEpoch { } } -impl TimeWriter for TimeProviderCcsdsEpoch { +impl TimeWriter for CucTime { fn write_to_bytes(&self, bytes: &mut [u8]) -> Result { // Cross check the sizes of the counters against byte widths in the ctor if bytes.len() < self.len_as_bytes() { @@ -573,18 +722,15 @@ impl TimeWriter for TimeProviderCcsdsEpoch { _ => panic!("invalid counter width value"), } current_idx += self.counter.0 as usize; - if let Some(fractions) = self.fractions { - match fractions.0 { - FractionalResolution::FourMs => bytes[current_idx] = fractions.1 as u8, - FractionalResolution::FifteenUs => bytes[current_idx..current_idx + 2] - .copy_from_slice(&(fractions.1 as u16).to_be_bytes()), - FractionalResolution::SixtyNs => bytes[current_idx..current_idx + 3] - .copy_from_slice(&fractions.1.to_be_bytes()[1..4]), - // Should also never happen - _ => panic!("invalid fractions value"), - } - current_idx += fractions.0 as usize; + match self.fractions.resolution() { + FractionalResolution::FourMs => bytes[current_idx] = self.fractions.counter as u8, + FractionalResolution::FifteenUs => bytes[current_idx..current_idx + 2] + .copy_from_slice(&(self.fractions.counter as u16).to_be_bytes()), + FractionalResolution::SixtyNs => bytes[current_idx..current_idx + 3] + .copy_from_slice(&self.fractions.counter.to_be_bytes()[1..4]), + _ => (), } + current_idx += self.fractions.resolution as usize; Ok(current_idx) } @@ -593,53 +739,34 @@ impl TimeWriter for TimeProviderCcsdsEpoch { } } -impl CcsdsTimeProvider for TimeProviderCcsdsEpoch { +impl CcsdsTimeProvider for CucTimeWithLeapSecs { fn len_as_bytes(&self) -> usize { - Self::len_packed_from_pfield(self.pfield) + self.time.len_as_bytes() } fn p_field(&self) -> (usize, [u8; 2]) { - (1, [self.pfield, 0]) + (1, [self.time.pfield, 0]) } - fn ccdsd_time_code(&self) -> CcsdsTimeCodes { - CcsdsTimeCodes::CucCcsdsEpoch + fn ccdsd_time_code(&self) -> CcsdsTimeCode { + self.time.ccsds_time_code() } - fn unix_seconds(&self) -> i64 { - self.unix_seconds() + fn unix_secs(&self) -> i64 { + self.time.unix_secs(self.leap_seconds) } - fn subsecond_millis(&self) -> u16 { - if let Some(fractions) = self.fractions { - if fractions.0 == FractionalResolution::Seconds { - return 0; - } - // Rounding down here is the correct approach. - return (convert_fractional_part_to_ns(fractions) / 10_u32.pow(6) as u64) as u16; - } - 0 - } - - fn date_time(&self) -> Option> { - let unix_seconds = self.unix_seconds(); - let ns = if let Some(fractional_part) = self.fractions { - convert_fractional_part_to_ns(fractional_part) - } else { - 0 - }; - if let LocalResult::Single(res) = Utc.timestamp_opt(unix_seconds, ns as u32) { - return Some(res); - } - None + fn subsec_nanos(&self) -> u32 { + self.time.subsec_nanos() } } -fn get_provider_values_after_duration_addition( - provider: &TimeProviderCcsdsEpoch, +// TODO: Introduce more overflow checks here. +fn get_time_values_after_duration_addition( + time: &CucTime, duration: Duration, -) -> (u32, Option) { - let mut new_counter = provider.counter.1; +) -> (u32, FractionalPart) { + let mut new_counter = time.counter.1; let subsec_nanos = duration.subsec_nanos(); let mut increment_counter = |amount: u32| { let mut sum: u64 = 0; @@ -651,7 +778,7 @@ fn get_provider_values_after_duration_addition( } new_counter = sum as u32; }; - match provider.counter.0 { + match time.counter.0 { 1 => counter_inc_handler(u8::MAX as u64), 2 => counter_inc_handler(u16::MAX as u64), 3 => counter_inc_handler((2_u32.pow(24) - 1) as u64), @@ -662,108 +789,103 @@ fn get_provider_values_after_duration_addition( } } }; - let fractional_part = if let Some(fractional_part) = &provider.fractions { - let fractional_increment = - fractional_part_from_subsec_ns(fractional_part.0, subsec_nanos as u64).unwrap(); - let mut increment_fractions = |resolution| { - let mut new_fractions = fractional_part.1 + fractional_increment.1; - let max_fractions = fractional_res_to_div(resolution); - if new_fractions > max_fractions { - increment_counter(1); - new_fractions -= max_fractions; - } - Some(FractionalPart(resolution, new_fractions)) - }; - match fractional_increment.0 { - FractionalResolution::Seconds => None, - _ => increment_fractions(fractional_increment.0), + let resolution = time.fractions().resolution(); + let fractional_increment = fractional_part_from_subsec_ns(resolution, subsec_nanos as u64); + let mut fractional_part = FractionalPart::new_with_seconds_resolution(); + if resolution != FractionalResolution::Seconds { + let mut new_fractions = time.fractions().counter() + fractional_increment.counter; + let max_fractions = fractional_res_to_div(resolution); + if new_fractions > max_fractions { + increment_counter(1); + new_fractions -= max_fractions; } - } else { - None - }; + fractional_part = FractionalPart { + resolution, + counter: new_fractions, + } + } increment_counter(duration.as_secs() as u32); (new_counter, fractional_part) } -impl AddAssign for TimeProviderCcsdsEpoch { +impl AddAssign for CucTime { fn add_assign(&mut self, duration: Duration) { let (new_counter, new_fractional_part) = - get_provider_values_after_duration_addition(self, duration); + get_time_values_after_duration_addition(self, duration); self.counter.1 = new_counter; - if self.fractions.is_some() { - self.fractions = new_fractional_part; - } + self.fractions = new_fractional_part; } } -impl Add for TimeProviderCcsdsEpoch { +impl Add for CucTime { type Output = Self; fn add(self, duration: Duration) -> Self::Output { let (new_counter, new_fractional_part) = - get_provider_values_after_duration_addition(&self, duration); - if let Some(fractional_part) = new_fractional_part { - // The generated fractional part should always be valid, so its okay to unwrap here. - return Self::new_with_fractions(new_counter, fractional_part).unwrap(); - } - Self::new(new_counter) + get_time_values_after_duration_addition(&self, duration); + // The generated fractional part should always be valid, so its okay to unwrap here. + Self::new_with_fractions(new_counter, new_fractional_part).unwrap() } } -impl Add for &TimeProviderCcsdsEpoch { - type Output = TimeProviderCcsdsEpoch; +impl Add for &CucTime { + type Output = CucTime; fn add(self, duration: Duration) -> Self::Output { let (new_counter, new_fractional_part) = - get_provider_values_after_duration_addition(self, duration); - if let Some(fractional_part) = new_fractional_part { - // The generated fractional part should always be valid, so its okay to unwrap here. - return Self::Output::new_with_fractions(new_counter, fractional_part).unwrap(); - } - Self::Output::new(new_counter) + get_time_values_after_duration_addition(self, duration); + // The generated fractional part should always be valid, so its okay to unwrap here. + Self::Output::new_with_fractions(new_counter, new_fractional_part).unwrap() } } #[cfg(test)] mod tests { + use crate::time::{UnixTime, DAYS_CCSDS_TO_UNIX, SECONDS_PER_DAY}; + use super::*; use alloc::string::ToString; - use chrono::{Datelike, Timelike}; + use chrono::{Datelike, TimeZone, Timelike}; #[allow(unused_imports)] use std::println; + const LEAP_SECONDS: u32 = 37; + #[test] fn test_basic_zero_epoch() { - let zero_cuc = TimeProviderCcsdsEpoch::new(0); + // Do not include leap second corrections, which do not apply to dates before 1972. + let zero_cuc = CucTime::new(0); assert_eq!(zero_cuc.len_as_bytes(), 5); assert_eq!(zero_cuc.width(), zero_cuc.width_counter_pair().0); assert_eq!(zero_cuc.counter(), zero_cuc.width_counter_pair().1); - assert_eq!(zero_cuc.ccdsd_time_code(), CcsdsTimeCodes::CucCcsdsEpoch); + let ccsds_cuc = zero_cuc.to_leap_sec_helper(0); + assert_eq!(ccsds_cuc.ccdsd_time_code(), CcsdsTimeCode::CucCcsdsEpoch); let counter = zero_cuc.width_counter_pair(); assert_eq!(counter.0, 4); assert_eq!(counter.1, 0); - let fractions = zero_cuc.width_fractions_pair(); - assert!(fractions.is_none()); - let dt = zero_cuc.date_time(); - assert!(dt.is_some()); - let dt = dt.unwrap(); - assert_eq!(dt.year(), 1958); - assert_eq!(dt.month(), 1); - assert_eq!(dt.day(), 1); - assert_eq!(dt.hour(), 0); - assert_eq!(dt.minute(), 0); - assert_eq!(dt.second(), 0); + let fractions = zero_cuc.fractions(); + assert_eq!(fractions, FractionalPart::new_with_seconds_resolution()); + let dt = ccsds_cuc.chrono_date_time(); + if let chrono::LocalResult::Single(dt) = dt { + assert_eq!(dt.year(), 1958); + assert_eq!(dt.month(), 1); + assert_eq!(dt.day(), 1); + assert_eq!(dt.hour(), 0); + assert_eq!(dt.minute(), 0); + assert_eq!(dt.second(), 0); + } } #[test] fn test_write_no_fractions() { let mut buf: [u8; 16] = [0; 16]; - let zero_cuc = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(4, 0x20102030), None); + let zero_cuc = + CucTime::new_generic(WidthCounterPair(4, 0x20102030), FractionalPart::new_empty()); assert!(zero_cuc.is_ok()); let zero_cuc = zero_cuc.unwrap(); let res = zero_cuc.write_to_bytes(&mut buf); assert!(res.is_ok()); - assert_eq!(zero_cuc.subsecond_millis(), 0); + assert_eq!(zero_cuc.subsec_nanos(), 0); assert_eq!(zero_cuc.len_as_bytes(), 5); assert_eq!(pfield_len(buf[0]), 1); let written = res.unwrap(); @@ -771,7 +893,7 @@ mod tests { assert_eq!((buf[0] >> 7) & 0b1, 0); let time_code = ccsds_time_code_from_p_field(buf[0]); assert!(time_code.is_ok()); - assert_eq!(time_code.unwrap(), CcsdsTimeCodes::CucCcsdsEpoch); + assert_eq!(time_code.unwrap(), CcsdsTimeCode::CucCcsdsEpoch); assert_eq!((buf[0] >> 2) & 0b11, 0b11); assert_eq!(buf[0] & 0b11, 0); let raw_counter = u32::from_be_bytes(buf[1..5].try_into().unwrap()); @@ -781,37 +903,42 @@ mod tests { #[test] fn test_datetime_now() { - let now = Utc::now(); - let cuc_now = TimeProviderCcsdsEpoch::from_now(FractionalResolution::SixtyNs); + let now = chrono::Utc::now(); + let cuc_now = CucTime::from_now(FractionalResolution::SixtyNs, LEAP_SECONDS); assert!(cuc_now.is_ok()); let cuc_now = cuc_now.unwrap(); - let dt_opt = cuc_now.date_time(); - assert!(dt_opt.is_some()); - let dt = dt_opt.unwrap(); - let diff = dt - now; - assert!(diff.num_milliseconds() < 1000); - println!("datetime from cuc: {}", dt); - println!("datetime now: {}", now); + let ccsds_cuc = cuc_now.to_leap_sec_helper(LEAP_SECONDS); + let dt_opt = ccsds_cuc.chrono_date_time(); + if let chrono::LocalResult::Single(dt) = dt_opt { + let diff = dt - now; + assert!(diff.num_milliseconds() < 1000); + println!("datetime from cuc: {}", dt); + println!("datetime now: {}", now); + } else { + panic!("date time creation from now failed") + } } #[test] fn test_read_no_fractions() { let mut buf: [u8; 16] = [0; 16]; - let zero_cuc = - TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(4, 0x20102030), None).unwrap(); + let zero_cuc = CucTime::new_generic( + WidthCounterPair(4, 0x20102030), + FractionalPart::new_with_seconds_resolution(), + ) + .unwrap(); zero_cuc.write_to_bytes(&mut buf).unwrap(); - let cuc_read_back = - TimeProviderCcsdsEpoch::from_bytes(&buf).expect("reading cuc timestamp failed"); + let cuc_read_back = CucTime::from_bytes(&buf).expect("reading cuc timestamp failed"); assert_eq!(cuc_read_back, zero_cuc); assert_eq!(cuc_read_back.width_counter_pair().1, 0x20102030); - assert_eq!(cuc_read_back.width_fractions_pair(), None); + assert_eq!(cuc_read_back.fractions(), FractionalPart::new_empty()); } #[test] fn invalid_read_len() { let mut buf: [u8; 16] = [0; 16]; for i in 0..2 { - let res = TimeProviderCcsdsEpoch::from_bytes(&buf[0..i]); + let res = CucTime::from_bytes(&buf[0..i]); assert!(res.is_err()); let err = res.unwrap_err(); if let TimestampError::ByteConversion(ByteConversionError::FromSliceTooSmall { @@ -823,10 +950,10 @@ mod tests { assert_eq!(expected, 2); } } - let large_stamp = TimeProviderCcsdsEpoch::new_with_fine_fractions(22, 300).unwrap(); + let large_stamp = CucTime::new_with_fine_fractions(22, 300).unwrap(); large_stamp.write_to_bytes(&mut buf).unwrap(); for i in 2..large_stamp.len_as_bytes() - 1 { - let res = TimeProviderCcsdsEpoch::from_bytes(&buf[0..i]); + let res = CucTime::from_bytes(&buf[0..i]); assert!(res.is_err()); let err = res.unwrap_err(); if let TimestampError::ByteConversion(ByteConversionError::FromSliceTooSmall { @@ -843,7 +970,7 @@ mod tests { #[test] fn write_and_read_tiny_stamp() { let mut buf = [0; 2]; - let cuc = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(1, 200), None); + let cuc = CucTime::new_generic(WidthCounterPair(1, 200), FractionalPart::new_empty()); assert!(cuc.is_ok()); let cuc = cuc.unwrap(); assert_eq!(cuc.len_as_bytes(), 2); @@ -852,7 +979,7 @@ mod tests { let written = res.unwrap(); assert_eq!(written, 2); assert_eq!(buf[1], 200); - let cuc_read_back = TimeProviderCcsdsEpoch::from_bytes(&buf); + let cuc_read_back = CucTime::from_bytes(&buf); assert!(cuc_read_back.is_ok()); let cuc_read_back = cuc_read_back.unwrap(); assert_eq!(cuc_read_back, cuc); @@ -861,7 +988,7 @@ mod tests { #[test] fn write_slightly_larger_stamp() { let mut buf = [0; 4]; - let cuc = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(2, 40000), None); + let cuc = CucTime::new_generic(WidthCounterPair(2, 40000), FractionalPart::new_empty()); assert!(cuc.is_ok()); let cuc = cuc.unwrap(); assert_eq!(cuc.len_as_bytes(), 3); @@ -870,7 +997,7 @@ mod tests { let written = res.unwrap(); assert_eq!(written, 3); assert_eq!(u16::from_be_bytes(buf[1..3].try_into().unwrap()), 40000); - let cuc_read_back = TimeProviderCcsdsEpoch::from_bytes(&buf); + let cuc_read_back = CucTime::from_bytes(&buf); assert!(cuc_read_back.is_ok()); let cuc_read_back = cuc_read_back.unwrap(); assert_eq!(cuc_read_back, cuc); @@ -882,7 +1009,10 @@ mod tests { #[test] fn write_read_three_byte_cntr_stamp() { let mut buf = [0; 4]; - let cuc = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(3, 2_u32.pow(24) - 2), None); + let cuc = CucTime::new_generic( + WidthCounterPair(3, 2_u32.pow(24) - 2), + FractionalPart::new_empty(), + ); assert!(cuc.is_ok()); let cuc = cuc.unwrap(); assert_eq!(cuc.len_as_bytes(), 4); @@ -893,7 +1023,7 @@ mod tests { let mut temp_buf = [0; 4]; temp_buf[1..4].copy_from_slice(&buf[1..4]); assert_eq!(u32::from_be_bytes(temp_buf), 2_u32.pow(24) - 2); - let cuc_read_back = TimeProviderCcsdsEpoch::from_bytes(&buf); + let cuc_read_back = CucTime::from_bytes(&buf); assert!(cuc_read_back.is_ok()); let cuc_read_back = cuc_read_back.unwrap(); assert_eq!(cuc_read_back, cuc); @@ -902,7 +1032,7 @@ mod tests { #[test] fn test_write_invalid_buf() { let mut buf: [u8; 16] = [0; 16]; - let res = TimeProviderCcsdsEpoch::new_with_fine_fractions(0, 0); + let res = CucTime::new_with_fine_fractions(0, 0); let cuc = res.unwrap(); for i in 0..cuc.len_as_bytes() - 1 { let err = cuc.write_to_bytes(&mut buf[0..i]); @@ -923,13 +1053,13 @@ mod tests { #[test] fn invalid_ccsds_stamp_type() { let mut buf: [u8; 16] = [0; 16]; - buf[0] |= (CcsdsTimeCodes::CucAgencyEpoch as u8) << 4; - let res = TimeProviderCcsdsEpoch::from_bytes(&buf); + buf[0] |= (CcsdsTimeCode::CucAgencyEpoch as u8) << 4; + let res = CucTime::from_bytes(&buf); assert!(res.is_err()); let err = res.unwrap_err(); if let TimestampError::InvalidTimeCode { expected, found } = err { - assert_eq!(expected, CcsdsTimeCodes::CucCcsdsEpoch); - assert_eq!(found, CcsdsTimeCodes::CucAgencyEpoch as u8); + assert_eq!(expected, CcsdsTimeCode::CucCcsdsEpoch); + assert_eq!(found, CcsdsTimeCode::CucAgencyEpoch as u8); } else { panic!("unexpected error: {}", err); } @@ -938,10 +1068,9 @@ mod tests { #[test] fn test_write_with_coarse_fractions() { let mut buf: [u8; 16] = [0; 16]; - let cuc = TimeProviderCcsdsEpoch::new_with_coarse_fractions(0x30201060, 120); - assert!(cuc.fractions.is_some()); - assert_eq!(cuc.fractions.unwrap().1, 120); - assert_eq!(cuc.fractions.unwrap().0, FractionalResolution::FourMs); + let cuc = CucTime::new_with_coarse_fractions(0x30201060, 120); + assert_eq!(cuc.fractions().counter(), 120); + assert_eq!(cuc.fractions().resolution(), FractionalResolution::FourMs); let res = cuc.write_to_bytes(&mut buf); assert!(res.is_ok()); let written = res.unwrap(); @@ -957,10 +1086,10 @@ mod tests { #[test] fn test_read_with_coarse_fractions() { let mut buf: [u8; 16] = [0; 16]; - let cuc = TimeProviderCcsdsEpoch::new_with_coarse_fractions(0x30201060, 120); + let cuc = CucTime::new_with_coarse_fractions(0x30201060, 120); let res = cuc.write_to_bytes(&mut buf); assert!(res.is_ok()); - let res = TimeProviderCcsdsEpoch::from_bytes(&buf); + let res = CucTime::from_bytes(&buf); assert!(res.is_ok()); let read_back = res.unwrap(); assert_eq!(read_back, cuc); @@ -969,7 +1098,7 @@ mod tests { #[test] fn test_write_with_medium_fractions() { let mut buf: [u8; 16] = [0; 16]; - let cuc = TimeProviderCcsdsEpoch::new_with_medium_fractions(0x30303030, 30000); + let cuc = CucTime::new_with_medium_fractions(0x30303030, 30000); let res = cuc.write_to_bytes(&mut buf); assert!(res.is_ok()); let written = res.unwrap(); @@ -981,10 +1110,10 @@ mod tests { #[test] fn test_read_with_medium_fractions() { let mut buf: [u8; 16] = [0; 16]; - let cuc = TimeProviderCcsdsEpoch::new_with_medium_fractions(0x30303030, 30000); + let cuc = CucTime::new_with_medium_fractions(0x30303030, 30000); let res = cuc.write_to_bytes(&mut buf); assert!(res.is_ok()); - let res = TimeProviderCcsdsEpoch::from_bytes(&buf); + let res = CucTime::from_bytes(&buf); assert!(res.is_ok()); let cuc_read_back = res.unwrap(); assert_eq!(cuc_read_back, cuc); @@ -993,8 +1122,7 @@ mod tests { #[test] fn test_write_with_fine_fractions() { let mut buf: [u8; 16] = [0; 16]; - let cuc = - TimeProviderCcsdsEpoch::new_with_fine_fractions(0x30303030, u16::MAX as u32 + 60000); + let cuc = CucTime::new_with_fine_fractions(0x30303030, u16::MAX as u32 + 60000); assert!(cuc.is_ok()); let cuc = cuc.unwrap(); let res = cuc.write_to_bytes(&mut buf); @@ -1009,13 +1137,12 @@ mod tests { #[test] fn test_read_with_fine_fractions() { let mut buf: [u8; 16] = [0; 16]; - let cuc = - TimeProviderCcsdsEpoch::new_with_fine_fractions(0x30303030, u16::MAX as u32 + 60000); + let cuc = CucTime::new_with_fine_fractions(0x30303030, u16::MAX as u32 + 60000); assert!(cuc.is_ok()); let cuc = cuc.unwrap(); let res = cuc.write_to_bytes(&mut buf); assert!(res.is_ok()); - let res = TimeProviderCcsdsEpoch::from_bytes(&buf); + let res = CucTime::from_bytes(&buf); assert!(res.is_ok()); let cuc_read_back = res.unwrap(); assert_eq!(cuc_read_back, cuc); @@ -1023,44 +1150,49 @@ mod tests { #[test] fn test_fractional_converter() { - let ns = convert_fractional_part_to_ns(FractionalPart(FractionalResolution::FourMs, 2)); + let ns = convert_fractional_part_to_ns(FractionalPart { + resolution: FractionalResolution::FourMs, + counter: 2, + }); // The formula for this is 2/255 * 10e9 = 7.843.137. assert_eq!(ns, 7843137); // This is the largest value we should be able to pass without this function panicking. - let ns = convert_fractional_part_to_ns(FractionalPart( - FractionalResolution::SixtyNs, - 2_u32.pow(24) - 2, - )); + let ns = convert_fractional_part_to_ns(FractionalPart { + resolution: FractionalResolution::SixtyNs, + counter: 2_u32.pow(24) - 2, + }); assert_eq!(ns, 999999940); } #[test] #[should_panic] fn test_fractional_converter_invalid_input() { - convert_fractional_part_to_ns(FractionalPart(FractionalResolution::FourMs, 256)); + convert_fractional_part_to_ns(FractionalPart { + resolution: FractionalResolution::FourMs, + counter: 256, + }); } #[test] #[should_panic] fn test_fractional_converter_invalid_input_2() { - convert_fractional_part_to_ns(FractionalPart( - FractionalResolution::SixtyNs, - 2_u32.pow(32) - 1, - )); + convert_fractional_part_to_ns(FractionalPart { + resolution: FractionalResolution::SixtyNs, + counter: 2_u32.pow(32) - 1, + }); } #[test] fn fractional_part_formula() { - let fractional_part = - fractional_part_from_subsec_ns(FractionalResolution::FourMs, 7843138).unwrap(); - assert_eq!(fractional_part.1, 2); + let fractional_part = fractional_part_from_subsec_ns(FractionalResolution::FourMs, 7843138); + assert_eq!(fractional_part.counter, 2); } #[test] fn fractional_part_formula_2() { let fractional_part = - fractional_part_from_subsec_ns(FractionalResolution::FourMs, 12000000).unwrap(); - assert_eq!(fractional_part.1, 3); + fractional_part_from_subsec_ns(FractionalResolution::FourMs, 12000000); + assert_eq!(fractional_part.counter, 3); } #[test] @@ -1073,69 +1205,109 @@ mod tests { let fractional_part = fractional_part_from_subsec_ns( FractionalResolution::FifteenUs, hundred_fractions_and_some, - ) - .unwrap(); - assert_eq!(fractional_part.1, 100); + ); + assert_eq!(fractional_part.counter, 100); // Using exactly 101.0 can yield values which will later be rounded down to 100 let hundred_and_one_fractions = (101.001 * one_fraction_with_width_two_in_ns).floor() as u64; let fractional_part = fractional_part_from_subsec_ns( FractionalResolution::FifteenUs, hundred_and_one_fractions, - ) - .unwrap(); - assert_eq!(fractional_part.1, 101); + ); + assert_eq!(fractional_part.counter, 101); } #[test] fn update_fractions() { - let mut stamp = TimeProviderCcsdsEpoch::new(2000); - let res = stamp.set_fractions(FractionalPart(FractionalResolution::SixtyNs, 5000)); + let mut stamp = CucTime::new(2000); + let res = stamp.set_fractions(FractionalPart { + resolution: FractionalResolution::SixtyNs, + counter: 5000, + }); assert!(res.is_ok()); - assert!(stamp.fractions.is_some()); - let fractions = stamp.fractions.unwrap(); - assert_eq!(fractions.0, FractionalResolution::SixtyNs); - assert_eq!(fractions.1, 5000); + assert_eq!( + stamp.fractions().resolution(), + FractionalResolution::SixtyNs + ); + assert_eq!(stamp.fractions().counter(), 5000); } #[test] fn set_fract_resolution() { - let mut stamp = TimeProviderCcsdsEpoch::new(2000); + let mut stamp = CucTime::new(2000); stamp.set_fractional_resolution(FractionalResolution::SixtyNs); - assert!(stamp.fractions.is_some()); - let fractions = stamp.fractions.unwrap(); - assert_eq!(fractions.0, FractionalResolution::SixtyNs); - assert_eq!(fractions.1, 0); - let res = stamp.update_from_now(); + assert_eq!( + stamp.fractions().resolution(), + FractionalResolution::SixtyNs + ); + assert_eq!(stamp.fractions().counter(), 0); + let res = stamp.update_from_now(LEAP_SECONDS); assert!(res.is_ok()); } #[test] - fn assert_largest_fractions() { - let fractions = - fractional_part_from_subsec_ns(FractionalResolution::SixtyNs, 10u64.pow(9) - 1) - .unwrap(); - // The value can not be larger than representable by 3 bytes - // Assert that the maximum resolution can be reached - assert_eq!(fractions.1, 2_u32.pow(3 * 8) - 2); + fn test_small_fraction_floored_to_zero() { + let fractions = fractional_part_from_subsec_ns(FractionalResolution::SixtyNs, 59); + assert_eq!(fractions.counter, 0); } - fn check_stamp_after_addition(cuc_stamp: &TimeProviderCcsdsEpoch) { + #[test] + fn test_small_fraction_becomes_fractional_part() { + let fractions = fractional_part_from_subsec_ns(FractionalResolution::SixtyNs, 61); + assert_eq!(fractions.counter, 1); + } + + #[test] + fn test_smallest_resolution_small_nanoseconds_floored_to_zero() { + let fractions = + fractional_part_from_subsec_ns(FractionalResolution::FourMs, 3800 * 1e3 as u64); + assert_eq!(fractions.counter, 0); + } + + #[test] + fn test_smallest_resolution_small_nanoseconds_becomes_one_fraction() { + let fractions = + fractional_part_from_subsec_ns(FractionalResolution::FourMs, 4000 * 1e3 as u64); + assert_eq!(fractions.counter, 1); + } + + #[test] + fn test_smallest_resolution_large_nanoseconds_becomes_largest_fraction() { + let fractions = + fractional_part_from_subsec_ns(FractionalResolution::FourMs, 10u64.pow(9) - 1); + assert_eq!(fractions.counter, 2_u32.pow(8) - 1); + } + + #[test] + fn test_largest_fractions_with_largest_resolution() { + let fractions = + fractional_part_from_subsec_ns(FractionalResolution::SixtyNs, 10u64.pow(9) - 1); + // The value can not be larger than representable by 3 bytes + // Assert that the maximum resolution can be reached + assert_eq!(fractions.counter, 2_u32.pow(3 * 8) - 1); + } + + fn check_stamp_after_addition(cuc_stamp: &CucTime) { + let cuc_with_leaps = cuc_stamp.to_leap_sec_helper(LEAP_SECONDS); + assert_eq!( + cuc_with_leaps.ccdsd_time_code(), + CcsdsTimeCode::CucCcsdsEpoch + ); assert_eq!(cuc_stamp.width_counter_pair().1, 202); - let fractions = cuc_stamp.width_fractions_pair().unwrap().1; + let fractions = cuc_stamp.fractions().counter(); let expected_val = - (0.5 * fractional_res_to_div(FractionalResolution::FifteenUs) as f64).floor() as u32; + (0.5 * fractional_res_to_div(FractionalResolution::FifteenUs) as f64).ceil() as u32; assert_eq!(fractions, expected_val); let cuc_stamp2 = cuc_stamp + Duration::from_millis(501); // What I would roughly expect assert_eq!(cuc_stamp2.counter.1, 203); - assert!(cuc_stamp2.fractions.unwrap().1 < 100); - assert!(cuc_stamp2.subsecond_millis() <= 1); + assert!(cuc_stamp2.fractions().counter() < 100); + assert!(cuc_stamp2.subsec_millis() <= 1); } #[test] fn add_duration_basic() { - let mut cuc_stamp = TimeProviderCcsdsEpoch::new(200); + let mut cuc_stamp = CucTime::new(200); cuc_stamp.set_fractional_resolution(FractionalResolution::FifteenUs); let duration = Duration::from_millis(2500); cuc_stamp += duration; @@ -1144,7 +1316,7 @@ mod tests { #[test] fn add_duration_basic_on_ref() { - let mut cuc_stamp = TimeProviderCcsdsEpoch::new(200); + let mut cuc_stamp = CucTime::new(200); cuc_stamp.set_fractional_resolution(FractionalResolution::FifteenUs); let duration = Duration::from_millis(2500); let new_stamp = cuc_stamp + duration; @@ -1153,25 +1325,25 @@ mod tests { #[test] fn add_duration_basic_no_fractions() { - let mut cuc_stamp = TimeProviderCcsdsEpoch::new(200); + let mut cuc_stamp = CucTime::new(200); let duration = Duration::from_millis(2000); cuc_stamp += duration; assert_eq!(cuc_stamp.counter(), 202); - assert_eq!(cuc_stamp.width_fractions_pair(), None); + assert_eq!(cuc_stamp.fractions(), FractionalPart::new_empty()); } #[test] fn add_duration_basic_on_ref_no_fractions() { - let cuc_stamp = TimeProviderCcsdsEpoch::new(200); + let cuc_stamp = CucTime::new(200); let duration = Duration::from_millis(2000); let new_stamp = cuc_stamp + duration; assert_eq!(new_stamp.counter(), 202); - assert_eq!(new_stamp.width_fractions_pair(), None); + assert_eq!(new_stamp.fractions(), FractionalPart::new_empty()); } #[test] fn add_duration_overflow() { let mut cuc_stamp = - TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(1, 255), None).unwrap(); + CucTime::new_generic(WidthCounterPair(1, 255), FractionalPart::new_empty()).unwrap(); let duration = Duration::from_secs(10); cuc_stamp += duration; assert_eq!(cuc_stamp.counter.1, 10); @@ -1179,7 +1351,7 @@ mod tests { #[test] fn test_invalid_width_param() { - let error = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(8, 0), None); + let error = CucTime::new_generic(WidthCounterPair(8, 0), FractionalPart::new_empty()); assert!(error.is_err()); let error = error.unwrap_err(); if let CucError::InvalidCounterWidth(width) = error { @@ -1192,34 +1364,34 @@ mod tests { #[test] fn test_from_dt() { - let dt = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(); - let cuc = - TimeProviderCcsdsEpoch::from_date_time(&dt, FractionalResolution::Seconds).unwrap(); - assert_eq!(cuc.counter(), dt.timestamp() as u32); + let dt = chrono::Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(); + let cuc = CucTime::from_chrono_date_time(&dt, FractionalResolution::Seconds, LEAP_SECONDS) + .unwrap(); + assert_eq!(cuc.counter(), dt.timestamp() as u32 + LEAP_SECONDS); } #[test] fn test_new_u16_width() { - let cuc = TimeProviderCcsdsEpoch::new_u16_counter(0); + let cuc = CucTime::new_u16_counter(0); assert_eq!(cuc.width(), 2); assert_eq!(cuc.counter(), 0); } #[test] fn from_unix_stamp() { - let unix_stamp = UnixTimestamp::new(0, 0).unwrap(); + let unix_stamp = UnixTime::new(0, 0); let cuc = - TimeProviderCcsdsEpoch::from_unix_stamp(&unix_stamp, FractionalResolution::Seconds) + CucTime::from_unix_stamp(&unix_stamp, FractionalResolution::Seconds, LEAP_SECONDS) .expect("failed to create cuc from unix stamp"); assert_eq!( cuc.counter(), - (-DAYS_CCSDS_TO_UNIX * SECONDS_PER_DAY as i32) as u32 + (-DAYS_CCSDS_TO_UNIX * SECONDS_PER_DAY as i32) as u32 + LEAP_SECONDS ); } #[test] fn test_invalid_counter() { - let cuc_error = TimeProviderCcsdsEpoch::new_generic(WidthCounterPair(1, 256), None); + let cuc_error = CucTime::new_generic(WidthCounterPair(1, 256), FractionalPart::new_empty()); assert!(cuc_error.is_err()); let cuc_error = cuc_error.unwrap_err(); if let CucError::InvalidCounter { width, counter } = cuc_error { @@ -1233,7 +1405,7 @@ mod tests { #[test] fn test_stamp_to_vec() { - let stamp = TimeProviderCcsdsEpoch::new_u16_counter(100); + let stamp = CucTime::new_u16_counter(100); let stamp_vec = stamp.to_vec().unwrap(); let mut buf: [u8; 16] = [0; 16]; stamp.write_to_bytes(&mut buf).unwrap(); diff --git a/src/time/mod.rs b/src/time/mod.rs index c231404..96f6094 100644 --- a/src/time/mod.rs +++ b/src/time/mod.rs @@ -1,6 +1,7 @@ //! CCSDS Time Code Formats according to [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) use crate::ByteConversionError; -use chrono::{DateTime, LocalResult, TimeZone, Utc}; +#[cfg(feature = "chrono")] +use chrono::{TimeZone, Utc}; use core::cmp::Ordering; use core::fmt::{Display, Formatter}; use core::ops::{Add, AddAssign, Sub}; @@ -13,6 +14,7 @@ use num_traits::float::FloatCore; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; + #[cfg(feature = "std")] use std::error::Error; #[cfg(feature = "std")] @@ -27,10 +29,11 @@ pub mod cuc; pub const DAYS_CCSDS_TO_UNIX: i32 = -4383; pub const SECONDS_PER_DAY: u32 = 86400; pub const MS_PER_DAY: u32 = SECONDS_PER_DAY * 1000; +pub const NANOS_PER_SECOND: u32 = 1_000_000_000; #[derive(Debug, PartialEq, Eq, Copy, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum CcsdsTimeCodes { +pub enum CcsdsTimeCode { CucCcsdsEpoch = 0b001, CucAgencyEpoch = 0b010, Cds = 0b100, @@ -38,16 +41,16 @@ pub enum CcsdsTimeCodes { AgencyDefined = 0b110, } -impl TryFrom for CcsdsTimeCodes { +impl TryFrom for CcsdsTimeCode { type Error = (); fn try_from(value: u8) -> Result { match value { - x if x == CcsdsTimeCodes::CucCcsdsEpoch as u8 => Ok(CcsdsTimeCodes::CucCcsdsEpoch), - x if x == CcsdsTimeCodes::CucAgencyEpoch as u8 => Ok(CcsdsTimeCodes::CucAgencyEpoch), - x if x == CcsdsTimeCodes::Cds as u8 => Ok(CcsdsTimeCodes::Cds), - x if x == CcsdsTimeCodes::Ccs as u8 => Ok(CcsdsTimeCodes::Ccs), - x if x == CcsdsTimeCodes::AgencyDefined as u8 => Ok(CcsdsTimeCodes::AgencyDefined), + x if x == CcsdsTimeCode::CucCcsdsEpoch as u8 => Ok(CcsdsTimeCode::CucCcsdsEpoch), + x if x == CcsdsTimeCode::CucAgencyEpoch as u8 => Ok(CcsdsTimeCode::CucAgencyEpoch), + x if x == CcsdsTimeCode::Cds as u8 => Ok(CcsdsTimeCode::Cds), + x if x == CcsdsTimeCode::Ccs as u8 => Ok(CcsdsTimeCode::Ccs), + x if x == CcsdsTimeCode::AgencyDefined as u8 => Ok(CcsdsTimeCode::AgencyDefined), _ => Err(()), } } @@ -55,20 +58,20 @@ impl TryFrom for CcsdsTimeCodes { /// Retrieve the CCSDS time code from the p-field. If no valid time code identifier is found, the /// value of the raw time code identification field is returned. -pub fn ccsds_time_code_from_p_field(pfield: u8) -> Result { +pub fn ccsds_time_code_from_p_field(pfield: u8) -> Result { let raw_bits = (pfield >> 4) & 0b111; - CcsdsTimeCodes::try_from(raw_bits).map_err(|_| raw_bits) + CcsdsTimeCode::try_from(raw_bits).map_err(|_| raw_bits) } #[derive(Debug, PartialEq, Eq, Copy, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[non_exhaustive] pub enum TimestampError { - InvalidTimeCode { expected: CcsdsTimeCodes, found: u8 }, + InvalidTimeCode { expected: CcsdsTimeCode, found: u8 }, ByteConversion(ByteConversionError), Cds(cds::CdsError), Cuc(cuc::CucError), - DateBeforeCcsdsEpoch(DateTime), + DateBeforeCcsdsEpoch(UnixTime), CustomEpochNotSupported, } @@ -91,7 +94,7 @@ impl Display for TimestampError { write!(f, "time stamp: {e}") } TimestampError::DateBeforeCcsdsEpoch(e) => { - write!(f, "datetime with date before ccsds epoch: {e}") + write!(f, "datetime with date before ccsds epoch: {e:?}") } TimestampError::CustomEpochNotSupported => { write!(f, "custom epochs are not supported") @@ -204,16 +207,15 @@ pub trait TimeWriter { } } -pub trait TimeReader { - fn from_bytes(buf: &[u8]) -> Result - where - Self: Sized; +pub trait TimeReader: Sized { + fn from_bytes(buf: &[u8]) -> Result; } /// Trait for generic CCSDS time providers. /// -/// The UNIX helper methods and the [Self::date_time] method are not strictly necessary but extremely +/// The UNIX helper methods and the helper method are not strictly necessary but extremely /// practical because they are a very common and simple exchange format for time information. +/// Therefore, it was decided to keep them in this trait as well. pub trait CcsdsTimeProvider { fn len_as_bytes(&self) -> usize; @@ -222,56 +224,120 @@ pub trait CcsdsTimeProvider { /// entry denotes the length of the pfield and the second entry is the value of the pfield /// in big endian format. fn p_field(&self) -> (usize, [u8; 2]); - fn ccdsd_time_code(&self) -> CcsdsTimeCodes; + fn ccdsd_time_code(&self) -> CcsdsTimeCode; - fn unix_seconds(&self) -> i64; - fn subsecond_millis(&self) -> u16; - fn unix_stamp(&self) -> UnixTimestamp { - UnixTimestamp::const_new(self.unix_seconds(), self.subsecond_millis()) + fn unix_secs(&self) -> i64; + fn subsec_nanos(&self) -> u32; + + fn subsec_millis(&self) -> u16 { + (self.subsec_nanos() / 1_000_000) as u16 } - fn date_time(&self) -> Option>; + fn unix_time(&self) -> UnixTime { + UnixTime::new(self.unix_secs(), self.subsec_nanos()) + } + + #[cfg(feature = "chrono")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "chrono")))] + fn chrono_date_time(&self) -> chrono::LocalResult> { + chrono::Utc.timestamp_opt(self.unix_secs(), self.subsec_nanos()) + } + + #[cfg(feature = "timelib")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "timelib")))] + fn timelib_date_time(&self) -> Result { + Ok(time::OffsetDateTime::from_unix_timestamp(self.unix_secs())? + + time::Duration::nanoseconds(self.subsec_nanos().into())) + } } -/// UNIX timestamp: Elapsed seconds since 1970-01-01T00:00:00+00:00. +/// UNIX time: Elapsed non-leap seconds since 1970-01-01T00:00:00+00:00 UTC. /// -/// Also can optionally include subsecond millisecond for greater accuracy. +/// This is a commonly used time format and can therefore also be used as a generic format to +/// convert other CCSDS time formats to and from. The subsecond precision is in nanoseconds +/// similarly to other common time formats and libraries. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct UnixTimestamp { - pub unix_seconds: i64, - subsecond_millis: u16, +pub struct UnixTime { + secs: i64, + subsec_nanos: u32, } -impl UnixTimestamp { - /// Returns [None] if the subsecond millisecond value is larger than 999. - pub fn new(unix_seconds: i64, subsec_millis: u16) -> Option { - if subsec_millis > 999 { +impl UnixTime { + /// The UNIX epoch time: 1970-01-01T00:00:00+00:00 UTC. + pub const EPOCH: Self = Self { + secs: 0, + subsec_nanos: 0, + }; + + /// The minimum possible `UnixTime`. + pub const MIN: Self = Self { + secs: i64::MIN, + subsec_nanos: 0, + }; + + /// The maximum possible `UnixTime`. + pub const MAX: Self = Self { + secs: i64::MAX, + subsec_nanos: NANOS_PER_SECOND - 1, + }; + + /// Returns [None] if the subsecond nanosecond value is invalid (larger than fraction of a + /// second) + pub fn new_checked(unix_seconds: i64, subsec_nanos: u32) -> Option { + if subsec_nanos >= NANOS_PER_SECOND { return None; } - Some(Self::const_new(unix_seconds, subsec_millis)) + Some(Self::new(unix_seconds, subsec_nanos)) } - /// Like [Self::new] but const. Panics if the subsecond value is larger than 999. - pub const fn const_new(unix_seconds: i64, subsecond_millis: u16) -> Self { - if subsecond_millis > 999 { - panic!("subsec milliseconds exceeds 999"); + /// Returns [None] if the subsecond millisecond value is invalid (larger than fraction of a + /// second) + pub fn new_subsec_millis_checked(unix_seconds: i64, subsec_millis: u16) -> Option { + if subsec_millis >= 1000 { + return None; + } + Self::new_checked(unix_seconds, subsec_millis as u32 * 1_000_000) + } + + /// This function will panic if the subsecond value is larger than the fraction of a second. + /// Use [Self::new_checked] if you want to handle this case without a panic. + pub const fn new(unix_seconds: i64, subsecond_nanos: u32) -> Self { + if subsecond_nanos >= NANOS_PER_SECOND { + panic!("invalid subsecond nanos value"); } Self { - unix_seconds, - subsecond_millis, + secs: unix_seconds, + subsec_nanos: subsecond_nanos, } } - pub fn new_only_seconds(unix_seconds: i64) -> Self { + /// This function will panic if the subsecond value is larger than the fraction of a second. + /// Use [Self::new_subsec_millis_checked] if you want to handle this case without a panic. + pub const fn new_subsec_millis(unix_seconds: i64, subsecond_millis: u16) -> Self { + if subsecond_millis >= 1000 { + panic!("invalid subsecond millisecond value"); + } Self { - unix_seconds, - subsecond_millis: 0, + secs: unix_seconds, + subsec_nanos: subsecond_millis as u32 * 1_000_000, } } - pub fn subsecond_millis(&self) -> u16 { - self.subsecond_millis + pub fn new_only_secs(unix_seconds: i64) -> Self { + Self { + secs: unix_seconds, + subsec_nanos: 0, + } + } + + #[inline] + pub fn subsec_millis(&self) -> u16 { + (self.subsec_nanos / 1_000_000) as u16 + } + + pub fn subsec_nanos(&self) -> u32 { + self.subsec_nanos } #[cfg(feature = "std")] @@ -279,68 +345,88 @@ impl UnixTimestamp { pub fn from_now() -> Result { let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; let epoch = now.as_secs(); - Ok(Self::const_new(epoch as i64, now.subsec_millis() as u16)) + Ok(Self::new(epoch as i64, now.subsec_nanos())) } #[inline] - pub fn unix_seconds_f64(&self) -> f64 { - self.unix_seconds as f64 + (self.subsecond_millis as f64 / 1000.0) + pub fn unix_secs_f64(&self) -> f64 { + self.secs as f64 + (self.subsec_nanos as f64 / 1_000_000_000.0) } - pub fn as_date_time(&self) -> LocalResult> { - Utc.timestamp_opt( - self.unix_seconds, - self.subsecond_millis as u32 * 10_u32.pow(6), - ) + pub fn secs(&self) -> i64 { + self.secs + } + + #[cfg(feature = "chrono")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "chrono")))] + pub fn chrono_date_time(&self) -> chrono::LocalResult> { + Utc.timestamp_opt(self.secs, self.subsec_nanos) + } + + #[cfg(feature = "timelib")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "timelib")))] + pub fn timelib_date_time(&self) -> Result { + Ok(time::OffsetDateTime::from_unix_timestamp(self.secs())? + + time::Duration::nanoseconds(self.subsec_nanos().into())) } // Calculate the difference in milliseconds between two UnixTimestamps - pub fn difference_in_millis(&self, other: &UnixTimestamp) -> i64 { - let seconds_difference = self.unix_seconds - other.unix_seconds; + pub fn diff_in_millis(&self, other: &UnixTime) -> Option { + let seconds_difference = self.secs.checked_sub(other.secs)?; // Convert seconds difference to milliseconds - let milliseconds_difference = seconds_difference * 1000; + let milliseconds_difference = seconds_difference.checked_mul(1000)?; // Calculate the difference in subsecond milliseconds directly - let subsecond_difference = self.subsecond_millis as i64 - other.subsecond_millis as i64; + let subsecond_difference_nanos = self.subsec_nanos as i64 - other.subsec_nanos as i64; // Combine the differences - milliseconds_difference + subsecond_difference + Some(milliseconds_difference + (subsecond_difference_nanos / 1_000_000)) } } -impl From> for UnixTimestamp { - fn from(value: DateTime) -> Self { - Self::const_new(value.timestamp(), value.timestamp_subsec_millis() as u16) +#[cfg(feature = "chrono")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "chrono")))] +impl From> for UnixTime { + fn from(value: chrono::DateTime) -> Self { + Self::new(value.timestamp(), value.timestamp_subsec_nanos()) } } -impl PartialOrd for UnixTimestamp { +#[cfg(feature = "timelib")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "timelib")))] +impl From for UnixTime { + fn from(value: time::OffsetDateTime) -> Self { + Self::new(value.unix_timestamp(), value.nanosecond()) + } +} + +impl PartialOrd for UnixTime { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Ord for UnixTimestamp { +impl Ord for UnixTime { fn cmp(&self, other: &Self) -> Ordering { if self == other { return Ordering::Equal; } - match self.unix_seconds.cmp(&other.unix_seconds) { + match self.secs.cmp(&other.secs) { Ordering::Less => return Ordering::Less, Ordering::Greater => return Ordering::Greater, _ => (), } - match self.subsecond_millis().cmp(&other.subsecond_millis()) { + match self.subsec_millis().cmp(&other.subsec_millis()) { Ordering::Less => { - return if self.unix_seconds < 0 { + return if self.secs < 0 { Ordering::Greater } else { Ordering::Less } } Ordering::Greater => { - return if self.unix_seconds < 0 { + return if self.secs < 0 { Ordering::Less } else { Ordering::Greater @@ -360,12 +446,12 @@ pub struct StampDiff { pub duration_absolute: Duration, } -impl Sub for UnixTimestamp { - type Output = StampDiff; +impl Sub for UnixTime { + type Output = Option; fn sub(self, rhs: Self) -> Self::Output { - let difference = self.difference_in_millis(&rhs); - if difference < 0 { + let difference = self.diff_in_millis(&rhs)?; + Some(if difference < 0 { StampDiff { positive_duration: false, duration_absolute: Duration::from_millis(-difference as u64), @@ -375,16 +461,13 @@ impl Sub for UnixTimestamp { positive_duration: true, duration_absolute: Duration::from_millis(difference as u64), } - } + }) } } -fn get_new_stamp_after_addition( - current_stamp: &UnixTimestamp, - duration: Duration, -) -> UnixTimestamp { - let mut new_subsec_millis = current_stamp.subsecond_millis() + duration.subsec_millis() as u16; - let mut new_unix_seconds = current_stamp.unix_seconds; +fn get_new_stamp_after_addition(current_stamp: &UnixTime, duration: Duration) -> UnixTime { + let mut new_subsec_nanos = current_stamp.subsec_nanos() + duration.subsec_nanos(); + let mut new_unix_seconds = current_stamp.secs; let mut increment_seconds = |value: u32| { if new_unix_seconds < 0 { new_unix_seconds = new_unix_seconds @@ -396,8 +479,8 @@ fn get_new_stamp_after_addition( .expect("new unix seconds would exceed i64::MAX"); } }; - if new_subsec_millis >= 1000 { - new_subsec_millis -= 1000; + if new_subsec_nanos >= 1_000_000_000 { + new_subsec_nanos -= 1_000_000_000; increment_seconds(1); } increment_seconds( @@ -406,7 +489,7 @@ fn get_new_stamp_after_addition( .try_into() .expect("duration seconds exceeds u32::MAX"), ); - UnixTimestamp::const_new(new_unix_seconds, new_subsec_millis) + UnixTime::new(new_unix_seconds, new_subsec_nanos) } /// Please note that this operation will panic on the following conditions: @@ -414,7 +497,7 @@ fn get_new_stamp_after_addition( /// - Unix seconds after subtraction for stamps before the unix epoch exceeds [i64::MIN]. /// - Unix seconds after addition exceeds [i64::MAX]. /// - Seconds from duration to add exceeds [u32::MAX]. -impl AddAssign for UnixTimestamp { +impl AddAssign for UnixTime { fn add_assign(&mut self, duration: Duration) { *self = get_new_stamp_after_addition(self, duration); } @@ -425,7 +508,7 @@ impl AddAssign for UnixTimestamp { /// - Unix seconds after subtraction for stamps before the unix epoch exceeds [i64::MIN]. /// - Unix seconds after addition exceeds [i64::MAX]. /// - Unix seconds exceeds [u32::MAX]. -impl Add for UnixTimestamp { +impl Add for UnixTime { type Output = Self; fn add(self, duration: Duration) -> Self::Output { @@ -433,8 +516,8 @@ impl Add for UnixTimestamp { } } -impl Add for &UnixTimestamp { - type Output = UnixTimestamp; +impl Add for &UnixTime { + type Output = UnixTime; fn add(self, duration: Duration) -> Self::Output { get_new_stamp_after_addition(self, duration) @@ -445,10 +528,15 @@ impl Add for &UnixTimestamp { mod tests { use alloc::string::ToString; use chrono::{Datelike, Timelike}; - use std::format; + use std::{format, println}; use super::{cuc::CucError, *}; + #[allow(dead_code)] + const UNIX_STAMP_CONST: UnixTime = UnixTime::new(5, 999_999_999); + #[allow(dead_code)] + const UNIX_STAMP_CONST_2: UnixTime = UnixTime::new_subsec_millis(5, 999); + #[test] fn test_days_conversion() { assert_eq!(unix_to_ccsds_days(DAYS_CCSDS_TO_UNIX.into()), 0); @@ -484,28 +572,29 @@ mod tests { #[test] fn basic_unix_stamp_test() { - let stamp = UnixTimestamp::new_only_seconds(-200); - assert_eq!(stamp.unix_seconds, -200); - assert_eq!(stamp.subsecond_millis(), 0); - let stamp = UnixTimestamp::new_only_seconds(250); - assert_eq!(stamp.unix_seconds, 250); - assert_eq!(stamp.subsecond_millis(), 0); + let stamp = UnixTime::new_only_secs(-200); + assert_eq!(stamp.secs, -200); + assert_eq!(stamp.subsec_millis(), 0); + let stamp = UnixTime::new_only_secs(250); + assert_eq!(stamp.secs, 250); + assert_eq!(stamp.subsec_millis(), 0); } #[test] fn basic_float_unix_stamp_test() { - let stamp = UnixTimestamp::new(500, 600).unwrap(); - assert_eq!(stamp.unix_seconds, 500); - let subsec_millis = stamp.subsecond_millis(); + let stamp = UnixTime::new_subsec_millis_checked(500, 600).unwrap(); + assert_eq!(stamp.secs, 500); + let subsec_millis = stamp.subsec_millis(); assert_eq!(subsec_millis, 600); - assert!((500.6 - stamp.unix_seconds_f64()).abs() < 0.0001); + println!("{:?}", (500.6 - stamp.unix_secs_f64()).to_string()); + assert!((500.6 - stamp.unix_secs_f64()).abs() < 0.0001); } #[test] fn test_ord_larger() { - let stamp0 = UnixTimestamp::new_only_seconds(5); - let stamp1 = UnixTimestamp::new(5, 500).unwrap(); - let stamp2 = UnixTimestamp::new_only_seconds(6); + let stamp0 = UnixTime::new_only_secs(5); + let stamp1 = UnixTime::new_subsec_millis_checked(5, 500).unwrap(); + let stamp2 = UnixTime::new_only_secs(6); assert!(stamp1 > stamp0); assert!(stamp2 > stamp0); assert!(stamp2 > stamp1); @@ -513,9 +602,9 @@ mod tests { #[test] fn test_ord_smaller() { - let stamp0 = UnixTimestamp::new_only_seconds(5); - let stamp1 = UnixTimestamp::new(5, 500).unwrap(); - let stamp2 = UnixTimestamp::new_only_seconds(6); + let stamp0 = UnixTime::new_only_secs(5); + let stamp1 = UnixTime::new_subsec_millis_checked(5, 500).unwrap(); + let stamp2 = UnixTime::new_only_secs(6); assert!(stamp0 < stamp1); assert!(stamp0 < stamp2); assert!(stamp1 < stamp2); @@ -523,9 +612,9 @@ mod tests { #[test] fn test_ord_larger_neg_numbers() { - let stamp0 = UnixTimestamp::new_only_seconds(-5); - let stamp1 = UnixTimestamp::new(-5, 500).unwrap(); - let stamp2 = UnixTimestamp::new_only_seconds(-6); + let stamp0 = UnixTime::new_only_secs(-5); + let stamp1 = UnixTime::new_subsec_millis_checked(-5, 500).unwrap(); + let stamp2 = UnixTime::new_only_secs(-6); assert!(stamp0 > stamp1); assert!(stamp0 > stamp2); assert!(stamp1 > stamp2); @@ -535,9 +624,9 @@ mod tests { #[test] fn test_ord_smaller_neg_numbers() { - let stamp0 = UnixTimestamp::new_only_seconds(-5); - let stamp1 = UnixTimestamp::new(-5, 500).unwrap(); - let stamp2 = UnixTimestamp::new_only_seconds(-6); + let stamp0 = UnixTime::new_only_secs(-5); + let stamp1 = UnixTime::new_subsec_millis_checked(-5, 500).unwrap(); + let stamp2 = UnixTime::new_only_secs(-6); assert!(stamp2 < stamp1); assert!(stamp2 < stamp0); assert!(stamp1 < stamp0); @@ -548,8 +637,8 @@ mod tests { #[allow(clippy::nonminimal_bool)] #[test] fn test_eq() { - let stamp0 = UnixTimestamp::new(5, 0).unwrap(); - let stamp1 = UnixTimestamp::new_only_seconds(5); + let stamp0 = UnixTime::new(5, 0); + let stamp1 = UnixTime::new_only_secs(5); assert_eq!(stamp0, stamp1); assert!(stamp0 <= stamp1); assert!(stamp0 >= stamp1); @@ -559,27 +648,27 @@ mod tests { #[test] fn test_addition() { - let mut stamp0 = UnixTimestamp::new_only_seconds(1); + let mut stamp0 = UnixTime::new_only_secs(1); stamp0 += Duration::from_secs(5); - assert_eq!(stamp0.unix_seconds, 6); - assert_eq!(stamp0.subsecond_millis(), 0); + assert_eq!(stamp0.secs(), 6); + assert_eq!(stamp0.subsec_millis(), 0); let stamp1 = stamp0 + Duration::from_millis(500); - assert_eq!(stamp1.unix_seconds, 6); - assert_eq!(stamp1.subsecond_millis(), 500); + assert_eq!(stamp1.secs, 6); + assert_eq!(stamp1.subsec_millis(), 500); } #[test] fn test_addition_on_ref() { - let stamp0 = &UnixTimestamp::new(20, 500).unwrap(); + let stamp0 = &UnixTime::new_subsec_millis_checked(20, 500).unwrap(); let stamp1 = stamp0 + Duration::from_millis(2500); - assert_eq!(stamp1.unix_seconds, 23); - assert_eq!(stamp1.subsecond_millis(), 0); + assert_eq!(stamp1.secs, 23); + assert_eq!(stamp1.subsec_millis(), 0); } #[test] fn test_as_dt() { - let stamp = UnixTimestamp::new_only_seconds(0); - let dt = stamp.as_date_time().unwrap(); + let stamp = UnixTime::new_only_secs(0); + let dt = stamp.chrono_date_time().unwrap(); assert_eq!(dt.year(), 1970); assert_eq!(dt.month(), 1); assert_eq!(dt.day(), 1); @@ -590,55 +679,55 @@ mod tests { #[test] fn test_from_now() { - let stamp_now = UnixTimestamp::from_now().unwrap(); - let dt_now = stamp_now.as_date_time().unwrap(); + let stamp_now = UnixTime::from_now().unwrap(); + let dt_now = stamp_now.chrono_date_time().unwrap(); assert!(dt_now.year() >= 2020); } #[test] fn test_stamp_diff_positive_0() { - let stamp_later = UnixTimestamp::new(2, 0).unwrap(); + let stamp_later = UnixTime::new(2, 0); let StampDiff { positive_duration, duration_absolute, - } = stamp_later - UnixTimestamp::new(1, 0).unwrap(); + } = (stamp_later - UnixTime::new(1, 0)).expect("stamp diff error"); assert!(positive_duration); assert_eq!(duration_absolute, Duration::from_secs(1)); } #[test] fn test_stamp_diff_positive_1() { - let stamp_later = UnixTimestamp::new(3, 800).unwrap(); - let stamp_earlier = UnixTimestamp::new(1, 900).unwrap(); + let stamp_later = UnixTime::new(3, 800 * 1_000_000); + let stamp_earlier = UnixTime::new_subsec_millis_checked(1, 900).unwrap(); let StampDiff { positive_duration, duration_absolute, - } = stamp_later - stamp_earlier; + } = (stamp_later - stamp_earlier).expect("stamp diff error"); assert!(positive_duration); assert_eq!(duration_absolute, Duration::from_millis(1900)); } #[test] fn test_stamp_diff_negative() { - let stamp_later = UnixTimestamp::new(3, 800).unwrap(); - let stamp_earlier = UnixTimestamp::new(1, 900).unwrap(); + let stamp_later = UnixTime::new_subsec_millis_checked(3, 800).unwrap(); + let stamp_earlier = UnixTime::new_subsec_millis_checked(1, 900).unwrap(); let StampDiff { positive_duration, duration_absolute, - } = stamp_earlier - stamp_later; + } = (stamp_earlier - stamp_later).expect("stamp diff error"); assert!(!positive_duration); assert_eq!(duration_absolute, Duration::from_millis(1900)); } #[test] fn test_addition_spillover() { - let mut stamp0 = UnixTimestamp::new(1, 900).unwrap(); + let mut stamp0 = UnixTime::new_subsec_millis_checked(1, 900).unwrap(); stamp0 += Duration::from_millis(100); - assert_eq!(stamp0.unix_seconds, 2); - assert_eq!(stamp0.subsecond_millis(), 0); + assert_eq!(stamp0.secs, 2); + assert_eq!(stamp0.subsec_millis(), 0); stamp0 += Duration::from_millis(1100); - assert_eq!(stamp0.unix_seconds, 3); - assert_eq!(stamp0.subsecond_millis(), 100); + assert_eq!(stamp0.secs, 3); + assert_eq!(stamp0.subsec_millis(), 100); } #[test] @@ -647,4 +736,26 @@ mod tests { let stamp_error = TimestampError::from(cuc_error); assert_eq!(stamp_error.to_string(), format!("cuc error: {cuc_error}")); } + + #[test] + #[cfg(feature = "timelib")] + fn test_unix_stamp_as_timelib_datetime() { + let stamp_epoch = UnixTime::EPOCH; + let timelib_dt = stamp_epoch.timelib_date_time().unwrap(); + assert_eq!(timelib_dt.year(), 1970); + assert_eq!(timelib_dt.month(), time::Month::January); + assert_eq!(timelib_dt.day(), 1); + assert_eq!(timelib_dt.hour(), 0); + assert_eq!(timelib_dt.minute(), 0); + assert_eq!(timelib_dt.second(), 0); + } + + #[test] + #[cfg(feature = "timelib")] + fn test_unix_stamp_from_timelib_datetime() { + let timelib_dt = time::OffsetDateTime::UNIX_EPOCH; + let unix_time = UnixTime::from(timelib_dt); + let timelib_converted_back = unix_time.timelib_date_time().unwrap(); + assert_eq!(timelib_dt, timelib_converted_back); + } }