From f89793f33ead17b7c485817428596d9becfe72e9 Mon Sep 17 00:00:00 2001 From: Angus Morrison Date: Wed, 12 Jun 2024 12:01:10 +0100 Subject: [PATCH] Update documentation for top-level lox-time modules (#110) --- crates/lox-time/src/calendar_dates.rs | 34 ++++- crates/lox-time/src/constants.rs | 2 + crates/lox-time/src/constants/f64.rs | 2 + crates/lox-time/src/constants/i64.rs | 2 + crates/lox-time/src/constants/julian_dates.rs | 5 + crates/lox-time/src/deltas.rs | 67 ++++++++-- crates/lox-time/src/julian_dates.rs | 29 +++++ crates/lox-time/src/lib.rs | 116 ++++++++++++++---- crates/lox-time/src/prelude.rs | 8 ++ crates/lox-time/src/python.rs | 10 ++ crates/lox-time/src/subsecond.rs | 8 +- crates/lox-time/src/test_helpers.rs | 13 ++ crates/lox-time/src/time_of_day.rs | 54 ++++++++ crates/lox-time/src/time_scales.rs | 14 ++- crates/lox-time/src/transformations.rs | 34 ++++- crates/lox-time/src/ut1.rs | 35 +++++- crates/lox-time/src/utc.rs | 62 ++++++++++ crates/lox-time/src/utc/leap_seconds.rs | 43 +++++++ crates/lox-time/src/utc/transformations.rs | 2 +- 19 files changed, 490 insertions(+), 50 deletions(-) diff --git a/crates/lox-time/src/calendar_dates.rs b/crates/lox-time/src/calendar_dates.rs index 2b9dcdca..30c86986 100644 --- a/crates/lox-time/src/calendar_dates.rs +++ b/crates/lox-time/src/calendar_dates.rs @@ -6,6 +6,11 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ +/*! + `calendar_dates` exposes a concrete [Date] struct and the [CalendarDate] trait for working with + human-readable dates. +*/ + use std::{ cmp::Ordering, fmt::{Display, Formatter}, @@ -30,6 +35,7 @@ fn iso_regex() -> &'static Regex { ISO.get_or_init(|| Regex::new(r"(?-?\d{4,})-(?\d{2})-(?\d{2})").unwrap()) } +/// Error type returned when attempting to construct a [Date] from invalid inputs. #[derive(Debug, Clone, Error, PartialEq, Eq, PartialOrd, Ord)] pub enum DateError { #[error("invalid date `{0}-{1}-{2}`")] @@ -40,6 +46,7 @@ pub enum DateError { NonLeapYear, } +/// The calendars supported by Lox. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Calendar { ProlepticJulian, @@ -47,6 +54,7 @@ pub enum Calendar { Gregorian, } +/// A calendar date. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Date { calendar: Calendar, @@ -70,6 +78,7 @@ impl FromStr for Date { } impl Default for Date { + /// [Date] defaults to 2000-01-01 of the Gregorian calendar. fn default() -> Self { Self { calendar: Calendar::Gregorian, @@ -124,6 +133,12 @@ impl Date { self.day } + /// Construct a new [Date] from a year, month and day. The [Calendar] is inferred from the input + /// fields. + /// + /// # Errors + /// + /// - [DateError::InvalidDate] if the input fields do not represent a valid date. pub fn new(year: i64, month: u8, day: u8) -> Result { if !(1..=12).contains(&month) { Err(DateError::InvalidDate(year, month, day)) @@ -144,6 +159,12 @@ impl Date { } } + /// Constructs a new [Date] from an ISO 8601 string. + /// + /// # Errors + /// + /// - [DateError::InvalidIsoString] if the input string does not contain a valid ISO 8601 date. + /// - [DateError::InvalidDate] if the date parsed from the ISO 8601 string is invalid. pub fn from_iso(iso: &str) -> Result { let caps = iso_regex() .captures(iso) @@ -160,6 +181,8 @@ impl Date { Date::new(year, month, day) } + /// Constructs a new [Date] from a signed number of days since J2000. The [Calendar] is + /// inferred. pub fn from_days_since_j2000(days: i64) -> Self { let calendar = if days < LAST_JULIAN_DAY_J2K { if days > LAST_PROLEPTIC_JULIAN_DAY_J2K { @@ -187,6 +210,8 @@ impl Date { } } + /// Constructs a new [Date] from a signed number of seconds since J2000. The [Calendar] is + /// inferred. pub fn from_seconds_since_j2000(seconds: i64) -> Self { let seconds = seconds + SECONDS_PER_HALF_DAY; let mut time = seconds % SECONDS_PER_DAY; @@ -197,6 +222,12 @@ impl Date { Self::from_days_since_j2000(days) } + /// Constructs a new [Date] from a year and a day number within that year. The [Calendar] is + /// inferred. + /// + /// # Errors + /// + /// - [DateError::NonLeapYear] if the input day number is 366 and the year is not a leap year. pub fn from_day_of_year(year: i64, day_of_year: u16) -> Result { let calendar = calendar(year, 1, 1); let leap = is_leap_year(calendar, year); @@ -211,6 +242,7 @@ impl Date { }) } + /// Returns the day number of `self` relative to J2000. pub fn j2000_day_number(&self) -> i64 { j2000_day_number(self.calendar, self.year, self.month, self.day) } @@ -329,7 +361,7 @@ fn j2000_day_number(calendar: Calendar, year: i64, month: u8, day: u8) -> i64 { d1 + d2 as i64 } -/// CalendarDate allows continuous time formats to report their date in their respective calendar. +/// `CalendarDate` allows any date-time format to report its date in a human-readable way. pub trait CalendarDate { fn date(&self) -> Date; diff --git a/crates/lox-time/src/constants.rs b/crates/lox-time/src/constants.rs index f3e88f2d..d8ad97f9 100644 --- a/crates/lox-time/src/constants.rs +++ b/crates/lox-time/src/constants.rs @@ -6,6 +6,8 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ +//! Module `constants` exposes time-related constants. + pub mod f64; pub mod i64; pub mod julian_dates; diff --git a/crates/lox-time/src/constants/f64.rs b/crates/lox-time/src/constants/f64.rs index e7f31966..16c488c6 100644 --- a/crates/lox-time/src/constants/f64.rs +++ b/crates/lox-time/src/constants/f64.rs @@ -6,4 +6,6 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ +//! Module `f64` exposes time-related `f64` constants. + pub const SECONDS_PER_FEMTOSECOND: f64 = 1e-15; diff --git a/crates/lox-time/src/constants/i64.rs b/crates/lox-time/src/constants/i64.rs index e315ef7b..1eac0454 100644 --- a/crates/lox-time/src/constants/i64.rs +++ b/crates/lox-time/src/constants/i64.rs @@ -6,6 +6,8 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ +//! Module `i64` exposes time-related `i64` constants. + pub const SECONDS_PER_MINUTE: i64 = 60; pub const SECONDS_PER_HOUR: i64 = 60 * SECONDS_PER_MINUTE; diff --git a/crates/lox-time/src/constants/julian_dates.rs b/crates/lox-time/src/constants/julian_dates.rs index 039e24cd..da4d7e57 100644 --- a/crates/lox-time/src/constants/julian_dates.rs +++ b/crates/lox-time/src/constants/julian_dates.rs @@ -6,6 +6,11 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ +/*! + Module `julian_dates` exposes constants related to standard Julian epochs and dates in a variety + of formats. +*/ + use crate::deltas::TimeDelta; use crate::subsecond::Subsecond; diff --git a/crates/lox-time/src/deltas.rs b/crates/lox-time/src/deltas.rs index 5dd11edd..6baacf59 100644 --- a/crates/lox-time/src/deltas.rs +++ b/crates/lox-time/src/deltas.rs @@ -6,6 +6,16 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ +/*! + Module `deltas` contains [TimeDelta], the key primitive of the `lox-time` crate. + + [TimeDelta] is a signed, unscaled delta relative to an arbitrary epoch. This forms the basis + of instants in all continuous time scales. + + The [ToDelta] trait specifies the method by which such scaled time representations should + expose their underlying [TimeDelta]. +*/ + use std::ops::{Add, Neg, Sub}; use num::ToPrimitive; @@ -25,11 +35,13 @@ use crate::{ subsecond::Subsecond, }; +/// A unifying trait for types that can be converted into a [TimeDelta]. pub trait ToDelta { /// Transforms the value into a [TimeDelta]. fn to_delta(&self) -> TimeDelta; } +/// Error type returned when attempting to construct a [TimeDelta] from an invalid `f64`. #[derive(Clone, Debug, Default, Error)] #[error("`{raw}` cannot be represented as a `TimeDelta`: {detail}")] pub struct TimeDeltaError { @@ -49,7 +61,7 @@ impl PartialEq for TimeDeltaError { /// A signed, continuous time difference supporting femtosecond precision. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] pub struct TimeDelta { - // Like `BaseTime`, the sign of the delta is determined by the sign of the `seconds` field. + // The sign of the delta is determined by the sign of the `seconds` field. pub seconds: i64, // The positive subsecond since the last whole second. For example, a delta of -0.25 s would be @@ -64,10 +76,12 @@ pub struct TimeDelta { } impl TimeDelta { + /// Construct a new [TimeDelta] from a number of seconds and a [Subsecond]. pub fn new(seconds: i64, subsecond: Subsecond) -> Self { Self { seconds, subsecond } } + /// Construct a [TimeDelta] from an integral number of seconds. pub fn from_seconds(seconds: i64) -> Self { Self { seconds, @@ -75,11 +89,15 @@ impl TimeDelta { } } - /// Create a [TimeDelta] from a floating-point number of seconds. + /// Construct a [TimeDelta] from a floating-point number of seconds. /// /// As the magnitude of the input's significand grows, the precision of the resulting - /// `TimeDelta` falls. Applications requiring precision guarantees should use `TimeDelta::new` + /// `TimeDelta` falls. Applications requiring precision guarantees should use [TimeDelta::new] /// instead. + /// + /// # Errors + /// + /// - [TimeDeltaError] if the input is NaN or ±infinity. pub fn from_decimal_seconds(value: f64) -> Result { if value.is_nan() { return Err(TimeDeltaError { @@ -122,51 +140,72 @@ impl TimeDelta { Ok(result) } - /// Create a [TimeDelta] from a floating-point number of minutes. + /// Construct a [TimeDelta] from a floating-point number of minutes. /// /// As the magnitude of the input's significand grows, the resolution of the resulting - /// `TimeDelta` falls. Applications requiring precision guarantees should use `TimeDelta::new` + /// `TimeDelta` falls. Applications requiring precision guarantees should use [TimeDelta::new] /// instead. + /// + /// # Errors + /// + /// - [TimeDeltaError] if the input is NaN or ±infinity. pub fn from_minutes(value: f64) -> Result { Self::from_decimal_seconds(value * SECONDS_PER_MINUTE) } - /// Create a [TimeDelta] from a floating-point number of hours. + /// Construct a [TimeDelta] from a floating-point number of hours. /// /// As the magnitude of the input's significand grows, the resolution of the resulting - /// `TimeDelta` falls. Applications requiring precision guarantees should use `TimeDelta::new` + /// `TimeDelta` falls. Applications requiring precision guarantees should use [TimeDelta::new] /// instead. + /// + /// # Errors + /// + /// - [TimeDeltaError] if the input is NaN or ±infinity. pub fn from_hours(value: f64) -> Result { Self::from_decimal_seconds(value * SECONDS_PER_HOUR) } - /// Create a [TimeDelta] from a floating-point number of days. + /// Construct a [TimeDelta] from a floating-point number of days. /// /// As the magnitude of the input's significand grows, the resolution of the resulting - /// `TimeDelta` falls. Applications requiring precision guarantees should use `TimeDelta::new` + /// `TimeDelta` falls. Applications requiring precision guarantees should use [TimeDelta::new] /// instead. + /// + /// # Errors + /// + /// - [TimeDeltaError] if the input is NaN or ±infinity. pub fn from_days(value: f64) -> Result { Self::from_decimal_seconds(value * SECONDS_PER_DAY) } - /// Create a [TimeDelta] from a floating-point number of Julian years. + /// Construct a [TimeDelta] from a floating-point number of Julian years. /// /// As the magnitude of the input's significand grows, the resolution of the resulting - /// `TimeDelta` falls. Applications requiring precision guarantees should use `TimeDelta::new` + /// `TimeDelta` falls. Applications requiring precision guarantees should use [TimeDelta::new] /// instead. + /// + /// # Errors + /// + /// - [TimeDeltaError] if the input is NaN or ±infinity. pub fn from_julian_years(value: f64) -> Result { Self::from_decimal_seconds(value * SECONDS_PER_JULIAN_YEAR) } - /// Create a [TimeDelta] from a floating-point number of Julian centuries. + /// Construct a [TimeDelta] from a floating-point number of Julian centuries. /// /// As the magnitude of the input's significand grows, the resolution of the resulting - /// `TimeDelta` falls. Applications requiring precision guarantees should use `TimeDelta::new` + /// `TimeDelta` falls. Applications requiring precision guarantees should use [TimeDelta::new] /// instead. + /// + /// # Errors + /// + /// - [TimeDeltaError] if the input is NaN or ±infinity. pub fn from_julian_centuries(value: f64) -> Result { Self::from_decimal_seconds(value * SECONDS_PER_JULIAN_CENTURY) } + /// Express `&self` as a floating-point number of seconds, with potential loss of precision. pub fn to_decimal_seconds(&self) -> f64 { self.subsecond.0 + self.seconds.to_f64().unwrap() } @@ -183,6 +222,7 @@ impl TimeDelta { self.seconds > 0 || self.seconds == 0 && self.subsecond.0 > 0.0 } + /// Scale the [TimeDelta] by `factor`, with possible loss of precision. pub fn scale(mut self, mut factor: f64) -> Self { // Treating both `Self` and `factor` as positive and then correcting the sign at the end // substantially simplifies the implementation. @@ -225,6 +265,7 @@ impl TimeDelta { } } + /// Express the [TimeDelta] as an integral number of seconds since the given [Epoch]. pub fn seconds_from_epoch(&self, epoch: Epoch) -> i64 { match epoch { Epoch::JulianDate => self.seconds + SECONDS_BETWEEN_JD_AND_J2000, diff --git a/crates/lox-time/src/julian_dates.rs b/crates/lox-time/src/julian_dates.rs index 4bfeebff..793e7073 100644 --- a/crates/lox-time/src/julian_dates.rs +++ b/crates/lox-time/src/julian_dates.rs @@ -6,6 +6,12 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ +/*! + Module `julian_dates` exposes the [JulianDate] trait for expressing arbitrary time + representations as Julian dates relative to standard [Epoch]s and in a variety of [Unit]s. +*/ + +/// The Julian epochs supported by Lox. pub enum Epoch { JulianDate, ModifiedJulianDate, @@ -13,64 +19,87 @@ pub enum Epoch { J2000, } +/// The units of time in which a Julian date may be expressed. pub enum Unit { Seconds, Days, Centuries, } +/// Enables a time or date type to be expressed as a Julian date. pub trait JulianDate { + /// Expresses `self` as a Julian date in the specified [Unit], relative to the given [Epoch]. + /// + /// This is the only required method for implementing the [JulianDate] trait. fn julian_date(&self, epoch: Epoch, unit: Unit) -> f64; + /// Expresses `self` as a two-part Julian date in the specified [Unit], relative to the given + /// [Epoch]. + /// + /// The default implementation calls [JulianDate::julian_date] and returns the integer and + /// fractional parts of the single `f64` result. Applications that cannot afford the associated + /// loss of precision should provide their own implementations. fn two_part_julian_date(&self) -> (f64, f64) { let jd = self.julian_date(Epoch::JulianDate, Unit::Days); (jd.trunc(), jd.fract()) } + /// Returns the number of seconds since the Julian epoch as an `f64`. fn seconds_since_julian_epoch(&self) -> f64 { self.julian_date(Epoch::JulianDate, Unit::Seconds) } + /// Returns the number of seconds since the Modified Julian epoch as an `f64`. fn seconds_since_modified_julian_epoch(&self) -> f64 { self.julian_date(Epoch::ModifiedJulianDate, Unit::Seconds) } + /// Returns the number of seconds since J1950 as an `f64`. fn seconds_since_j1950(&self) -> f64 { self.julian_date(Epoch::J1950, Unit::Seconds) } + /// Returns the number of seconds since J2000 as an `f64`. fn seconds_since_j2000(&self) -> f64 { self.julian_date(Epoch::J2000, Unit::Seconds) } + /// Returns the number of days since the Julian epoch as an `f64`. fn days_since_julian_epoch(&self) -> f64 { self.julian_date(Epoch::JulianDate, Unit::Days) } + /// Returns the number of days since the Modified Julian epoch as an `f64`. fn days_since_modified_julian_epoch(&self) -> f64 { self.julian_date(Epoch::ModifiedJulianDate, Unit::Days) } + /// Returns the number of days since J1950 as an `f64`. fn days_since_j1950(&self) -> f64 { self.julian_date(Epoch::J1950, Unit::Days) } + /// Returns the number of days since J2000 as an `f64`. fn days_since_j2000(&self) -> f64 { self.julian_date(Epoch::J2000, Unit::Days) } + /// Returns the number of centuries since the Julian epoch as an `f64`. fn centuries_since_julian_epoch(&self) -> f64 { self.julian_date(Epoch::JulianDate, Unit::Centuries) } + /// Returns the number of centuries since the Modified Julian epoch as an `f64`. fn centuries_since_modified_julian_epoch(&self) -> f64 { self.julian_date(Epoch::ModifiedJulianDate, Unit::Centuries) } + /// Returns the number of centuries since J1950 as an `f64`. fn centuries_since_j1950(&self) -> f64 { self.julian_date(Epoch::J1950, Unit::Centuries) } + /// Returns the number of centuries since J2000 as an `f64`. fn centuries_since_j2000(&self) -> f64 { self.julian_date(Epoch::J2000, Unit::Centuries) } diff --git a/crates/lox-time/src/lib.rs b/crates/lox-time/src/lib.rs index bfe6dfc3..3a8e0e7e 100644 --- a/crates/lox-time/src/lib.rs +++ b/crates/lox-time/src/lib.rs @@ -6,13 +6,31 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ -//! lox-time provides structs and functions for working with instants in astronomical time scales. -//! -//! The main struct is [Time], which represents an instant in time generic over a [TimeScale] -//! without leap seconds. -//! -//! [Utc] and [Date] are used strictly as an I/O formats, avoiding much of the complexity inherent -//! in working with leap seconds. +/*! + `lox-time` defines an API for working with instants in astronomical time scales. + + # Overview + + `lox_time` exposes: + - the marker trait [TimeScale] and zero-sized implementations representing the most common, + continuous astronomical time scales; + - the concrete type [Time] representing an instant in a [TimeScale]; + - [Utc], the only discontinuous time representation supported by Lox; + - the [TryToScale] and [ToScale] traits, supporting transformations between pairs of time + scales; + - standard implementations of the most common time scale transformations. + + # Continuous vs discontinuous timescales + + Internally, Lox uses only continuous time scales (i.e. time scales without leap seconds). An + instance of [Time] represents an instant in time generic over a continuous [TimeScale]. + + [Utc] is used strictly as an I/O time format, which must be transformed into a continuous time + scale before use in the wider Lox ecosystem. + + This separation minimises the complexity in working with leap seconds, confining these + transformations to the crate boundaries. +*/ use std::cmp::Ordering; use std::fmt; @@ -98,7 +116,7 @@ pub enum TimeError { InvalidIsoString(String), } -/// An instant in time in a given [TimeScale]. +/// An instant in time in a given [TimeScale], relative to J2000. /// /// `Time` supports femtosecond precision, but be aware that many algorithms operating on `Time`s /// are not accurate to this level of precision. @@ -110,8 +128,8 @@ pub struct Time { } impl Time { - /// Instantiates a [Time] in the given scale from seconds since J2000 subdivided into integral - /// seconds and [Subsecond]. + /// Instantiates a [Time] in the given [TimeScale] from the count of seconds since J2000, subdivided + /// into integral seconds and [Subsecond]. pub fn new(scale: T, seconds: i64, subsecond: Subsecond) -> Self { Self { scale, @@ -120,7 +138,12 @@ impl Time { } } - /// Instantiates a [Time] in the given `scale` from a [Date] and a [TimeOfDay]. + /// Instantiates a [Time] in the given [TimeScale] from a [Date] and a [TimeOfDay]. + /// + /// # Errors + /// + /// * Returns `TimeError::LeapSecondsOutsideUtc` if `time` is a leap second, since leap seconds + /// cannot be unambiguously represented by a continuous time format. pub fn from_date_and_time(scale: T, date: Date, time: TimeOfDay) -> Result { let mut seconds = (date.days_since_j2000() * time::SECONDS_PER_DAY) .to_i64() @@ -138,6 +161,11 @@ impl Time { Ok(Self::new(scale, seconds, time.subsecond())) } + /// Instantiates a [Time] in the given [TimeScale] from an ISO 8601 string. + /// + /// # Errors + /// + /// * Returns `TimeError::InvalidIsoString` if `iso` is not a valid ISO 8601 timestamp. pub fn from_iso(scale: T, iso: &str) -> Result { let Some((date, time_and_scale)) = iso.split_once('T') else { return Err(TimeError::InvalidIsoString(iso.to_owned())); @@ -158,7 +186,7 @@ impl Time { Self::from_date_and_time(scale, date, time) } - /// Instantiates a [Time] in the given `scale` from an offset from the J2000 epoch given as a [TimeDelta]. + /// Instantiates a [Time] in the given [TimeScale] and a [TimeDelta] relative to J2000. pub fn from_delta(scale: T, delta: TimeDelta) -> Self { Self { scale, @@ -167,7 +195,10 @@ impl Time { } } - /// Returns the epoch for the given [Epoch] in the given timescale. + /// Returns the [Time] at `epoch` in the given [TimeScale]. + /// + /// Since [Time] is defined relative to J2000, this is equivalent to the delta between + /// J2000 and `epoch`. pub fn from_epoch(scale: T, epoch: Epoch) -> Self { match epoch { Epoch::JulianDate => Self { @@ -193,7 +224,12 @@ impl Time { } } - /// Instantiates a [Time] in the given scale from a `julian_date` with the given `epoch`. + /// Given a Julian date, instantiates a [Time] in the specified [TimeScale], relative to + /// `epoch`. + /// + /// # Errors + /// + /// * Returns `TimeError::JulianDateOutOfRange` if `julian_date` is NaN or ±infinity. pub fn from_julian_date(scale: T, julian_date: Days, epoch: Epoch) -> Result { let seconds = julian_date * time::SECONDS_PER_DAY; if !(i64::MIN as f64..=i64::MAX as f64).contains(&seconds) { @@ -217,7 +253,7 @@ impl Time { Ok(Self::new(scale, seconds, subsecond)) } - /// Returns a [TimeBuilder] for constructing a new [Time] in the given `scale`. + /// Returns a [TimeBuilder] for constructing a new [Time] in the given [TimeScale]. pub fn builder_with_scale(scale: T) -> TimeBuilder { TimeBuilder::new(scale) } @@ -230,43 +266,47 @@ impl Time { self.scale.clone() } - /// Returns a new [Time] with `scale` without changing the underlying timestamp. + /// Returns a new [Time] with the delta of `self` but time scale `scale`. + /// + /// Note that the underlying delta is simply copied – no time scale transformation takes place. pub fn with_scale(&self, scale: S) -> Time { Time::new(scale, self.seconds, self.subsecond) } - /// Returns a new [Time] with `scale` with its offset adjusted by `delta`. + /// Returns a new [Time] with the delta of `self` adjusted by `delta`, and time scale `scale`. + /// + /// Note that no time scale transformation takes place beyond the adjustment specified by + /// `delta`. pub fn with_scale_and_delta(&self, scale: S, delta: TimeDelta) -> Time { Time::from_delta(scale, self.to_delta() + delta) } - /// Returns, as an epoch in the given timescale, midday on the first day of the proleptic Julian - /// calendar. + /// Returns the Julian epoch as a [Time] in the given [TimeScale]. pub fn jd0(scale: T) -> Self { Self::from_epoch(scale, Epoch::JulianDate) } - /// Returns the epoch of the Modified Julian Date in the given timescale. + /// Returns the modified Julian epoch as a [Time] in the given [TimeScale]. pub fn mjd0(scale: T) -> Self { Self::from_epoch(scale, Epoch::ModifiedJulianDate) } - /// Returns the J1950 epoch in the given timescale. + /// Returns the J1950 epoch as a [Time] in the given [TimeScale]. pub fn j1950(scale: T) -> Self { Self::from_epoch(scale, Epoch::J1950) } - /// Returns the J2000 epoch in the given timescale. + /// Returns the J2000 epoch as a [Time] in the given [TimeScale]. pub fn j2000(scale: T) -> Self { Self::from_epoch(scale, Epoch::J2000) } - /// The number of whole seconds since J2000. + /// Returns the number of whole seconds since J2000. pub fn seconds(&self) -> i64 { self.seconds } - /// The number of femtoseconds from the last whole second. + /// Returns the fraction of a second from the last whole second as an `f64`. pub fn subsecond(&self) -> f64 { self.subsecond.into() } @@ -377,8 +417,8 @@ impl FromStr for Time { impl Add for Time { type Output = Self; - /// The implementation of [Add] for [Time] follows the default Rust rules for integer overflow, which - /// should be sufficient for all practical purposes. + /// The implementation of [Add] for [Time] follows the default Rust rules for integer overflow, + /// which should be sufficient for all practical purposes. fn add(self, rhs: TimeDelta) -> Self::Output { if rhs.is_negative() { return self - (-rhs); @@ -438,6 +478,8 @@ impl CalendarDate for Time { Date::from_seconds_since_j2000(self.seconds) } } + +/// `TimeBuilder` supports the construction of [Time] instances piecewise using the builder pattern. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct TimeBuilder { scale: T, @@ -446,6 +488,7 @@ pub struct TimeBuilder { } impl TimeBuilder { + /// Returns a new [TimeBuilder], equivalent to a [Time] at J2000 in the given [TimeScale]. pub fn new(scale: T) -> Self { Self { scale, @@ -471,6 +514,12 @@ impl TimeBuilder { } /// Builds the [Time] instance. + /// + /// # Errors + /// + /// * [DateError] if `ymd` data passed into the builder did not correspond to a valid date; + /// * [TimeOfDayError] if `hms` data passed into the builder did not correspond to a valid time + /// of day. pub fn build(self) -> Result, TimeError> { let date = self.date?; let time = self.time?; @@ -478,6 +527,21 @@ impl TimeBuilder { } } +/// Convenience macro to simplify the construction of [Time] instances. +/// +/// # Examples +/// +/// ``` +/// use lox_time::Time; +/// use lox_time::time; +/// use lox_time::time_scales::Tai; +/// +/// +/// time!(Tai, 2020, 1, 2); // 2020-01-02T00:00:00.000 TAI +/// time!(Tai, 2020, 1, 2, 3) ; // 2020-01-02T03:00:00.000 TAI +/// time!(Tai, 2020, 1, 2, 3, 4); // 2020-01-02T03:04:00.000 TAI +/// time!(Tai, 2020, 1, 2, 3, 4, 5.006); // 2020-01-02T03:04:05.006 TAI +/// ``` #[macro_export] macro_rules! time { ($scale:expr, $year:literal, $month:literal, $day:literal) => { diff --git a/crates/lox-time/src/prelude.rs b/crates/lox-time/src/prelude.rs index 02659eeb..6ac7e789 100644 --- a/crates/lox-time/src/prelude.rs +++ b/crates/lox-time/src/prelude.rs @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2023. Helge Eichhorn and the LOX contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + */ + pub use crate::calendar_dates::{CalendarDate, Date}; pub use crate::julian_dates::JulianDate; pub use crate::time_of_day::{CivilTime, TimeOfDay}; diff --git a/crates/lox-time/src/python.rs b/crates/lox-time/src/python.rs index df9f8c41..da494ef9 100644 --- a/crates/lox-time/src/python.rs +++ b/crates/lox-time/src/python.rs @@ -1,3 +1,13 @@ +/* + * Copyright (c) 2023. Helge Eichhorn and the LOX contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + */ + +//! Module `python` aggregates the Python binding for `lox-time`. + pub mod deltas; pub mod time; pub mod time_scales; diff --git a/crates/lox-time/src/subsecond.rs b/crates/lox-time/src/subsecond.rs index cd69dfa5..00c4e168 100644 --- a/crates/lox-time/src/subsecond.rs +++ b/crates/lox-time/src/subsecond.rs @@ -6,6 +6,8 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ +//! Module `subsecond` exposes the [Subsecond] newtype for working with fractions of seconds. + use std::cmp::Ordering; use std::fmt; use std::fmt::{Display, Formatter}; @@ -14,6 +16,7 @@ use num::ToPrimitive; use thiserror::Error; +/// Error type returned when attempting to construct a [Subsecond] from an invalid `f64`. #[derive(Debug, Copy, Clone, Error)] #[error("subsecond must be in the range [0.0, 1.0), but was `{0}`")] pub struct InvalidSubsecond(f64); @@ -38,7 +41,7 @@ impl PartialEq for InvalidSubsecond { impl Eq for InvalidSubsecond {} -/// An f64 value in the range [0.0, 1.0) representing a fraction of a second with femtosecond +/// An `f64` value in the range `[0.0, 1.0)` representing a fraction of a second with femtosecond /// precision. #[derive(Debug, Default, Copy, Clone)] pub struct Subsecond(pub(crate) f64); @@ -189,7 +192,8 @@ mod tests { #[test] fn test_subsecond_into_f64() { let subsecond = Subsecond(0.0); - assert_eq!(0.0, subsecond.into()); + let as_f64: f64 = subsecond.into(); + assert_eq!(0.0, as_f64); } #[test] diff --git a/crates/lox-time/src/test_helpers.rs b/crates/lox-time/src/test_helpers.rs index 842e6ef5..d641b1bb 100644 --- a/crates/lox-time/src/test_helpers.rs +++ b/crates/lox-time/src/test_helpers.rs @@ -1,11 +1,24 @@ +/* + * Copyright (c) 2024. Helge Eichhorn and the LOX contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#![cfg(test)] + use std::{path::PathBuf, sync::OnceLock}; use crate::{ut1::DeltaUt1Tai, utc::leap_seconds::BuiltinLeapSeconds}; +/// Returns a [PathBuf] to the test fixture directory. pub fn data_dir() -> PathBuf { PathBuf::from(format!("{}/../../data", env!("CARGO_MANIFEST_DIR"))) } +/// Returns a [DeltaUt1Tai] loaded from the default IERS finals CSV located in the text fixture +/// directory. pub fn delta_ut1_tai() -> &'static DeltaUt1Tai { static PROVIDER: OnceLock = OnceLock::new(); PROVIDER.get_or_init(|| { diff --git a/crates/lox-time/src/time_of_day.rs b/crates/lox-time/src/time_of_day.rs index 86c3cb40..a860ce82 100644 --- a/crates/lox-time/src/time_of_day.rs +++ b/crates/lox-time/src/time_of_day.rs @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2024. Helge Eichhorn and the LOX contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + */ + +/*! + Module `time_of_day` exposes the concrete representation of a time of day with leap second + support, [TimeOfDay]. + + The [CivilTime] trait supports arbitrary time representations to express themselves as a + human-readable time of day. +*/ + use std::fmt::Display; use std::str::FromStr; use std::{cmp::Ordering, sync::OnceLock}; @@ -18,6 +34,8 @@ fn iso_regex() -> &'static Regex { }) } +/// Error type returned when attempting to construct a [TimeOfDay] with a greater number of +/// floating-point seconds than are in a day. #[derive(Debug, Copy, Clone, Error)] #[error("seconds must be in the range [0.0..86401.0) but was {0}")] pub struct InvalidSeconds(f64); @@ -42,6 +60,7 @@ impl PartialEq for InvalidSeconds { impl Eq for InvalidSeconds {} +/// Error type returned when attempting to construct a [TimeOfDay] from invalid components. #[derive(Debug, Clone, Error, PartialEq, Eq, PartialOrd, Ord)] pub enum TimeOfDayError { #[error("hour must be in the range [0..24) but was {0}")] @@ -104,6 +123,7 @@ pub trait CivilTime { } } +/// A human-readable time representation with support for representing leap seconds. #[derive(Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct TimeOfDay { hour: u8, @@ -113,6 +133,13 @@ pub struct TimeOfDay { } impl TimeOfDay { + /// Constructs a new `TimeOfDay` instance from the given hour, minute, and second components. + /// + /// # Errors + /// + /// - [TimeOfDayError::InvalidHour] if `hour` is not in the range `0..24`. + /// - [TimeOfDayError::InvalidMinute] if `minute` is not in the range `0..60`. + /// - [TimeOfDayError::InvalidSecond] if `second` is not in the range `0..61`. pub fn new(hour: u8, minute: u8, second: u8) -> Result { if !(0..24).contains(&hour) { return Err(TimeOfDayError::InvalidHour(hour)); @@ -131,6 +158,15 @@ impl TimeOfDay { }) } + /// Constructs a new `TimeOfDay` instance from an ISO 8601 time string. + /// + /// # Errors + /// + /// - [TimeOfDayError::InvalidIsoString] if the input string is not a valid ISO 8601 time + /// string. + /// - [TimeOfDayError::InvalidHour] if the hour component is not in the range `0..24`. + /// - [TimeOfDayError::InvalidMinute] if the minute component is not in the range `0..60`. + /// - [TimeOfDayError::InvalidSecond] if the second component is not in the range `0..61`. pub fn from_iso(iso: &str) -> Result { let caps = iso_regex() .captures(iso) @@ -155,6 +191,14 @@ impl TimeOfDay { Ok(time) } + /// Constructs a new `TimeOfDay` instance from the given hour, minute, and floating-point second + /// components. + /// + /// # Errors + /// + /// - [TimeOfDayError::InvalidHour] if `hour` is not in the range `0..24`. + /// - [TimeOfDayError::InvalidMinute] if `minute` is not in the range `0..60`. + /// - [TimeOfDayError::InvalidSeconds] if `seconds` is not in the range `0.0..86401.0`. pub fn from_hms(hour: u8, minute: u8, seconds: f64) -> Result { if !(0.0..86401.0).contains(&seconds) { return Err(TimeOfDayError::InvalidSeconds(InvalidSeconds(seconds))); @@ -165,6 +209,11 @@ impl TimeOfDay { Ok(Self::new(hour, minute, second)?.with_subsecond(subsecond)) } + /// Constructs a new `TimeOfDay` instance from the given second of a day. + /// + /// # Errors + /// + /// - [TimeOfDayError::InvalidSecondOfDay] if `second_of_day` is not in the range `0..86401`. pub fn from_second_of_day(second_of_day: u64) -> Result { if !(0..86401).contains(&second_of_day) { return Err(TimeOfDayError::InvalidSecondOfDay(second_of_day)); @@ -178,6 +227,9 @@ impl TimeOfDay { Self::new(hour, minute, second) } + /// Constructs a new `TimeOfDay` instance from an integral number of seconds since J2000. + /// + /// Note that this constructor is not leap-second aware. pub fn from_seconds_since_j2000(seconds: i64) -> Self { let mut second_of_day = (seconds + SECONDS_PER_HALF_DAY) % SECONDS_PER_DAY; if second_of_day.is_negative() { @@ -191,6 +243,7 @@ impl TimeOfDay { .unwrap_or_else(|_| unreachable!("second of day should be in range")) } + /// Sets the [TimeOfDay]'s subsecond component. pub fn with_subsecond(&mut self, subsecond: Subsecond) -> Self { self.subsecond = subsecond; *self @@ -212,6 +265,7 @@ impl TimeOfDay { self.subsecond } + /// Returns the number of integral seconds since the start of the day. pub fn second_of_day(&self) -> i64 { self.hour as i64 * SECONDS_PER_HOUR + self.minute as i64 * SECONDS_PER_MINUTE diff --git a/crates/lox-time/src/time_scales.rs b/crates/lox-time/src/time_scales.rs index c9f12e67..12a38f72 100644 --- a/crates/lox-time/src/time_scales.rs +++ b/crates/lox-time/src/time_scales.rs @@ -6,11 +6,17 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ -//! Module `time_scales` provides a marker trait with associated constants denoting a continuous -//! astronomical time scale, along with zero-sized implementations for the most commonly used -//! scales. +/*! + Module `time_scales` provides a marker trait denoting a continuous astronomical time scale, + along with zero-sized implementations for the most commonly used scales. -/// Marker trait with associated constants denoting a continuous astronomical time scale. + # Utc + + As a discontinuous time scale, [Utc] does not implement [TimeScale] and is treated by Lox + exclusively as an IO format. +*/ + +/// Marker trait denoting a continuous astronomical time scale. pub trait TimeScale { fn abbreviation(&self) -> &'static str; fn name(&self) -> &'static str; diff --git a/crates/lox-time/src/transformations.rs b/crates/lox-time/src/transformations.rs index cb205983..4331dff0 100644 --- a/crates/lox-time/src/transformations.rs +++ b/crates/lox-time/src/transformations.rs @@ -6,8 +6,10 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ -//! Module transform provides a trait for transforming between pairs of timescales, together -//! with a default implementation for the most commonly used time scale pairs. +/*! + Module `transformations` provides traits for transforming between pairs of [TimeScale]s, together + with default implementations for the most commonly used time scale pairs. +*/ use std::convert::Infallible; @@ -20,57 +22,79 @@ use crate::ut1::DeltaUt1TaiProvider; use crate::utc::Utc; use crate::Time; +/// Marker trait denoting a type that returns an offset between a pair of [TimeScale]s. pub trait OffsetProvider {} +/// A no-op [OffsetProvider] equivalent to `()`, used to guide the type system when implementing +/// transformations with constant offsets. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct NoOpOffsetProvider; impl OffsetProvider for NoOpOffsetProvider {} +/// The base trait underlying all time scale transformations. +/// +/// By default, `TryToScale` assumes that no [OffsetProvider] is required and that the +/// transformation is infallible. pub trait TryToScale: ToDelta { fn try_to_scale(&self, scale: T, provider: &U) -> Result, E>; } +/// `ToScale` narrows [TryToScale] for the case where no [OffsetProvider] is required and the +/// transformation is infallible. pub trait ToScale: TryToScale { fn to_scale(&self, scale: T) -> Time { self.try_to_scale(scale, &NoOpOffsetProvider).unwrap() } } +/// Blanket implementation of [ToScale] for all types that implement [TryToScale] infallibly with +/// no [OffsetProvider]. impl> ToScale for U {} +/// Convenience trait and default implementation for infallible conversions to [Tai] in terms of +/// [ToScale]. pub trait ToTai: ToScale { fn to_tai(&self) -> Time { self.to_scale(Tai) } } +/// Convenience trait and default implementation for infallible conversions to [Tt] in terms of +/// [ToScale]. pub trait ToTt: ToScale { fn to_tt(&self) -> Time { self.to_scale(Tt) } } +/// Convenience trait and default implementation for infallible conversions to [Tcg] in terms of +/// [ToScale]. pub trait ToTcg: ToScale { fn to_tcg(&self) -> Time { self.to_scale(Tcg) } } +/// Convenience trait and default implementation for infallible conversions to [Tcb] in terms of +/// [ToScale]. pub trait ToTcb: ToScale { fn to_tcb(&self) -> Time { self.to_scale(Tcb) } } +/// Convenience trait and default implementation for infallible conversions to [Tdb] in terms of +/// [ToScale]. pub trait ToTdb: ToScale { fn to_tdb(&self) -> Time { self.to_scale(Tdb) } } +/// Convenience trait and default implementation for conversions to [Ut1] in terms of [TryToScale]. pub trait ToUt1: TryToScale { fn try_to_ut1(&self, provider: &T) -> Result, T::Error> { self.try_to_scale(Ut1, provider) @@ -466,13 +490,19 @@ impl TryToScale for Time { } } +/// Implementers of `LeapSecondsProvider` provide the offset between TAI and UTC in leap seconds at +/// an instant in either time scale. pub trait LeapSecondsProvider: OffsetProvider { + /// The difference in leap seconds between TAI and UTC at the given TAI instant. fn delta_tai_utc(&self, tai: Time) -> Option; + /// The difference in leap seconds between UTC and TAI at the given UTC instant. fn delta_utc_tai(&self, utc: Utc) -> Option; + /// Returns `true` if a leap second occurs on `date`. fn is_leap_second_date(&self, date: Date) -> bool; + /// Returns `true` if a leap second occurs at `tai`. fn is_leap_second(&self, tai: Time) -> bool; } diff --git a/crates/lox-time/src/ut1.rs b/crates/lox-time/src/ut1.rs index 60c85091..3b7f3afd 100644 --- a/crates/lox-time/src/ut1.rs +++ b/crates/lox-time/src/ut1.rs @@ -6,6 +6,14 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ +/*! + Module `ut1` exposes [DeltaUt1TaiProvider], which describes an API for providing the delta + between UT1 and TAI at a time of interest. + + [DeltaUt1Tai] is `lox-time`'s default implementation of [DeltaUt1TaiProvider], which parses + Earth Orientation Parameters from an IERS CSV file. +*/ + use std::iter::zip; use thiserror::Error; @@ -24,13 +32,22 @@ use lox_utils::series::{Series, SeriesError}; use num::ToPrimitive; use std::path::Path; +/// Implementers of `DeltaUt1TaiProvider` provide the difference between UT1 and TAI at an instant +/// in either time scale. +/// +/// This crate provides a standard implementation over IERS Earth Orientation Parameters in +/// [DeltaUt1Tai]. pub trait DeltaUt1TaiProvider: OffsetProvider { type Error; + /// Returns the difference between UT1 and TAI at the given TAI instant. fn delta_ut1_tai(&self, tai: &Time) -> Result; + + /// Returns the difference between TAI and UT1 at the given UT1 instant. fn delta_tai_ut1(&self, ut1: &Time) -> Result; } +/// Error type returned when [DeltaUt1Tai] instantiation fails. #[derive(Clone, Debug, Error)] pub enum DeltaUt1TaiError { #[error(transparent)] @@ -39,6 +56,11 @@ pub enum DeltaUt1TaiError { Series(#[from] SeriesError), } +/// Error type indicating that an input date to [DeltaUt1Tai] was outside the range of available +/// Earth Orientation Parameters. +/// +/// It includes an extrapolated value for the input date which is unlikely to be accurate and should +/// be used with great caution. #[derive(Clone, Debug, Error, PartialEq, Eq)] #[error("UT1-TAI is only available between {min_date} and {max_date}; value for {req_date} was extrapolated")] pub struct ExtrapolatedDeltaUt1Tai { @@ -49,7 +71,7 @@ pub struct ExtrapolatedDeltaUt1Tai { } impl ExtrapolatedDeltaUt1Tai { - pub fn new(t0: f64, tn: f64, t: f64, val: f64) -> Self { + fn new(t0: f64, tn: f64, t: f64, val: f64) -> Self { let min_date = Time::new(Tai, t0.to_i64().unwrap(), Subsecond::default()); let max_date = Time::new(Tai, tn.to_i64().unwrap(), Subsecond::default()); let req_date = Time::new(Tai, t.to_i64().unwrap(), Subsecond::default()); @@ -62,10 +84,21 @@ impl ExtrapolatedDeltaUt1Tai { } } +/// Provides a standard implementation of [DeltaUt1TaiProvider] based on cubic spline interpolation +/// of the target time over IERS Earth Orientation Parameters. #[derive(Clone, Debug, PartialEq)] pub struct DeltaUt1Tai(Series, Vec>); impl DeltaUt1Tai { + /// Instantiates a new [DeltaUt1Tai] provider from a path to an IERS Earth Orientation + /// Parameters finals CSV and a [LeapSecondsProvider]. + /// + /// `ls` should provide leap second data for the full range of the EOP data. + /// + /// # Errors + /// + /// - [DeltaUt1TaiError::Csv] if the CSV file could not be parsed. + /// - [DeltaUt1TaiError::Series] if construction of a cubic spline from the input series fails. pub fn new>( path: P, ls: &impl LeapSecondsProvider, diff --git a/crates/lox-time/src/utc.rs b/crates/lox-time/src/utc.rs index e8583570..d4f28cd6 100644 --- a/crates/lox-time/src/utc.rs +++ b/crates/lox-time/src/utc.rs @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2023. Helge Eichhorn and the LOX contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + */ + +/*! + Module `utc` exposes [Utc], a leap-second aware representation for UTC datetimes. + + Due to the complexity inherent in working with leap seconds, it is intentionally segregated + from the continuous time formats, and is used exclusively as an input format to Lox. +*/ + use std::fmt::{self, Display, Formatter}; use std::str::FromStr; @@ -16,6 +31,7 @@ use self::leap_seconds::BuiltinLeapSeconds; pub mod leap_seconds; pub mod transformations; +/// Error type returned when attempting to construct a [Utc] instance from invalid inputs. #[derive(Debug, Clone, Error, PartialEq, Eq, PartialOrd, Ord)] pub enum UtcError { #[error(transparent)] @@ -30,6 +46,7 @@ pub enum UtcError { InvalidIsoString(String), } +/// Coordinated Universal Time. #[derive(Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct Utc { date: Date, @@ -37,6 +54,14 @@ pub struct Utc { } impl Utc { + /// Creates a new [Utc] instance from the given [Date] and [TimeOfDay], with leap second + /// validation provided by the [LeapSecondsProvider]. + /// + /// # Errors + /// + /// - [UtcError::UtcUndefined] if the date is before 1960-01-01. + /// - [UtcError::NonLeapSecondDate] if `time.seconds` is 60 seconds and the date is not a leap + /// second date. pub fn new( date: Date, time: TimeOfDay, @@ -51,10 +76,22 @@ impl Utc { Ok(Self { date, time }) } + /// Returns a new [UtcBuilder]. pub fn builder() -> UtcBuilder { UtcBuilder::default() } + /// Constructs a new [Utc] instance from the given ISO 8601 string, with leap second validation + /// provided by the [LeapSecondsProvider]. + /// + /// # Errors + /// + /// - [UtcError::InvalidIsoString] if the input string is not a valid ISO 8601 string. + /// - [UtcError::DateError] if the date component of the string is invalid. + /// - [UtcError::TimeError] if the time component of the string is invalid. + /// - [UtcError::UtcUndefined] if the date is before 1960-01-01. + /// - [UtcError::NonLeapSecondDate] if the time component is 60 seconds and the date is not a + /// leap second date. pub fn from_iso_with_provider( iso: &str, provider: &T, @@ -80,10 +117,15 @@ impl Utc { Utc::new(date, time, provider) } + /// Constructs a new [Utc] instance from the given ISO 8601 string, with leap second validation + /// provided by [BuiltinLeapSeconds]. pub fn from_iso(iso: &str) -> Result { Self::from_iso_with_provider(iso, &BuiltinLeapSeconds) } + /// Constructs a new [Utc] instance from a [TimeDelta] relative to J2000. + /// + /// Note that this constructor is not leap-second aware. pub fn from_delta(delta: TimeDelta) -> Self { let date = Date::from_seconds_since_j2000(delta.seconds); let time = @@ -135,6 +177,7 @@ impl CivilTime for Utc { } } +/// A builder for constructing [Utc] instances piecewise. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct UtcBuilder { date: Result, @@ -142,6 +185,7 @@ pub struct UtcBuilder { } impl Default for UtcBuilder { + /// Returns a new [UtcBuilder] at 2000-01-01T00:00:00.000 UTC. fn default() -> Self { Self { date: Ok(Date::default()), @@ -151,6 +195,7 @@ impl Default for UtcBuilder { } impl UtcBuilder { + /// Sets the year, month and day fields of the [Utc] instance being built. pub fn with_ymd(self, year: i64, month: u8, day: u8) -> Self { Self { date: Date::new(year, month, day), @@ -158,6 +203,7 @@ impl UtcBuilder { } } + /// Sets the hour, minute, second and subsecond fields of the [Utc] instance being built. pub fn with_hms(self, hour: u8, minute: u8, seconds: f64) -> Self { Self { time: TimeOfDay::from_hms(hour, minute, seconds), @@ -165,17 +211,33 @@ impl UtcBuilder { } } + /// Constructs the [Utc] instance with leap second validation provided by the given + /// [LeapSecondsProvider]. pub fn build_with_provider(self, provider: &impl LeapSecondsProvider) -> Result { let date = self.date?; let time = self.time?; Utc::new(date, time, provider) } + /// Constructs the [Utc] instance with leap second validation provided by [BuiltinLeapSeconds]. pub fn build(self) -> Result { self.build_with_provider(&BuiltinLeapSeconds) } } +/// The `utc` macro simplifies the creation of [Utc] instances. +/// +/// # Examples +/// +/// ```rust +/// use lox_time::utc; +/// use lox_time::utc::Utc; +/// +/// utc!(2000, 1, 2); // 2000-01-02T00:00:00.000 UTC +/// utc!(2000, 1, 2, 3); // 2000-01-01T03:00:00.000 UTC +/// utc!(2000, 1, 2, 3, 4); // 2000-01-01T03:04:00.000 UTC +/// utc!(2000, 1, 2, 3, 4, 5.6); // 2000-01-01T03:04:05.600 UTC +/// ``` #[macro_export] macro_rules! utc { ($year:literal, $month:literal, $day:literal) => { diff --git a/crates/lox-time/src/utc/leap_seconds.rs b/crates/lox-time/src/utc/leap_seconds.rs index fb8d3dad..4f1ffd3c 100644 --- a/crates/lox-time/src/utc/leap_seconds.rs +++ b/crates/lox-time/src/utc/leap_seconds.rs @@ -6,6 +6,17 @@ * file, you can obtain one at https://mozilla.org/MPL/2.0/. */ +/*! + Module `leap_seconds` exposes the [LeapSecondsProvider] trait for defining sources of leap + second data. Lox's standard implementation, [BuiltinLeapSeconds], is suitable for most + applications. + + `leap_seconds` additionally exposes the lower-level [LeapSecondsKernel] for working directly + with [NAIF Leap Seconds Kernel][LSK] data. + + [LSK]: https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/time.html#The%20Leapseconds%20Kernel%20LSK +*/ + use crate::calendar_dates::{Date, DateError}; use crate::constants::i64::{SECONDS_PER_DAY, SECONDS_PER_HALF_DAY}; use crate::deltas::{TimeDelta, ToDelta}; @@ -41,6 +52,12 @@ const LEAP_SECONDS: [i64; 28] = [ 34, 35, 36, 37, ]; +/// `lox-time`'s default [LeapSecondsProvider], suitable for most applications. +/// +/// `BuiltinLeapSeconds` relies on a hard-coded table of leap second data. As new leap seconds are +/// announced, `lox-time` will be updated to include the new data, reflected by a minor version +/// change. If this is unsuitable for your use case, we recommend implementing [LeapSecondsProvider] +/// manually. #[derive(Debug)] pub struct BuiltinLeapSeconds; @@ -64,6 +81,7 @@ impl LeapSecondsProvider for BuiltinLeapSeconds { } } +/// Error type related to parsing leap seconds data from a NAIF Leap Seconds Kernel. #[derive(Debug, Error)] pub enum LeapSecondsKernelError { #[error(transparent)] @@ -81,6 +99,10 @@ pub enum LeapSecondsKernelError { DateError(#[from] DateError), } +/// In-memory representation of a NAIF Leap Seconds Kernel. +/// +/// Most users should prefer [BuiltinLeapSeconds] to implementing their own [LeapSecondsProvider] +/// using the kernel. #[derive(Debug)] pub struct LeapSecondsKernel { epochs_utc: Vec, @@ -89,6 +111,16 @@ pub struct LeapSecondsKernel { } impl LeapSecondsKernel { + /// Parse a NAIF Leap Seconds Kernel from a string. + /// + /// # Errors + /// + /// - [LeapSecondsKernelError::Kernel] if the kernel format is unparseable. + /// - [LeapSecondsKernelError::NoLeapSeconds] if the kernel contains no leap second data. + /// - [LeapSecondsKernelError::ParseInt] if a leap second entry in the kernel can't be + /// represented as an i64. + /// - [LeapSecondsKernelError::DateError] if a date contained within the kernel is not + /// represented as a valid ISO 8601 string. pub fn from_string(kernel: impl AsRef) -> Result { let kernel = Kernel::from_string(kernel.as_ref())?; let data = kernel @@ -117,6 +149,17 @@ impl LeapSecondsKernel { }) } + /// Parse a NAIF Leap Seconds Kernel located at `path`. + /// + /// # Errors + /// + /// - [LeapSecondsKernelError::Io] if the file at `path` can't be read. + /// - [LeapSecondsKernelError::Kernel] if the kernel format is unparseable. + /// - [LeapSecondsKernelError::NoLeapSeconds] if the kernel contains no leap second data. + /// - [LeapSecondsKernelError::ParseInt] if a leap second entry in the kernel can't be + /// represented as an i64. + /// - [LeapSecondsKernelError::DateError] if a date contained within the kernel is not + /// represented as a valid ISO 8601 string. pub fn from_file(path: impl AsRef) -> Result { let path = path.as_ref(); let kernel = read_to_string(path)?; diff --git a/crates/lox-time/src/utc/transformations.rs b/crates/lox-time/src/utc/transformations.rs index 4355b378..ae3869c6 100644 --- a/crates/lox-time/src/utc/transformations.rs +++ b/crates/lox-time/src/utc/transformations.rs @@ -139,7 +139,7 @@ fn tai_at_utc_1972_01_01() -> &'static Time { } #[cfg(test)] -pub mod test { +mod test { use crate::test_helpers::delta_ut1_tai; use crate::time; use crate::transformations::{ToTcb, ToTcg, ToTdb, ToTt};