Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

timeofweek: introduce time of week construction and conversion #180

Merged
merged 22 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 211 additions & 3 deletions src/epoch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ use crate::duration::{Duration, Unit};
use crate::parser::Token;
use crate::{
Errors, TimeScale, BDT_REF_EPOCH, DAYS_PER_YEAR_NLD, ET_EPOCH_S, GPST_REF_EPOCH, GST_REF_EPOCH,
J1900_OFFSET, J2000_TO_J1900_DURATION, MJD_OFFSET, NANOSECONDS_PER_MICROSECOND,
NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_SECOND_U32, UNIX_REF_EPOCH,
J1900_OFFSET, J2000_TO_J1900_DURATION, MJD_OFFSET,
NANOSECONDS_PER_MICROSECOND, NANOSECONDS_PER_MILLISECOND, NANOSECONDS_PER_SECOND_U32,
SECONDS_PER_DAY, UNIX_REF_EPOCH,
};
use core::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd};
use core::fmt;
use core::hash::{Hash, Hasher};
use core::ops::{Add, AddAssign, Sub, SubAssign};

use crate::ParsingErrors;
use crate::Weekday;

#[cfg(feature = "python")]
use pyo3::prelude::*;
Expand All @@ -36,7 +38,7 @@ use core::str::FromStr;
use std::time::SystemTime;

#[cfg(not(feature = "std"))]
use num_traits::Float;
use num_traits::{Euclid, Float};

const TT_OFFSET_MS: i64 = 32_184;
const ET_OFFSET_US: i64 = 32_184_935;
Expand Down Expand Up @@ -1186,6 +1188,20 @@ impl Epoch {
)
}
}

/// Builds an Epoch from given `wk` week counter into the desired Time scale.
/// `ns` is the amount of nanoseconds into that week from closest Sunday Midnight.
/// This is usually how GNSS receivers describe a GNSS time scale epoch
#[must_use]
pub fn from_time_of_week(wk: u32, ns: u64, ts: TimeScale) -> Self {
let week = Duration::from_seconds(wk as f64 * SECONDS_PER_DAY * Weekday::DAYS_PER_WEEK);
Self::from_duration(week + (ns as f64) * Unit::Nanosecond, ts)
}

#[must_use]
pub fn from_time_of_week_utc(wk: u32, ns: u64) -> Self {
Self::from_time_of_week(wk, ns, TimeScale::UTC)
}
}

#[cfg_attr(feature = "python", pymethods)]
Expand Down Expand Up @@ -2226,6 +2242,198 @@ impl Epoch {
me
}

/// Converts this epoch into the time of week, represented as a rolling week counter into that time scale and the number of nanoseconds since closest Sunday midnight into that week.
/// This is usually how GNSS receivers describe a timestamp.
pub fn to_time_of_week(&self) -> (u32, u64) {
// fractional days in this time scale
let days = self.to_duration().to_unit(Unit::Day);
// wk: rolling week counter into timescale
let wk = (days / Weekday::DAYS_PER_WEEK).floor() as u32;
// tow: number of nanoseconds between self and previous sunday midnight / start of week
let mut start_of_week = self.previous_weekday_at_midnight(Weekday::Sunday);
let ref_epoch = self.time_scale.ref_epoch();
// restrict start of week/sunday to the start of the time scale
if start_of_week < ref_epoch {
start_of_week = ref_epoch;
}
let dw = *self - start_of_week; // difference in weekdays [0..6]
(wk, dw.nanoseconds)
}

/// Returns the weekday in provided time scale **ASSUMING** that the reference epoch of that time scale is a Monday.
/// You _probably_ do not want to use this. You probably either want `weekday()` or `weekday_utc()`.
/// Several time scales do _not_ have a reference day that's on a Monday, e.g. BDT.
pub fn weekday_in_time_scale(&self, time_scale: TimeScale) -> Weekday {
#[cfg(feature = "std")]
{
(self
.to_duration_in_time_scale(time_scale)
.to_unit(Unit::Day)
.rem_euclid(Weekday::DAYS_PER_WEEK)
.floor() as u8)
.into()
}
#[cfg(not(feature = "std"))]
{
// The rem_euclid call of num_traits requires a pointer not a value.
(self
.to_duration_in_time_scale(time_scale)
.to_unit(Unit::Day)
.rem_euclid(&Weekday::DAYS_PER_WEEK)
.floor() as u8)
.into()
}
}

/// Returns weekday (uses the TAI representation for this calculation).
pub fn weekday(&self) -> Weekday {
// J1900 was a Monday so we just have to modulo the number of days by the number of days per week.
// The function call will be optimized away.
self.weekday_in_time_scale(TimeScale::TAI)
}

/// Returns weekday in UTC timescale
pub fn weekday_utc(&self) -> Weekday {
self.weekday_in_time_scale(TimeScale::UTC)
}

/// Returns the next weekday.
///
/// ```
/// use hifitime::prelude::*;
///
/// let epoch = Epoch::from_gregorian_utc_at_midnight(1988, 1, 2);
/// assert_eq!(epoch.weekday_utc(), Weekday::Saturday);
/// assert_eq!(epoch.next(Weekday::Sunday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 3));
/// assert_eq!(epoch.next(Weekday::Monday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 4));
/// assert_eq!(epoch.next(Weekday::Tuesday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 5));
/// assert_eq!(epoch.next(Weekday::Wednesday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 6));
/// assert_eq!(epoch.next(Weekday::Thursday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 7));
/// assert_eq!(epoch.next(Weekday::Friday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 8));
/// assert_eq!(epoch.next(Weekday::Saturday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 9));
/// ```
pub fn next(&self, weekday: Weekday) -> Self {
let delta_days = self.weekday() - weekday;
if delta_days == Duration::ZERO {
*self + 7 * Unit::Day
} else {
*self + delta_days
}
}

pub fn next_weekday_at_midnight(&self, weekday: Weekday) -> Self {
self.next(weekday).with_hms_strict(0, 0, 0)
}

pub fn next_weekday_at_noon(&self, weekday: Weekday) -> Self {
self.next(weekday).with_hms_strict(12, 0, 0)
}

/// Returns the next weekday.
///
/// ```
/// use hifitime::prelude::*;
///
/// let epoch = Epoch::from_gregorian_utc_at_midnight(1988, 1, 2);
/// assert_eq!(epoch.previous(Weekday::Friday), Epoch::from_gregorian_utc_at_midnight(1988, 1, 1));
/// assert_eq!(epoch.previous(Weekday::Thursday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 31));
/// assert_eq!(epoch.previous(Weekday::Wednesday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 30));
/// assert_eq!(epoch.previous(Weekday::Tuesday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 29));
/// assert_eq!(epoch.previous(Weekday::Monday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 28));
/// assert_eq!(epoch.previous(Weekday::Sunday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 27));
/// assert_eq!(epoch.previous(Weekday::Saturday), Epoch::from_gregorian_utc_at_midnight(1987, 12, 26));
/// ```
pub fn previous(&self, weekday: Weekday) -> Self {
let delta_days = weekday - self.weekday();
gwbres marked this conversation as resolved.
Show resolved Hide resolved
if delta_days == Duration::ZERO {
*self - 7 * Unit::Day
} else {
*self - delta_days
}
}

pub fn previous_weekday_at_midnight(&self, weekday: Weekday) -> Self {
self.previous(weekday).with_hms_strict(0, 0, 0)
}

pub fn previous_weekday_at_noon(&self, weekday: Weekday) -> Self {
self.previous(weekday).with_hms_strict(12, 0, 0)
}

/// Returns the duration since the start of the year
pub fn duration_in_year(&self) -> Duration {
let year = Self::compute_gregorian(self.to_duration()).0;
let start_of_year = Self::from_gregorian(year, 1, 1, 0, 0, 0, 0, self.time_scale);
self.to_duration() - start_of_year.to_duration()
}

/// Returns the number of days since the start of the year.
pub fn day_of_year(&self) -> f64 {
self.duration_in_year().to_unit(Unit::Day)
}

/// Returns the hours of the Gregorian representation of this epoch in the time scale it was initialized in.
pub fn hours(&self) -> u64 {
gwbres marked this conversation as resolved.
Show resolved Hide resolved
self.to_duration().decompose().2
}

/// Returns the minutes of the Gregorian representation of this epoch in the time scale it was initialized in.
pub fn minutes(&self) -> u64 {
self.to_duration().decompose().3
}

/// Returns the seconds of the Gregorian representation of this epoch in the time scale it was initialized in.
pub fn seconds(&self) -> u64 {
self.to_duration().decompose().4
}

/// Returns the milliseconds of the Gregorian representation of this epoch in the time scale it was initialized in.
pub fn milliseconds(&self) -> u64 {
self.to_duration().decompose().5
}

/// Returns the microseconds of the Gregorian representation of this epoch in the time scale it was initialized in.
pub fn microseconds(&self) -> u64 {
self.to_duration().decompose().6
}

/// Returns the nanoseconds of the Gregorian representation of this epoch in the time scale it was initialized in.
pub fn nanoseconds(&self) -> u64 {
self.to_duration().decompose().7
}

/// Returns a copy of self where the time is set to the provided hours, minutes, seconds
/// Invalid number of hours, minutes, and seconds will overflow into their higher unit.
/// Warning: this does _not_ set the subdivisions of second to zero.
pub fn with_hms(&self, hours: u64, minutes: u64, seconds: u64) -> Self {
let (sign, days, _, _, _, milliseconds, microseconds, nanoseconds) =
self.to_duration().decompose();
Self::from_duration(
Duration::compose(
sign,
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
),
self.time_scale,
)
}

/// Returns a copy of self where the time is set to the provided hours, minutes, seconds
/// Invalid number of hours, minutes, and seconds will overflow into their higher unit.
/// Warning: this will set the subdivisions of seconds to zero.
pub fn with_hms_strict(&self, hours: u64, minutes: u64, seconds: u64) -> Self {
let (sign, days, _, _, _, _, _, _) = self.to_duration().decompose();
Self::from_duration(
Duration::compose(sign, days, hours, minutes, seconds, 0, 0, 0),
self.time_scale,
)
}

// Python helpers

#[cfg(feature = "python")]
Expand Down
7 changes: 6 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,17 @@ pub use timeunits::*;
mod timeseries;
pub use timeseries::*;

mod weekday;
pub use weekday::*;

/// This module defines all of the deprecated methods.
mod deprecated;

#[allow(deprecated)]
pub mod prelude {
pub use crate::{
deprecated::TimeSystem, Duration, Epoch, Errors, Freq, Frequencies, TimeScale, TimeSeries,
TimeUnits, Unit,
TimeUnits, Unit, Weekday,
};
}

Expand Down Expand Up @@ -132,6 +135,8 @@ pub enum ParsingErrors {
UnknownFormat,
UnknownOrMissingUnit,
UnsupportedTimeSystem,
/// Non recognized Weekday description
ParseWeekdayError,
}

impl fmt::Display for Errors {
Expand Down
17 changes: 16 additions & 1 deletion src/timescale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ use serde_derive::{Deserialize, Serialize};
use core::fmt;
use core::str::FromStr;

use crate::{Duration, Epoch, Errors, ParsingErrors, SECONDS_PER_DAY};
use crate::{Duration, Epoch, Errors, ParsingErrors, J2000_TO_J1900_DURATION, SECONDS_PER_DAY};

/// The J1900 reference epoch (1900-01-01 at noon) TAI.
pub const J1900_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration::ZERO);

/// The J2000 reference epoch (2000-01-01 at midnight) TAI.
pub const J2000_REF_EPOCH: Epoch = Epoch::from_tai_duration(J2000_TO_J1900_DURATION);

/// GPS reference epoch is UTC midnight between 05 January and 06 January 1980; cf. <https://gssc.esa.int/navipedia/index.php/Time_References_in_GNSS#GPS_Time_.28GPST.29>.
pub const GPST_REF_EPOCH: Epoch = Epoch::from_tai_duration(Duration {
Expand Down Expand Up @@ -99,6 +105,15 @@ impl TimeScale {
pub const fn is_gnss(&self) -> bool {
matches!(self, Self::GPST | Self::GST | Self::BDT)
}
/// Returns Reference Epoch (t(0)) for given timescale
pub(crate) const fn ref_epoch(&self) -> Epoch {
match self {
Self::GPST => GPST_REF_EPOCH,
Self::GST => GST_REF_EPOCH,
Self::BDT => BDT_REF_EPOCH,
_ => J1900_REF_EPOCH,
gwbres marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

impl fmt::Display for TimeScale {
Expand Down
Loading