diff --git a/.idea/lox-space.iml b/.idea/lox-space.iml
index b2060bea..1e600b65 100644
--- a/.idea/lox-space.iml
+++ b/.idea/lox-space.iml
@@ -21,6 +21,7 @@
+
diff --git a/crates/lox-space/examples/iss.rs b/crates/lox-space/examples/iss.rs
index 66767908..ecc8e060 100644
--- a/crates/lox-space/examples/iss.rs
+++ b/crates/lox-space/examples/iss.rs
@@ -6,15 +6,16 @@
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*/
+use lox_core::time::utc::UTC;
use lox_space::prelude::*;
fn main() {
let date = Date::new(2016, 5, 30).unwrap();
- let time = Time::new(12, 0, 0).unwrap();
- let epoch = Epoch::from_date_and_time(TimeScale::TDB, date, time);
+ let utc = UTC::new(12, 0, 0).unwrap();
+ let time = Time::from_date_and_utc_timestamp(TimeScale::TDB, date, utc);
let position = DVec3::new(6068279.27, -1692843.94, -2516619.18) * 1e-3;
let velocity = DVec3::new(-660.415582, 5495.938726, -5303.093233) * 1e-3;
- let iss_cartesian = Cartesian::new(epoch, Earth, Icrf, position, velocity);
+ let iss_cartesian = Cartesian::new(time, Earth, Icrf, position, velocity);
let iss = Keplerian::from(iss_cartesian);
println!("ISS Orbit for Julian Day {}", iss.time().days_since_j2000(),);
diff --git a/crates/lox-space/src/prelude.rs b/crates/lox-space/src/prelude.rs
index b9f9c620..3f8a9894 100644
--- a/crates/lox-space/src/prelude.rs
+++ b/crates/lox-space/src/prelude.rs
@@ -7,8 +7,9 @@
*/
pub use lox_core::bodies::*;
+
pub use lox_core::coords::two_body::{Cartesian, Keplerian};
pub use lox_core::coords::DVec3;
pub use lox_core::frames::*;
+pub use lox_core::time::continuous::*;
pub use lox_core::time::dates::*;
-pub use lox_core::time::epochs::*;
diff --git a/crates/lox_core/src/coords/states.rs b/crates/lox_core/src/coords/states.rs
index 94900e47..6de8772c 100644
--- a/crates/lox_core/src/coords/states.rs
+++ b/crates/lox_core/src/coords/states.rs
@@ -10,23 +10,23 @@ use float_eq::float_eq;
use glam::{DMat3, DVec3};
use crate::math::{mod_two_pi, normalize_two_pi};
-use crate::time::epochs::Epoch;
+use crate::time::continuous::Time;
pub trait TwoBodyState {
- fn time(&self) -> Epoch;
+ fn time(&self) -> Time;
fn to_cartesian_state(&self, grav_param: f64) -> CartesianState;
fn to_keplerian_state(&self, grav_param: f64) -> KeplerianState;
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct CartesianState {
- time: Epoch,
+ time: Time,
position: DVec3,
velocity: DVec3,
}
impl CartesianState {
- pub fn new(time: Epoch, position: DVec3, velocity: DVec3) -> Self {
+ pub fn new(time: Time, position: DVec3, velocity: DVec3) -> Self {
Self {
time,
position,
@@ -44,7 +44,7 @@ impl CartesianState {
}
impl TwoBodyState for CartesianState {
- fn time(&self) -> Epoch {
+ fn time(&self) -> Time {
self.time
}
@@ -120,7 +120,7 @@ impl TwoBodyState for CartesianState {
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct KeplerianState {
- time: Epoch,
+ time: Time,
semi_major: f64,
eccentricity: f64,
inclination: f64,
@@ -131,7 +131,7 @@ pub struct KeplerianState {
impl KeplerianState {
pub fn new(
- time: Epoch,
+ time: Time,
semi_major: f64,
eccentricity: f64,
inclination: f64,
@@ -196,7 +196,7 @@ impl KeplerianState {
}
impl TwoBodyState for KeplerianState {
- fn time(&self) -> Epoch {
+ fn time(&self) -> Time {
self.time
}
@@ -231,7 +231,7 @@ fn is_circular(eccentricity: f64) -> bool {
#[cfg(test)]
mod tests {
- use crate::time::epochs::TimeScale;
+ use crate::time::continuous::TimeScale;
use float_eq::assert_float_eq;
use glam::DVec3;
@@ -239,7 +239,7 @@ mod tests {
#[test]
fn test_elliptic() {
- let time = Epoch::j2000(TimeScale::TDB);
+ let time = Time::j2000(TimeScale::TDB);
let grav_param = 3.9860047e14;
let semi_major = 24464560.0;
let eccentricity = 0.7311;
@@ -295,7 +295,7 @@ mod tests {
#[test]
fn test_circular() {
- let time = Epoch::j2000(TimeScale::TDB);
+ let time = Time::j2000(TimeScale::TDB);
let grav_param = 3.986004418e14;
let semi_major = 6778136.6;
let eccentricity = 0.0;
@@ -336,7 +336,7 @@ mod tests {
#[test]
fn test_circular_orekit() {
- let time = Epoch::j2000(TimeScale::TDB);
+ let time = Time::j2000(TimeScale::TDB);
let grav_param = 3.9860047e14;
let semi_major = 24464560.0;
let eccentricity = 0.0;
@@ -368,7 +368,7 @@ mod tests {
#[test]
fn test_hyperbolic_orekit() {
- let time = Epoch::j2000(TimeScale::TDB);
+ let time = Time::j2000(TimeScale::TDB);
let grav_param = 3.9860047e14;
let semi_major = -24464560.0;
let eccentricity = 1.7311;
@@ -400,7 +400,7 @@ mod tests {
#[test]
fn test_equatorial() {
- let time = Epoch::j2000(TimeScale::TDB);
+ let time = Time::j2000(TimeScale::TDB);
let grav_param = 3.9860047e14;
let semi_major = 24464560.0;
let eccentricity = 0.7311;
@@ -432,7 +432,7 @@ mod tests {
#[test]
fn test_circular_equatorial() {
- let time = Epoch::j2000(TimeScale::TDB);
+ let time = Time::j2000(TimeScale::TDB);
let grav_param = 3.9860047e14;
let semi_major = 24464560.0;
let eccentricity = 0.0;
diff --git a/crates/lox_core/src/coords/two_body.rs b/crates/lox_core/src/coords/two_body.rs
index 8a15cf5a..4484f669 100644
--- a/crates/lox_core/src/coords/two_body.rs
+++ b/crates/lox_core/src/coords/two_body.rs
@@ -12,7 +12,7 @@ use crate::bodies::PointMass;
use crate::coords::states::{CartesianState, KeplerianState, TwoBodyState};
use crate::coords::CoordinateSystem;
use crate::frames::{InertialFrame, ReferenceFrame};
-use crate::time::epochs::Epoch;
+use crate::time::continuous::Time;
pub trait TwoBody
where
@@ -40,7 +40,7 @@ where
T: PointMass + Copy,
S: ReferenceFrame + Copy,
{
- pub fn new(time: Epoch, origin: T, frame: S, position: DVec3, velocity: DVec3) -> Self {
+ pub fn new(time: Time, origin: T, frame: S, position: DVec3, velocity: DVec3) -> Self {
let state = CartesianState::new(time, position, velocity);
Self {
state,
@@ -49,7 +49,7 @@ where
}
}
- pub fn time(&self) -> Epoch {
+ pub fn time(&self) -> Time {
self.state.time()
}
@@ -127,7 +127,7 @@ where
{
#[allow(clippy::too_many_arguments)]
pub fn new(
- time: Epoch,
+ time: Time,
origin: T,
frame: S,
semi_major: f64,
@@ -153,7 +153,7 @@ where
}
}
- pub fn time(&self) -> Epoch {
+ pub fn time(&self) -> Time {
self.state.time()
}
@@ -233,18 +233,18 @@ where
mod tests {
use float_eq::assert_float_eq;
+ use super::*;
use crate::bodies::Earth;
use crate::frames::Icrf;
- use crate::time::dates::{Date, Time};
- use crate::time::epochs::TimeScale;
-
- use super::*;
+ use crate::time::continuous::{Time, TimeScale};
+ use crate::time::dates::Date;
+ use crate::time::utc::UTC;
#[test]
fn test_cartesian() {
let date = Date::new(2023, 3, 25).expect("Date should be valid");
- let time = Time::new(21, 8, 0).expect("Time should be valid");
- let epoch = Epoch::from_date_and_time(TimeScale::TDB, date, time);
+ let utc = UTC::new(21, 8, 0).expect("Time should be valid");
+ let time = Time::from_date_and_utc_timestamp(TimeScale::TDB, date, utc);
let pos = DVec3::new(
-0.107622532467967e7,
-0.676589636432773e7,
@@ -256,12 +256,12 @@ mod tests {
-0.118801577532701e4,
) * 1e-3;
- let cartesian = Cartesian::new(epoch, Earth, Icrf, pos, vel);
+ let cartesian = Cartesian::new(time, Earth, Icrf, pos, vel);
assert_eq!(cartesian.to_cartesian(), cartesian);
let cartesian1 = cartesian.to_keplerian().to_cartesian();
- assert_eq!(cartesian1.time(), epoch);
+ assert_eq!(cartesian1.time(), time);
assert_eq!(cartesian1.origin(), Earth);
assert_eq!(cartesian1.reference_frame(), Icrf);
@@ -276,8 +276,8 @@ mod tests {
#[test]
fn test_keplerian() {
let date = Date::new(2023, 3, 25).expect("Date should be valid");
- let time = Time::new(21, 8, 0).expect("Time should be valid");
- let epoch = Epoch::from_date_and_time(TimeScale::TDB, date, time);
+ let utc = UTC::new(21, 8, 0).expect("Time should be valid");
+ let time = Time::from_date_and_utc_timestamp(TimeScale::TDB, date, utc);
let semi_major = 24464560.0e-3;
let eccentricity = 0.7311;
let inclination = 0.122138;
@@ -286,7 +286,7 @@ mod tests {
let true_anomaly = 0.44369564302687126;
let keplerian = Keplerian::new(
- epoch,
+ time,
Earth,
Icrf,
semi_major,
@@ -300,7 +300,7 @@ mod tests {
let keplerian1 = keplerian.to_cartesian().to_keplerian();
- assert_eq!(keplerian1.time(), epoch);
+ assert_eq!(keplerian1.time(), time);
assert_eq!(keplerian1.origin(), Earth);
assert_eq!(keplerian1.reference_frame(), Icrf);
diff --git a/crates/lox_core/src/earth/nutation.rs b/crates/lox_core/src/earth/nutation.rs
index ae03eeeb..9500a694 100644
--- a/crates/lox_core/src/earth/nutation.rs
+++ b/crates/lox_core/src/earth/nutation.rs
@@ -16,7 +16,7 @@ use crate::earth::nutation::iau2000::nutation_iau2000a;
use crate::earth::nutation::iau2000::nutation_iau2000b;
use crate::earth::nutation::iau2006::nutation_iau2006a;
use crate::math::RADIANS_IN_ARCSECOND;
-use crate::time::epochs::Epoch;
+use crate::time::continuous::Time;
use crate::time::intervals::tdb_julian_centuries_since_j2000;
use crate::types::Radians;
@@ -63,9 +63,9 @@ impl Add<&Self> for Nutation {
}
}
-/// Calculate nutation coefficients at `epoch` using the given [Model].
-pub fn nutation(model: Model, epoch: Epoch) -> Nutation {
- let t = tdb_julian_centuries_since_j2000(epoch);
+/// Calculate nutation coefficients at `time` using the given [Model].
+pub fn nutation(model: Model, time: Time) -> Nutation {
+ let t = tdb_julian_centuries_since_j2000(time);
match model {
Model::IAU1980 => nutation_iau1980(t),
Model::IAU2000A => nutation_iau2000a(t),
@@ -98,57 +98,56 @@ fn point1_microarcsec_to_rad(p1_uas: Point1Microarcsec) -> Radians {
#[cfg(test)]
mod tests {
+ use crate::time::continuous::TimeScale;
use float_eq::assert_float_eq;
- use crate::time::epochs::{Epoch, TimeScale};
-
use super::*;
const TOLERANCE: f64 = 1e-12;
#[test]
fn test_nutation_iau1980() {
- let epoch = Epoch::j2000(TimeScale::TT);
+ let time = Time::j2000(TimeScale::TT);
let expected = Nutation {
longitude: -0.00006750247617532478,
obliquity: -0.00002799221238377013,
};
- let actual = nutation(Model::IAU1980, epoch);
+ let actual = nutation(Model::IAU1980, time);
assert_float_eq!(expected.longitude, actual.longitude, rel <= TOLERANCE);
assert_float_eq!(expected.obliquity, actual.obliquity, rel <= TOLERANCE);
}
#[test]
fn test_nutation_iau2000a() {
- let epoch = Epoch::j2000(TimeScale::TT);
+ let time = Time::j2000(TimeScale::TT);
let expected = Nutation {
longitude: -0.00006754422426417299,
obliquity: -0.00002797083119237414,
};
- let actual = nutation(Model::IAU2000A, epoch);
+ let actual = nutation(Model::IAU2000A, time);
assert_float_eq!(expected.longitude, actual.longitude, rel <= TOLERANCE);
assert_float_eq!(expected.obliquity, actual.obliquity, rel <= TOLERANCE);
}
#[test]
fn test_nutation_iau2000b() {
- let epoch = Epoch::j2000(TimeScale::TT);
+ let time = Time::j2000(TimeScale::TT);
let expected = Nutation {
longitude: -0.00006754261253992235,
obliquity: -0.00002797092331098565,
};
- let actual = nutation(Model::IAU2000B, epoch);
+ let actual = nutation(Model::IAU2000B, time);
assert_float_eq!(expected.longitude, actual.longitude, rel <= TOLERANCE);
assert_float_eq!(expected.obliquity, actual.obliquity, rel <= TOLERANCE);
}
#[test]
fn test_nutation_iau2006a() {
- let epoch = Epoch::j2000(TimeScale::TT);
+ let time = Time::j2000(TimeScale::TT);
let expected = Nutation {
longitude: -0.00006754425598969513,
obliquity: -0.00002797083119237414,
};
- let actual = nutation(Model::IAU2006A, epoch);
+ let actual = nutation(Model::IAU2006A, time);
assert_float_eq!(expected.longitude, actual.longitude, rel <= TOLERANCE);
assert_float_eq!(expected.obliquity, actual.obliquity, rel <= TOLERANCE);
}
diff --git a/crates/lox_core/src/errors.rs b/crates/lox_core/src/errors.rs
index 926c19f5..b4cd4ea9 100644
--- a/crates/lox_core/src/errors.rs
+++ b/crates/lox_core/src/errors.rs
@@ -8,14 +8,16 @@
use thiserror::Error;
-#[derive(Error, Debug)]
+#[derive(Error, Debug, PartialEq)]
pub enum LoxError {
#[error("invalid date `{0}-{1}-{2}`")]
InvalidDate(i64, i64, i64),
#[error("invalid time `{0}:{1}:{2}`")]
- InvalidTime(i64, i64, i64),
- #[error("invalid time `{0}:{1}:{2}`")]
- InvalidSeconds(i64, i64, f64),
+ InvalidTime(u8, u8, u8),
+ #[error("seconds must be in the range [0.0, 60.0], but was `{0}`")]
+ InvalidSeconds(f64),
+ #[error("PerMille value must be in the range [0, 999], but was `{0}`")]
+ InvalidPerMille(u16),
#[error("day of year cannot be 366 for a non-leap year")]
NonLeapYear,
#[error("unknown body `{0}`")]
diff --git a/crates/lox_core/src/time.rs b/crates/lox_core/src/time.rs
index bd1c7a95..055248c5 100644
--- a/crates/lox_core/src/time.rs
+++ b/crates/lox_core/src/time.rs
@@ -6,8 +6,155 @@
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*/
+use crate::errors::LoxError;
+use std::fmt;
+use std::fmt::{Display, Formatter};
+
pub mod constants;
+pub mod continuous;
pub mod dates;
-pub mod epochs;
pub mod intervals;
pub mod leap_seconds;
+pub mod utc;
+
+/// `WallClock` is the trait by which high-precision time representations expose human-readable time components.
+pub trait WallClock {
+ fn hour(&self) -> i64;
+ fn minute(&self) -> i64;
+ fn second(&self) -> i64;
+ fn millisecond(&self) -> i64;
+ fn microsecond(&self) -> i64;
+ fn nanosecond(&self) -> i64;
+ fn picosecond(&self) -> i64;
+ fn femtosecond(&self) -> i64;
+ fn attosecond(&self) -> i64;
+}
+
+/// Newtype wrapper for thousandths of an SI-prefixed subsecond (milli, micro, nano, etc.).
+#[repr(transparent)]
+#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct PerMille(u16);
+
+impl PerMille {
+ pub fn new(per_mille: u16) -> Result {
+ if !(0..1000).contains(&per_mille) {
+ Err(LoxError::InvalidPerMille(per_mille))
+ } else {
+ Ok(Self(per_mille))
+ }
+ }
+}
+
+impl Display for PerMille {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(f, "{:03}", self.0)
+ }
+}
+
+impl TryFrom for PerMille {
+ type Error = LoxError;
+
+ fn try_from(per_mille: u16) -> Result {
+ Self::new(per_mille)
+ }
+}
+
+#[allow(clippy::from_over_into)] // the Into conversion is infallible, but From is not
+impl Into for PerMille {
+ fn into(self) -> i64 {
+ self.0 as i64
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::errors::LoxError;
+ use crate::time::PerMille;
+
+ #[test]
+ fn test_per_mille_new() {
+ struct TestCase {
+ desc: &'static str,
+ input: u16,
+ expected: Result,
+ }
+
+ let test_cases = [
+ TestCase {
+ desc: "on lower bound",
+ input: 0,
+ expected: Ok(PerMille(0)),
+ },
+ TestCase {
+ desc: "between bounds",
+ input: 1,
+ expected: Ok(PerMille(1)),
+ },
+ TestCase {
+ desc: "on upper bound",
+ input: 999,
+ expected: Ok(PerMille(999)),
+ },
+ TestCase {
+ desc: "above upper bound",
+ input: 1000,
+ expected: Err(LoxError::InvalidPerMille(1000)),
+ },
+ ];
+
+ for tc in test_cases {
+ let actual = PerMille::new(tc.input);
+ assert_eq!(
+ actual, tc.expected,
+ "expected {:?} when input is {:?}, but got {:?}",
+ tc.expected, tc.input, tc.desc
+ );
+ }
+ }
+
+ #[test]
+ fn test_per_mille_display() {
+ struct TestCase {
+ input: PerMille,
+ expected: &'static str,
+ }
+
+ let test_cases = [
+ TestCase {
+ input: PerMille(1),
+ expected: "001",
+ },
+ TestCase {
+ input: PerMille(11),
+ expected: "011",
+ },
+ TestCase {
+ input: PerMille(111),
+ expected: "111",
+ },
+ ];
+
+ for tc in test_cases {
+ let actual = format!("{}", tc.input);
+ assert_eq!(
+ actual, tc.expected,
+ "expected {:?} when input is {:?}, but got {:?}",
+ tc.expected, tc.input, actual,
+ );
+ }
+ }
+
+ #[test]
+ fn test_per_mille_try_from() {
+ assert_eq!(PerMille::try_from(0), Ok(PerMille(0)));
+ assert_eq!(
+ PerMille::try_from(1000),
+ Err(LoxError::InvalidPerMille(1000))
+ );
+ }
+
+ #[test]
+ fn test_per_mille_into_i64() {
+ assert_eq!(Into::::into(PerMille(0)), 0i64);
+ }
+}
diff --git a/crates/lox_core/src/time/constants.rs b/crates/lox_core/src/time/constants.rs
index a3ccdcc1..3634df7c 100644
--- a/crates/lox_core/src/time/constants.rs
+++ b/crates/lox_core/src/time/constants.rs
@@ -8,3 +8,4 @@
pub mod f64;
pub mod i64;
+pub mod u64;
diff --git a/crates/lox_core/src/time/constants/i64.rs b/crates/lox_core/src/time/constants/i64.rs
index c8e5f3b5..65644b7e 100644
--- a/crates/lox_core/src/time/constants/i64.rs
+++ b/crates/lox_core/src/time/constants/i64.rs
@@ -11,3 +11,15 @@ pub const SECONDS_PER_MINUTE: i64 = 60;
pub const SECONDS_PER_HOUR: i64 = 60 * SECONDS_PER_MINUTE;
pub const SECONDS_PER_DAY: i64 = 24 * SECONDS_PER_HOUR;
+
+pub const SECONDS_PER_HALF_DAY: i64 = SECONDS_PER_DAY / 2;
+
+pub const MILLISECONDS_PER_SECOND: i64 = 1_000;
+
+pub const MICROSECONDS_PER_SECOND: i64 = 1_000_000;
+
+pub const NANOSECONDS_PER_SECOND: i64 = 1_000_000_000;
+
+pub const PICOSECONDS_PER_SECOND: i64 = 1_000_000_000_000;
+
+pub const FEMTOSECONDS_PER_SECOND: i64 = 1_000_000_000_000_000;
diff --git a/crates/lox_core/src/time/constants/u64.rs b/crates/lox_core/src/time/constants/u64.rs
new file mode 100644
index 00000000..06c5b990
--- /dev/null
+++ b/crates/lox_core/src/time/constants/u64.rs
@@ -0,0 +1,29 @@
+/*
+ * 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/.
+ */
+
+pub const MILLISECONDS_PER_SECOND: u64 = 1_000;
+
+pub const MICROSECONDS_PER_SECOND: u64 = 1_000_000;
+
+pub const NANOSECONDS_PER_SECOND: u64 = 1_000_000_000;
+
+pub const PICOSECONDS_PER_SECOND: u64 = 1_000_000_000_000;
+
+pub const FEMTOSECONDS_PER_SECOND: u64 = 1_000_000_000_000_000;
+
+pub const ATTOSECONDS_PER_SECOND: u64 = 1_000_000_000_000_000_000;
+
+pub const ATTOSECONDS_PER_MILLISECOND: u64 = 1_000_000_000_000_000;
+
+pub const ATTOSECONDS_PER_MICROSECOND: u64 = 1_000_000_000_000;
+
+pub const ATTOSECONDS_PER_NANOSECOND: u64 = 1_000_000_000;
+
+pub const ATTOSECONDS_PER_PICOSECOND: u64 = 1_000_000;
+
+pub const ATTOSECONDS_PER_FEMTOSECOND: u64 = 1_000;
diff --git a/crates/lox_core/src/time/continuous.rs b/crates/lox_core/src/time/continuous.rs
new file mode 100644
index 00000000..e482802a
--- /dev/null
+++ b/crates/lox_core/src/time/continuous.rs
@@ -0,0 +1,1413 @@
+/*
+ * 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 continuous provides representations and functions for working with time scales without leap seconds.
+//!
+//! Continuous times are represented with attosecond precision.
+//!
+//! The supported timescales are specified by [TimeScale].
+
+use std::fmt;
+use std::fmt::{Display, Formatter};
+use std::ops::{Add, Sub};
+
+use num::{abs, ToPrimitive};
+
+use crate::time::constants::i64::{
+ SECONDS_PER_DAY, SECONDS_PER_HALF_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE,
+};
+use crate::time::constants::u64::{
+ ATTOSECONDS_PER_FEMTOSECOND, ATTOSECONDS_PER_MICROSECOND, ATTOSECONDS_PER_MILLISECOND,
+ ATTOSECONDS_PER_NANOSECOND, ATTOSECONDS_PER_PICOSECOND, ATTOSECONDS_PER_SECOND,
+};
+use crate::time::dates::Calendar::ProlepticJulian;
+use crate::time::dates::Date;
+use crate::time::utc::{UTCDateTime, UTC};
+use crate::time::{constants, WallClock};
+
+/// An absolute continuous time difference with attosecond precision.
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub struct TimeDelta {
+ seconds: u64,
+ attoseconds: u64,
+}
+
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
+/// `RawTime` is the base time representation for time scales without leap seconds. It is measured relative to
+/// J2000. `RawTime::default()` represents the epoch itself.
+///
+/// `RawTime` has attosecond precision, and supports times within 292 billion years either side of the epoch.
+pub struct RawTime {
+ // The sign of the time is determined exclusively by the sign of the `second` field. `attoseconds` is always the
+ // positive count of attoseconds since the last whole second. For example, one attosecond before the epoch is
+ // represented as
+ // ```
+ // let time = RawTime {
+ // seconds: -1,
+ // attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ // };
+ // ```
+ seconds: i64,
+ attoseconds: u64,
+}
+
+impl RawTime {
+ fn is_negative(&self) -> bool {
+ self.seconds < 0
+ }
+
+ fn hour(&self) -> i64 {
+ // Since J2000 is taken from midday, we offset by half a day to get the wall clock hour.
+ let day_seconds: i64 = if self.is_negative() {
+ SECONDS_PER_DAY - (abs(self.seconds) + SECONDS_PER_HALF_DAY) % SECONDS_PER_DAY
+ } else {
+ (self.seconds + SECONDS_PER_HALF_DAY) % SECONDS_PER_DAY
+ };
+ day_seconds / SECONDS_PER_HOUR
+ }
+
+ fn minute(&self) -> i64 {
+ let hour_seconds: i64 = if self.is_negative() {
+ SECONDS_PER_HOUR - abs(self.seconds) % SECONDS_PER_HOUR
+ } else {
+ self.seconds % SECONDS_PER_HOUR
+ };
+ hour_seconds / SECONDS_PER_MINUTE
+ }
+
+ fn second(&self) -> i64 {
+ if self.is_negative() {
+ SECONDS_PER_MINUTE - abs(self.seconds) % SECONDS_PER_MINUTE
+ } else {
+ self.seconds % SECONDS_PER_MINUTE
+ }
+ }
+
+ fn millisecond(&self) -> i64 {
+ (self.attoseconds / ATTOSECONDS_PER_MILLISECOND) as i64
+ }
+
+ fn microsecond(&self) -> i64 {
+ (self.attoseconds / ATTOSECONDS_PER_MICROSECOND % 1000) as i64
+ }
+
+ fn nanosecond(&self) -> i64 {
+ (self.attoseconds / ATTOSECONDS_PER_NANOSECOND % 1000) as i64
+ }
+
+ fn picosecond(&self) -> i64 {
+ (self.attoseconds / ATTOSECONDS_PER_PICOSECOND % 1000) as i64
+ }
+
+ fn femtosecond(&self) -> i64 {
+ (self.attoseconds / ATTOSECONDS_PER_FEMTOSECOND % 1000) as i64
+ }
+
+ fn attosecond(&self) -> i64 {
+ (self.attoseconds % 1000) as i64
+ }
+}
+
+impl Add for RawTime {
+ type Output = Self;
+
+ /// The implementation of [Add] for [RawTime] follows the default Rust rules for integer overflow, which
+ /// should be sufficient for all practical purposes.
+ fn add(self, rhs: TimeDelta) -> Self::Output {
+ let mut attoseconds = self.attoseconds + rhs.attoseconds;
+ let mut seconds = self.seconds + rhs.seconds as i64;
+ if attoseconds >= ATTOSECONDS_PER_SECOND {
+ seconds += 1;
+ attoseconds -= ATTOSECONDS_PER_SECOND;
+ }
+ Self {
+ seconds,
+ attoseconds,
+ }
+ }
+}
+
+impl Sub for RawTime {
+ type Output = Self;
+
+ /// The implementation of [Sub] for [RawTime] follows the default Rust rules for integer overflow, which
+ /// should be sufficient for all practical purposes.
+ fn sub(self, rhs: TimeDelta) -> Self::Output {
+ let mut seconds = self.seconds - rhs.seconds as i64;
+ let mut attoseconds = self.attoseconds;
+ if rhs.attoseconds > self.attoseconds {
+ seconds -= 1;
+ attoseconds = ATTOSECONDS_PER_SECOND - (rhs.attoseconds - self.attoseconds);
+ } else {
+ attoseconds -= rhs.attoseconds;
+ }
+ Self {
+ seconds,
+ attoseconds,
+ }
+ }
+}
+
+/// The continuous time scales supported by Lox.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum TimeScale {
+ TAI,
+ TCB,
+ TCG,
+ TDB,
+ TT,
+ UT1,
+}
+
+impl Display for TimeScale {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", Into::<&str>::into(*self))
+ }
+}
+
+#[allow(clippy::from_over_into)] // Into is infallible, but From is not
+impl Into<&str> for TimeScale {
+ fn into(self) -> &'static str {
+ match self {
+ TimeScale::TAI => "TAI",
+ TimeScale::TCB => "TCB",
+ TimeScale::TCG => "TCG",
+ TimeScale::TDB => "TDB",
+ TimeScale::TT => "TT",
+ TimeScale::UT1 => "UT1",
+ }
+ }
+}
+
+/// CalendarDate allows continuous time formats to report their date in their respective calendar.
+pub trait CalendarDate {
+ fn date(&self) -> Date;
+}
+
+/// International Atomic Time. Defaults to the J2000 epoch.
+#[derive(Debug, Copy, Default, Clone, Eq, PartialEq)]
+pub struct TAI(RawTime);
+
+impl TAI {
+ pub fn to_ut1(&self, _dut: TimeDelta, _dat: TimeDelta) -> UT1 {
+ todo!()
+ }
+}
+
+impl CalendarDate for TAI {
+ fn date(&self) -> Date {
+ todo!()
+ }
+}
+
+/// Barycentric Coordinate Time. Defaults to the J2000 epoch.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
+pub struct TCB(RawTime);
+
+/// Geocentric Coordinate Time. Defaults to the J2000 epoch.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
+pub struct TCG(RawTime);
+
+/// Barycentric Dynamical Time. Defaults to the J2000 epoch.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
+pub struct TDB(RawTime);
+
+/// Terrestrial Time. Defaults to the J2000 epoch.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
+pub struct TT(RawTime);
+
+/// Universal Time. Defaults to the J2000 epoch.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
+pub struct UT1(RawTime);
+
+/// Implements the `WallClock` trait for the a time scale based on [RawTime] in terms of the underlying
+/// raw time.
+macro_rules! wall_clock {
+ ($time_scale:ident, $test_module:ident) => {
+ impl WallClock for $time_scale {
+ fn hour(&self) -> i64 {
+ self.0.hour()
+ }
+
+ fn minute(&self) -> i64 {
+ self.0.minute()
+ }
+
+ fn second(&self) -> i64 {
+ self.0.second()
+ }
+
+ fn millisecond(&self) -> i64 {
+ self.0.millisecond()
+ }
+
+ fn microsecond(&self) -> i64 {
+ self.0.microsecond()
+ }
+
+ fn nanosecond(&self) -> i64 {
+ self.0.nanosecond()
+ }
+
+ fn picosecond(&self) -> i64 {
+ self.0.picosecond()
+ }
+
+ fn femtosecond(&self) -> i64 {
+ self.0.femtosecond()
+ }
+
+ fn attosecond(&self) -> i64 {
+ self.0.attosecond()
+ }
+ }
+
+ #[cfg(test)]
+ mod $test_module {
+ use super::{$time_scale, RawTime};
+ use crate::time::WallClock;
+
+ const RAW_TIME: RawTime = RawTime {
+ seconds: 1234,
+ attoseconds: 5678,
+ };
+
+ const TIME: $time_scale = $time_scale(RAW_TIME);
+
+ #[test]
+ fn test_hour_delegation() {
+ assert_eq!(TIME.hour(), RAW_TIME.hour());
+ }
+
+ #[test]
+ fn test_minute_delegation() {
+ assert_eq!(TIME.minute(), RAW_TIME.minute());
+ }
+
+ #[test]
+ fn test_second_delegation() {
+ assert_eq!(TIME.second(), RAW_TIME.second());
+ }
+
+ #[test]
+ fn test_millisecond_delegation() {
+ assert_eq!(TIME.millisecond(), RAW_TIME.millisecond());
+ }
+
+ #[test]
+ fn test_microsecond_delegation() {
+ assert_eq!(TIME.microsecond(), RAW_TIME.microsecond());
+ }
+
+ #[test]
+ fn test_nanosecond_delegation() {
+ assert_eq!(TIME.nanosecond(), RAW_TIME.nanosecond());
+ }
+
+ #[test]
+ fn test_picosecond_delegation() {
+ assert_eq!(TIME.picosecond(), RAW_TIME.picosecond());
+ }
+
+ #[test]
+ fn test_femtosecond_delegation() {
+ assert_eq!(TIME.femtosecond(), RAW_TIME.femtosecond());
+ }
+
+ #[test]
+ fn test_attosecond_delegation() {
+ assert_eq!(TIME.attosecond(), RAW_TIME.attosecond());
+ }
+ }
+ };
+}
+
+// Implement WallClock for all continuous time scales.
+wall_clock!(TAI, tai_wall_clock_tests);
+wall_clock!(TCB, tcb_wall_clock_tests);
+wall_clock!(TCG, tcg_wall_clock_tests);
+wall_clock!(TDB, tdb_wall_clock_tests);
+wall_clock!(TT, tt_wall_clock_tests);
+wall_clock!(UT1, ut1_wall_clock_tests);
+
+/// `Time` represents a time in any of the supported continuous timescales.
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub enum Time {
+ TAI(TAI),
+ TCB(TCB),
+ TCG(TCG),
+ TDB(TDB),
+ TT(TT),
+ UT1(UT1),
+}
+
+impl Time {
+ /// Instantiates a `Time` of the given scale from a date and UTC timestamp.
+ pub fn from_date_and_utc_timestamp(scale: TimeScale, date: Date, time: UTC) -> Self {
+ let day_in_seconds = date.j2000() * SECONDS_PER_DAY - SECONDS_PER_DAY / 2;
+ let hour_in_seconds = time.hour() * SECONDS_PER_HOUR;
+ let minute_in_seconds = time.minute() * SECONDS_PER_MINUTE;
+ let seconds = day_in_seconds + hour_in_seconds + minute_in_seconds + time.second();
+ let attoseconds = time.subsecond_as_attoseconds();
+ let raw = RawTime {
+ seconds,
+ attoseconds,
+ };
+ Self::from_raw(scale, raw)
+ }
+
+ /// Instantiates a `Time` of the given scale from a UTC datetime.
+ pub fn from_utc_datetime(scale: TimeScale, dt: UTCDateTime) -> Self {
+ Self::from_date_and_utc_timestamp(scale, dt.date(), dt.time())
+ }
+
+ pub fn scale(&self) -> TimeScale {
+ match &self {
+ Time::TAI(_) => TimeScale::TAI,
+ Time::TCB(_) => TimeScale::TCB,
+ Time::TCG(_) => TimeScale::TCG,
+ Time::TDB(_) => TimeScale::TDB,
+ Time::TT(_) => TimeScale::TT,
+ Time::UT1(_) => TimeScale::UT1,
+ }
+ }
+
+ /// Returns the J2000 epoch in the given timescale.
+ pub fn j2000(scale: TimeScale) -> Self {
+ Self::from_raw(scale, RawTime::default())
+ }
+
+ /// Returns, as an epoch in the given timescale, midday on the first day of the proleptic Julian
+ /// calendar.
+ pub fn jd0(scale: TimeScale) -> Self {
+ // This represents 4713 BC, since there is no year 0 separating BC and AD.
+ let first_proleptic_day = Date::new_unchecked(ProlepticJulian, -4712, 1, 1);
+ let midday = UTC::new(12, 0, 0).expect("midday should be a valid time");
+ Self::from_date_and_utc_timestamp(scale, first_proleptic_day, midday)
+ }
+
+ fn from_raw(scale: TimeScale, raw: RawTime) -> Self {
+ match scale {
+ TimeScale::TAI => Time::TAI(TAI(raw)),
+ TimeScale::TCB => Time::TCB(TCB(raw)),
+ TimeScale::TCG => Time::TCG(TCG(raw)),
+ TimeScale::TDB => Time::TDB(TDB(raw)),
+ TimeScale::TT => Time::TT(TT(raw)),
+ TimeScale::UT1 => Time::UT1(UT1(raw)),
+ }
+ }
+
+ fn raw(&self) -> RawTime {
+ match self {
+ Time::TAI(tai) => tai.0,
+ Time::TCB(tcb) => tcb.0,
+ Time::TCG(tcg) => tcg.0,
+ Time::TDB(tdb) => tdb.0,
+ Time::TT(tt) => tt.0,
+ Time::UT1(ut1) => ut1.0,
+ }
+ }
+
+ /// The number of whole seconds since J2000.
+ pub fn seconds(&self) -> i64 {
+ self.raw().seconds
+ }
+
+ /// The number of attoseconds from the last whole second.
+ pub fn attoseconds(&self) -> u64 {
+ self.raw().attoseconds
+ }
+
+ /// The fractional number of Julian days since J2000.
+ pub fn days_since_j2000(&self) -> f64 {
+ let d1 = self.seconds().to_f64().unwrap_or_default() / constants::f64::SECONDS_PER_DAY;
+ let d2 = self.attoseconds().to_f64().unwrap() / constants::f64::ATTOSECONDS_PER_DAY;
+ d2 + d1
+ }
+}
+
+impl Display for Time {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "{:02}:{:02}:{:02}.{:03}.{:03}.{:03}.{:03}.{:03}.{:03} {}",
+ self.hour(),
+ self.minute(),
+ self.second(),
+ self.millisecond(),
+ self.microsecond(),
+ self.nanosecond(),
+ self.picosecond(),
+ self.femtosecond(),
+ self.attosecond(),
+ self.scale(),
+ )
+ }
+}
+
+impl WallClock for Time {
+ fn hour(&self) -> i64 {
+ match self {
+ Time::TAI(t) => t.hour(),
+ Time::TCB(t) => t.hour(),
+ Time::TCG(t) => t.hour(),
+ Time::TDB(t) => t.hour(),
+ Time::TT(t) => t.hour(),
+ Time::UT1(t) => t.hour(),
+ }
+ }
+
+ fn minute(&self) -> i64 {
+ match self {
+ Time::TAI(t) => t.minute(),
+ Time::TCB(t) => t.minute(),
+ Time::TCG(t) => t.minute(),
+ Time::TDB(t) => t.minute(),
+ Time::TT(t) => t.minute(),
+ Time::UT1(t) => t.minute(),
+ }
+ }
+
+ fn second(&self) -> i64 {
+ match self {
+ Time::TAI(t) => t.second(),
+ Time::TCB(t) => t.second(),
+ Time::TCG(t) => t.second(),
+ Time::TDB(t) => t.second(),
+ Time::TT(t) => t.second(),
+ Time::UT1(t) => t.second(),
+ }
+ }
+
+ fn millisecond(&self) -> i64 {
+ match self {
+ Time::TAI(t) => t.millisecond(),
+ Time::TCB(t) => t.millisecond(),
+ Time::TCG(t) => t.millisecond(),
+ Time::TDB(t) => t.millisecond(),
+ Time::TT(t) => t.millisecond(),
+ Time::UT1(t) => t.millisecond(),
+ }
+ }
+
+ fn microsecond(&self) -> i64 {
+ match self {
+ Time::TAI(t) => t.microsecond(),
+ Time::TCB(t) => t.microsecond(),
+ Time::TCG(t) => t.microsecond(),
+ Time::TDB(t) => t.microsecond(),
+ Time::TT(t) => t.microsecond(),
+ Time::UT1(t) => t.microsecond(),
+ }
+ }
+
+ fn nanosecond(&self) -> i64 {
+ match self {
+ Time::TAI(t) => t.nanosecond(),
+ Time::TCB(t) => t.nanosecond(),
+ Time::TCG(t) => t.nanosecond(),
+ Time::TDB(t) => t.nanosecond(),
+ Time::TT(t) => t.nanosecond(),
+ Time::UT1(t) => t.nanosecond(),
+ }
+ }
+
+ fn picosecond(&self) -> i64 {
+ match self {
+ Time::TAI(t) => t.picosecond(),
+ Time::TCB(t) => t.picosecond(),
+ Time::TCG(t) => t.picosecond(),
+ Time::TDB(t) => t.picosecond(),
+ Time::TT(t) => t.picosecond(),
+ Time::UT1(t) => t.picosecond(),
+ }
+ }
+
+ fn femtosecond(&self) -> i64 {
+ match self {
+ Time::TAI(t) => t.femtosecond(),
+ Time::TCB(t) => t.femtosecond(),
+ Time::TCG(t) => t.femtosecond(),
+ Time::TDB(t) => t.femtosecond(),
+ Time::TT(t) => t.femtosecond(),
+ Time::UT1(t) => t.femtosecond(),
+ }
+ }
+
+ fn attosecond(&self) -> i64 {
+ match self {
+ Time::TAI(t) => t.attosecond(),
+ Time::TCB(t) => t.attosecond(),
+ Time::TCG(t) => t.attosecond(),
+ Time::TDB(t) => t.attosecond(),
+ Time::TT(t) => t.attosecond(),
+ Time::UT1(t) => t.attosecond(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::time::dates::Calendar::Gregorian;
+
+ const TIME_SCALES: [TimeScale; 6] = [
+ TimeScale::TAI,
+ TimeScale::TCB,
+ TimeScale::TCG,
+ TimeScale::TDB,
+ TimeScale::TT,
+ TimeScale::UT1,
+ ];
+
+ #[test]
+ fn test_raw_time_is_negative() {
+ assert!(RawTime {
+ seconds: -1,
+ attoseconds: 0
+ }
+ .is_negative());
+ assert!(!RawTime {
+ seconds: 0,
+ attoseconds: 0
+ }
+ .is_negative());
+ assert!(!RawTime {
+ seconds: 1,
+ attoseconds: 0
+ }
+ .is_negative());
+ }
+
+ #[test]
+ fn test_raw_time_hour() {
+ struct TestCase {
+ desc: &'static str,
+ time: RawTime,
+ expected_hour: i64,
+ }
+
+ let test_cases = [
+ TestCase {
+ desc: "zero value",
+ time: RawTime {
+ seconds: 0,
+ attoseconds: 0,
+ },
+ expected_hour: 12,
+ },
+ TestCase {
+ desc: "one attosecond less than an hour",
+ time: RawTime {
+ seconds: SECONDS_PER_HOUR - 1,
+ attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ },
+ expected_hour: 12,
+ },
+ TestCase {
+ desc: "exactly one hour",
+ time: RawTime {
+ seconds: SECONDS_PER_HOUR,
+ attoseconds: 0,
+ },
+ expected_hour: 13,
+ },
+ TestCase {
+ desc: "one day and one hour",
+ time: RawTime {
+ seconds: SECONDS_PER_HOUR * 25,
+ attoseconds: 0,
+ },
+ expected_hour: 13,
+ },
+ TestCase {
+ desc: "one attosecond less than the epoch",
+ time: RawTime {
+ seconds: -1,
+ attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ },
+ expected_hour: 11,
+ },
+ TestCase {
+ desc: "one hour less than the epoch",
+ time: RawTime {
+ seconds: -SECONDS_PER_HOUR,
+ attoseconds: 0,
+ },
+ expected_hour: 11,
+ },
+ TestCase {
+ desc: "one hour and one attosecond less than the epoch",
+ time: RawTime {
+ seconds: -SECONDS_PER_HOUR - 1,
+ attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ },
+ expected_hour: 10,
+ },
+ TestCase {
+ desc: "one day less than the epoch",
+ time: RawTime {
+ seconds: -SECONDS_PER_DAY,
+ attoseconds: 0,
+ },
+ expected_hour: 12,
+ },
+ TestCase {
+ // Exercises the case where the number of seconds exceeds the number of seconds in a day.
+ desc: "two days less than the epoch",
+ time: RawTime {
+ seconds: -SECONDS_PER_DAY * 2,
+ attoseconds: 0,
+ },
+ expected_hour: 12,
+ },
+ ];
+
+ for tc in test_cases {
+ let actual = tc.time.hour();
+ assert_eq!(
+ actual, tc.expected_hour,
+ "{}: expected {}, got {}",
+ tc.desc, tc.expected_hour, actual
+ );
+ }
+ }
+
+ #[test]
+ fn test_raw_time_minute() {
+ struct TestCase {
+ desc: &'static str,
+ time: RawTime,
+ expected_minute: i64,
+ }
+
+ let test_cases = [
+ TestCase {
+ desc: "zero value",
+ time: RawTime {
+ seconds: 0,
+ attoseconds: 0,
+ },
+ expected_minute: 0,
+ },
+ TestCase {
+ desc: "one attosecond less than one minute",
+ time: RawTime {
+ seconds: SECONDS_PER_MINUTE - 1,
+ attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ },
+ expected_minute: 0,
+ },
+ TestCase {
+ desc: "one minute",
+ time: RawTime {
+ seconds: SECONDS_PER_MINUTE,
+ attoseconds: 0,
+ },
+ expected_minute: 1,
+ },
+ TestCase {
+ desc: "one attosecond less than an hour",
+ time: RawTime {
+ seconds: SECONDS_PER_HOUR - 1,
+ attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ },
+ expected_minute: 59,
+ },
+ TestCase {
+ desc: "exactly one hour",
+ time: RawTime {
+ seconds: SECONDS_PER_HOUR,
+ attoseconds: 0,
+ },
+ expected_minute: 0,
+ },
+ TestCase {
+ desc: "one hour and one minute",
+ time: RawTime {
+ seconds: SECONDS_PER_HOUR + SECONDS_PER_MINUTE,
+ attoseconds: 0,
+ },
+ expected_minute: 1,
+ },
+ TestCase {
+ desc: "one attosecond less than the epoch",
+ time: RawTime {
+ seconds: -1,
+ attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ },
+ expected_minute: 59,
+ },
+ TestCase {
+ desc: "one minute less than the epoch",
+ time: RawTime {
+ seconds: -SECONDS_PER_MINUTE,
+ attoseconds: 0,
+ },
+ expected_minute: 59,
+ },
+ TestCase {
+ desc: "one minute and one attosecond less than the epoch",
+ time: RawTime {
+ seconds: -SECONDS_PER_MINUTE - 1,
+ attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ },
+ expected_minute: 58,
+ },
+ ];
+
+ for tc in test_cases {
+ let actual = tc.time.minute();
+ assert_eq!(
+ actual, tc.expected_minute,
+ "{}: expected {}, got {}",
+ tc.desc, tc.expected_minute, actual
+ );
+ }
+ }
+
+ #[test]
+ fn test_raw_time_second() {
+ struct TestCase {
+ desc: &'static str,
+ time: RawTime,
+ expected_second: i64,
+ }
+
+ let test_cases = [
+ TestCase {
+ desc: "zero value",
+ time: RawTime {
+ seconds: 0,
+ attoseconds: 0,
+ },
+ expected_second: 0,
+ },
+ TestCase {
+ desc: "one attosecond less than one second",
+ time: RawTime {
+ seconds: 0,
+ attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ },
+ expected_second: 0,
+ },
+ TestCase {
+ desc: "one second",
+ time: RawTime {
+ seconds: 1,
+ attoseconds: 0,
+ },
+ expected_second: 1,
+ },
+ TestCase {
+ desc: "one attosecond less than a minute",
+ time: RawTime {
+ seconds: SECONDS_PER_MINUTE - 1,
+ attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ },
+ expected_second: 59,
+ },
+ TestCase {
+ desc: "exactly one minute",
+ time: RawTime {
+ seconds: SECONDS_PER_MINUTE,
+ attoseconds: 0,
+ },
+ expected_second: 0,
+ },
+ TestCase {
+ desc: "one minute and one second",
+ time: RawTime {
+ seconds: SECONDS_PER_MINUTE + 1,
+ attoseconds: 0,
+ },
+ expected_second: 1,
+ },
+ TestCase {
+ desc: "one attosecond less than the epoch",
+ time: RawTime {
+ seconds: -1,
+ attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ },
+ expected_second: 59,
+ },
+ TestCase {
+ desc: "one second less than the epoch",
+ time: RawTime {
+ seconds: -1,
+ attoseconds: 0,
+ },
+ expected_second: 59,
+ },
+ TestCase {
+ desc: "one second and one attosecond less than the epoch",
+ time: RawTime {
+ seconds: -2,
+ attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ },
+ expected_second: 58,
+ },
+ ];
+
+ for tc in test_cases {
+ let actual = tc.time.second();
+ assert_eq!(
+ actual, tc.expected_second,
+ "{}: expected {}, got {}",
+ tc.desc, tc.expected_second, actual
+ );
+ }
+ }
+
+ #[test]
+ fn test_raw_time_subseconds_with_positive_seconds() {
+ let time = RawTime {
+ seconds: 0,
+ attoseconds: 123_456_789_012_345_678,
+ };
+
+ struct TestCase {
+ unit: &'static str,
+ expected: i64,
+ actual: i64,
+ }
+
+ let test_cases = [
+ TestCase {
+ unit: "millisecond",
+ expected: 123,
+ actual: time.millisecond(),
+ },
+ TestCase {
+ unit: "microsecond",
+ expected: 456,
+ actual: time.microsecond(),
+ },
+ TestCase {
+ unit: "nanosecond",
+ expected: 789,
+ actual: time.nanosecond(),
+ },
+ TestCase {
+ unit: "picosecond",
+ expected: 12,
+ actual: time.picosecond(),
+ },
+ TestCase {
+ unit: "femtosecond",
+ expected: 345,
+ actual: time.femtosecond(),
+ },
+ TestCase {
+ unit: "attosecond",
+ expected: 678,
+ actual: time.attosecond(),
+ },
+ ];
+
+ for tc in test_cases {
+ assert_eq!(
+ tc.actual, tc.expected,
+ "expected {} {}, got {}",
+ tc.unit, tc.expected, tc.actual
+ );
+ }
+ }
+
+ #[test]
+ fn test_raw_time_subseconds_with_negative_seconds() {
+ let time = RawTime {
+ seconds: -1,
+ attoseconds: 123_456_789_012_345_678,
+ };
+
+ struct TestCase {
+ unit: &'static str,
+ expected: i64,
+ actual: i64,
+ }
+
+ let test_cases = [
+ TestCase {
+ unit: "millisecond",
+ expected: 123,
+ actual: time.millisecond(),
+ },
+ TestCase {
+ unit: "microsecond",
+ expected: 456,
+ actual: time.microsecond(),
+ },
+ TestCase {
+ unit: "nanosecond",
+ expected: 789,
+ actual: time.nanosecond(),
+ },
+ TestCase {
+ unit: "picosecond",
+ expected: 12,
+ actual: time.picosecond(),
+ },
+ TestCase {
+ unit: "femtosecond",
+ expected: 345,
+ actual: time.femtosecond(),
+ },
+ TestCase {
+ unit: "attosecond",
+ expected: 678,
+ actual: time.attosecond(),
+ },
+ ];
+
+ for tc in test_cases {
+ assert_eq!(
+ tc.actual, tc.expected,
+ "expected {} {}, got {}",
+ tc.unit, tc.expected, tc.actual
+ );
+ }
+ }
+
+ #[test]
+ fn test_raw_time_add_time_delta() {
+ struct TestCase {
+ desc: &'static str,
+ delta: TimeDelta,
+ time: RawTime,
+ expected: RawTime,
+ }
+
+ let test_cases = [
+ TestCase {
+ desc: "positive time with no attosecond wrap",
+ delta: TimeDelta {
+ seconds: 1,
+ attoseconds: 1,
+ },
+ time: RawTime {
+ seconds: 1,
+ attoseconds: 0,
+ },
+ expected: RawTime {
+ seconds: 2,
+ attoseconds: 1,
+ },
+ },
+ TestCase {
+ desc: "positive time with attosecond wrap",
+ delta: TimeDelta {
+ seconds: 1,
+ attoseconds: 2,
+ },
+ time: RawTime {
+ seconds: 1,
+ attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ },
+ expected: RawTime {
+ seconds: 3,
+ attoseconds: 1,
+ },
+ },
+ TestCase {
+ desc: "negative time with no attosecond wrap",
+ delta: TimeDelta {
+ seconds: 1,
+ attoseconds: 1,
+ },
+ time: RawTime {
+ seconds: -1,
+ attoseconds: 0,
+ },
+ expected: RawTime {
+ seconds: 0,
+ attoseconds: 1,
+ },
+ },
+ TestCase {
+ desc: "negative time with attosecond wrap",
+ delta: TimeDelta {
+ seconds: 1,
+ attoseconds: 2,
+ },
+ time: RawTime {
+ seconds: -1,
+ attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ },
+ expected: RawTime {
+ seconds: 1,
+ attoseconds: 1,
+ },
+ },
+ ];
+
+ for tc in test_cases {
+ let actual = tc.time + tc.delta;
+ assert_eq!(
+ actual, tc.expected,
+ "{}: expected {:?}, got {:?}",
+ tc.desc, tc.expected, actual
+ );
+ }
+ }
+
+ #[test]
+ fn test_raw_time_sub_time_delta() {
+ struct TestCase {
+ desc: &'static str,
+ delta: TimeDelta,
+ time: RawTime,
+ expected: RawTime,
+ }
+
+ let test_cases = [
+ TestCase {
+ desc: "positive time with no attosecond wrap",
+ delta: TimeDelta {
+ seconds: 1,
+ attoseconds: 1,
+ },
+ time: RawTime {
+ seconds: 2,
+ attoseconds: 2,
+ },
+ expected: RawTime {
+ seconds: 1,
+ attoseconds: 1,
+ },
+ },
+ TestCase {
+ desc: "positive time with attosecond wrap",
+ delta: TimeDelta {
+ seconds: 1,
+ attoseconds: 2,
+ },
+ time: RawTime {
+ seconds: 2,
+ attoseconds: 1,
+ },
+ expected: RawTime {
+ seconds: 0,
+ attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ },
+ },
+ TestCase {
+ desc: "negative time with no attosecond wrap",
+ delta: TimeDelta {
+ seconds: 1,
+ attoseconds: 1,
+ },
+ time: RawTime {
+ seconds: -1,
+ attoseconds: 2,
+ },
+ expected: RawTime {
+ seconds: -2,
+ attoseconds: 1,
+ },
+ },
+ TestCase {
+ desc: "negative time with attosecond wrap",
+ delta: TimeDelta {
+ seconds: 1,
+ attoseconds: 2,
+ },
+ time: RawTime {
+ seconds: -1,
+ attoseconds: 1,
+ },
+ expected: RawTime {
+ seconds: -3,
+ attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ },
+ },
+ TestCase {
+ desc: "transition from positive to negative time",
+ delta: TimeDelta {
+ seconds: 1,
+ attoseconds: 2,
+ },
+ time: RawTime {
+ seconds: 0,
+ attoseconds: 1,
+ },
+ expected: RawTime {
+ seconds: -2,
+ attoseconds: ATTOSECONDS_PER_SECOND - 1,
+ },
+ },
+ ];
+
+ for tc in test_cases {
+ let actual = tc.time - tc.delta;
+ assert_eq!(
+ actual, tc.expected,
+ "{}: expected {:?}, got {:?}",
+ tc.desc, tc.expected, actual
+ );
+ }
+ }
+
+ #[test]
+ fn test_timescale_into_str() {
+ let test_cases = [
+ (TimeScale::TAI, "TAI"),
+ (TimeScale::TCB, "TCB"),
+ (TimeScale::TCG, "TCG"),
+ (TimeScale::TDB, "TDB"),
+ (TimeScale::TT, "TT"),
+ (TimeScale::UT1, "UT1"),
+ ];
+
+ for (scale, expected) in test_cases {
+ assert_eq!(Into::<&str>::into(scale), expected);
+ }
+ }
+
+ #[test]
+ fn test_time_from_date_and_utc_timestamp() {
+ let date = Date::new_unchecked(Gregorian, 2021, 1, 1);
+ let utc = UTC::new(12, 34, 56).expect("time should be valid");
+ let datetime = UTCDateTime::new(date, utc);
+
+ for scale in TIME_SCALES {
+ let actual = Time::from_date_and_utc_timestamp(scale, date, utc);
+ let expected = Time::from_utc_datetime(scale, datetime);
+ assert_eq!(actual, expected);
+ }
+ }
+
+ #[test]
+ fn test_time_display() {
+ let time = Time::TAI(TAI::default());
+ let expected = "12:00:00.000.000.000.000.000.000 TAI".to_string();
+ let actual = time.to_string();
+ assert_eq!(actual, expected);
+ }
+
+ #[test]
+ fn test_time_j2000() {
+ [
+ (TimeScale::TAI, Time::TAI(TAI::default())),
+ (TimeScale::TCB, Time::TCB(TCB::default())),
+ (TimeScale::TCG, Time::TCG(TCG::default())),
+ (TimeScale::TDB, Time::TDB(TDB::default())),
+ (TimeScale::TT, Time::TT(TT::default())),
+ (TimeScale::UT1, Time::UT1(UT1::default())),
+ ]
+ .iter()
+ .for_each(|(scale, expected)| {
+ let actual = Time::j2000(*scale);
+ assert_eq!(*expected, actual);
+ });
+ }
+
+ #[test]
+ fn test_time_jd0() {
+ [
+ (
+ TimeScale::TAI,
+ Time::TAI(TAI(RawTime {
+ seconds: -211813488000,
+ attoseconds: 0,
+ })),
+ ),
+ (
+ TimeScale::TCB,
+ Time::TCB(TCB(RawTime {
+ seconds: -211813488000,
+ attoseconds: 0,
+ })),
+ ),
+ (
+ TimeScale::TCG,
+ Time::TCG(TCG(RawTime {
+ seconds: -211813488000,
+ attoseconds: 0,
+ })),
+ ),
+ (
+ TimeScale::TDB,
+ Time::TDB(TDB(RawTime {
+ seconds: -211813488000,
+ attoseconds: 0,
+ })),
+ ),
+ (
+ TimeScale::TT,
+ Time::TT(TT(RawTime {
+ seconds: -211813488000,
+ attoseconds: 0,
+ })),
+ ),
+ (
+ TimeScale::UT1,
+ Time::UT1(UT1(RawTime {
+ seconds: -211813488000,
+ attoseconds: 0,
+ })),
+ ),
+ ]
+ .iter()
+ .for_each(|(scale, expected)| {
+ let actual = Time::jd0(*scale);
+ assert_eq!(*expected, actual);
+ });
+ }
+
+ #[test]
+ fn test_time_scale() {
+ let test_cases = [
+ (Time::TAI(TAI::default()), TimeScale::TAI),
+ (Time::TCB(TCB::default()), TimeScale::TCB),
+ (Time::TCG(TCG::default()), TimeScale::TCG),
+ (Time::TDB(TDB::default()), TimeScale::TDB),
+ (Time::TT(TT::default()), TimeScale::TT),
+ (Time::UT1(UT1::default()), TimeScale::UT1),
+ ];
+
+ for (time, expected) in test_cases {
+ assert_eq!(time.scale(), expected);
+ }
+ }
+
+ #[test]
+ fn test_time_wall_clock_hour() {
+ let raw_time = RawTime::default();
+ let expected = raw_time.hour();
+ for scale in TIME_SCALES {
+ let time = Time::from_raw(scale, raw_time);
+ let actual = time.hour();
+ assert_eq!(
+ actual, expected,
+ "expected time in scale {} to have hour {}, but got {}",
+ scale, expected, actual
+ );
+ }
+ }
+
+ #[test]
+ fn test_time_wall_clock_minute() {
+ let raw_time = RawTime::default();
+ let expected = raw_time.minute();
+ for scale in TIME_SCALES {
+ let time = Time::from_raw(scale, raw_time);
+ let actual = time.minute();
+ assert_eq!(
+ actual, expected,
+ "expected time in scale {} to have minute {}, but got {}",
+ scale, expected, actual
+ );
+ }
+ }
+
+ #[test]
+ fn test_time_wall_clock_second() {
+ let raw_time = RawTime::default();
+ let expected = raw_time.second();
+ for scale in TIME_SCALES {
+ let time = Time::from_raw(scale, raw_time);
+ let actual = time.second();
+ assert_eq!(
+ actual, expected,
+ "expected time in scale {} to have second {}, but got {}",
+ scale, expected, actual
+ );
+ }
+ }
+
+ #[test]
+ fn test_time_wall_clock_millisecond() {
+ let raw_time = RawTime::default();
+ let expected = raw_time.millisecond();
+ for scale in TIME_SCALES {
+ let time = Time::from_raw(scale, raw_time);
+ let actual = time.millisecond();
+ assert_eq!(
+ actual, expected,
+ "expected time in scale {} to have millisecond {}, but got {}",
+ scale, expected, actual
+ );
+ }
+ }
+
+ #[test]
+ fn test_time_wall_clock_microsecond() {
+ let raw_time = RawTime::default();
+ let expected = raw_time.microsecond();
+ for scale in TIME_SCALES {
+ let time = Time::from_raw(scale, raw_time);
+ let actual = time.microsecond();
+ assert_eq!(
+ actual, expected,
+ "expected time in scale {} to have microsecond {}, but got {}",
+ scale, expected, actual
+ );
+ }
+ }
+
+ #[test]
+ fn test_time_wall_clock_nanosecond() {
+ let raw_time = RawTime::default();
+ let expected = raw_time.nanosecond();
+ for scale in TIME_SCALES {
+ let time = Time::from_raw(scale, raw_time);
+ let actual = time.nanosecond();
+ assert_eq!(
+ actual, expected,
+ "expected time in scale {} to have nanosecond {}, but got {}",
+ scale, expected, actual
+ );
+ }
+ }
+
+ #[test]
+ fn test_time_wall_clock_picosecond() {
+ let raw_time = RawTime::default();
+ let expected = raw_time.picosecond();
+ for scale in TIME_SCALES {
+ let time = Time::from_raw(scale, raw_time);
+ let actual = time.picosecond();
+ assert_eq!(
+ actual, expected,
+ "expected time in scale {} to have picosecond {}, but got {}",
+ scale, expected, actual
+ );
+ }
+ }
+
+ #[test]
+ fn test_time_wall_clock_femtosecond() {
+ let raw_time = RawTime::default();
+ let expected = raw_time.femtosecond();
+ for scale in TIME_SCALES {
+ let time = Time::from_raw(scale, raw_time);
+ let actual = time.femtosecond();
+ assert_eq!(
+ actual, expected,
+ "expected time in scale {} to have femtosecond {}, but got {}",
+ scale, expected, actual
+ );
+ }
+ }
+
+ #[test]
+ fn test_time_wall_clock_attosecond() {
+ let raw_time = RawTime::default();
+ let expected = raw_time.attosecond();
+ for scale in TIME_SCALES {
+ let time = Time::from_raw(scale, raw_time);
+ let actual = time.attosecond();
+ assert_eq!(
+ actual, expected,
+ "expected time in scale {} to have attosecond {}, but got {}",
+ scale, expected, actual
+ );
+ }
+ }
+}
diff --git a/crates/lox_core/src/time/dates.rs b/crates/lox_core/src/time/dates.rs
index 9f19f79a..7562c70c 100644
--- a/crates/lox_core/src/time/dates.rs
+++ b/crates/lox_core/src/time/dates.rs
@@ -7,7 +7,6 @@
*/
use crate::errors::LoxError;
-use num::ToPrimitive;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Calendar {
@@ -16,7 +15,7 @@ pub enum Calendar {
Gregorian,
}
-#[derive(Debug, Copy, Clone)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Date {
calendar: Calendar,
year: i64,
@@ -105,125 +104,6 @@ impl Date {
}
}
-#[derive(Debug, Copy, Clone, Default)]
-pub struct Time {
- hour: i64,
- minute: i64,
- second: i64,
- milli: i64,
- micro: i64,
- nano: i64,
- pico: i64,
- femto: i64,
- atto: i64,
-}
-
-impl Time {
- pub fn new(hour: i64, minute: i64, second: i64) -> Result {
- if !(0..24).contains(&hour) || !(0..60).contains(&minute) || !(0..61).contains(&second) {
- Err(LoxError::InvalidTime(hour, minute, second))
- } else {
- Ok(Self {
- hour,
- minute,
- second,
- ..Default::default()
- })
- }
- }
-
- pub fn milli(mut self, milli: i64) -> Self {
- self.milli = milli;
- self
- }
-
- pub fn micro(mut self, micro: i64) -> Self {
- self.micro = micro;
- self
- }
-
- pub fn nano(mut self, nano: i64) -> Self {
- self.nano = nano;
- self
- }
-
- pub fn pico(mut self, pico: i64) -> Self {
- self.pico = pico;
- self
- }
-
- pub fn femto(mut self, femto: i64) -> Self {
- self.femto = femto;
- self
- }
-
- pub fn atto(mut self, atto: i64) -> Self {
- self.atto = atto;
- self
- }
-
- pub fn from_seconds(hour: i64, minute: i64, seconds: f64) -> Result {
- if !(0.0..61.0).contains(&seconds) {
- return Err(LoxError::InvalidSeconds(hour, minute, seconds));
- }
- let sub = split_seconds(seconds.fract()).unwrap();
- let second = seconds.round().to_i64().unwrap();
- Self::new(hour, minute, second)?;
- Ok(Self {
- hour,
- minute,
- second,
- milli: sub[0],
- micro: sub[1],
- nano: sub[2],
- pico: sub[3],
- femto: sub[4],
- atto: sub[5],
- })
- }
-
- pub fn hour(&self) -> i64 {
- self.hour
- }
-
- pub fn minute(&self) -> i64 {
- self.minute
- }
-
- pub fn second(&self) -> i64 {
- self.second
- }
-
- pub fn attosecond(&self) -> i64 {
- self.milli * i64::pow(10, 15)
- + self.micro * i64::pow(10, 12)
- + self.nano * i64::pow(10, 9)
- + self.pico * i64::pow(10, 6)
- + self.femto * i64::pow(10, 3)
- + self.atto
- }
-}
-
-#[derive(Debug, Copy, Clone)]
-pub struct DateTime {
- date: Date,
- time: Time,
-}
-
-impl DateTime {
- pub fn new(date: Date, time: Time) -> Self {
- Self { date, time }
- }
-
- pub fn date(&self) -> Date {
- self.date
- }
-
- pub fn time(&self) -> Time {
- self.time
- }
-}
-
fn find_year(calendar: Calendar, j2000day: i64) -> i64 {
match calendar {
Calendar::ProlepticJulian => -((-4 * j2000day - 2920488) / 1461),
@@ -310,91 +190,9 @@ fn j2000(calendar: Calendar, year: i64, month: i64, day: i64) -> i64 {
d1 + d2
}
-fn split_seconds(seconds: f64) -> Option<[i64; 6]> {
- if !(0.0..1.0).contains(&seconds) {
- return None;
- }
- let mut atto = (seconds * 1e18).to_i64()?;
- let mut parts: [i64; 6] = [0; 6];
- for (i, exponent) in (3..18).step_by(3).rev().enumerate() {
- let factor = i64::pow(10, exponent);
- parts[i] = atto / factor;
- atto -= parts[i] * factor;
- }
- parts[5] = atto / 10 * 10;
- Some(parts)
-}
-
#[cfg(test)]
mod tests {
- use super::*;
- use proptest::prelude::*;
-
- proptest! {
- #[test]
- fn prop_test_split_seconds(s in 0.0..1.0) {
- prop_assert!(split_seconds(s).is_some())
- }
- }
-
- #[test]
- fn test_sub_second() {
- let s1 = split_seconds(0.123).expect("seconds should be valid");
- assert_eq!(123, s1[0]);
- assert_eq!(0, s1[1]);
- assert_eq!(0, s1[2]);
- assert_eq!(0, s1[3]);
- assert_eq!(0, s1[4]);
- assert_eq!(0, s1[5]);
- let s2 = split_seconds(0.123_456).expect("seconds should be valid");
- assert_eq!(123, s2[0]);
- assert_eq!(456, s2[1]);
- assert_eq!(0, s2[2]);
- assert_eq!(0, s2[3]);
- assert_eq!(0, s2[4]);
- assert_eq!(0, s2[5]);
- let s3 = split_seconds(0.123_456_789).expect("seconds should be valid");
- assert_eq!(123, s3[0]);
- assert_eq!(456, s3[1]);
- assert_eq!(789, s3[2]);
- assert_eq!(0, s3[3]);
- assert_eq!(0, s3[4]);
- assert_eq!(0, s3[5]);
- let s4 = split_seconds(0.123_456_789_123).expect("seconds should be valid");
- assert_eq!(123, s4[0]);
- assert_eq!(456, s4[1]);
- assert_eq!(789, s4[2]);
- assert_eq!(123, s4[3]);
- assert_eq!(0, s4[4]);
- assert_eq!(0, s4[5]);
- let s5 = split_seconds(0.123_456_789_123_456).expect("seconds should be valid");
- assert_eq!(123, s5[0]);
- assert_eq!(456, s5[1]);
- assert_eq!(789, s5[2]);
- assert_eq!(123, s5[3]);
- assert_eq!(456, s5[4]);
- assert_eq!(0, s5[5]);
- let s6 = split_seconds(0.123_456_789_123_456_78).expect("seconds should be valid");
- assert_eq!(123, s6[0]);
- assert_eq!(456, s6[1]);
- assert_eq!(789, s6[2]);
- assert_eq!(123, s6[3]);
- assert_eq!(456, s6[4]);
- assert_eq!(780, s6[5]);
- let s7 = split_seconds(0.000_000_000_000_000_01).expect("seconds should be valid");
- assert_eq!(0, s7[0]);
- assert_eq!(0, s7[1]);
- assert_eq!(0, s7[2]);
- assert_eq!(0, s7[3]);
- assert_eq!(0, s7[4]);
- assert_eq!(10, s7[5]);
- }
-
- #[test]
- fn test_illegal_split_second() {
- assert!(split_seconds(2.0).is_none());
- assert!(split_seconds(-0.2).is_none());
- }
+ use crate::time::dates::{Calendar, Date};
#[test]
fn test_date_new_unchecked() {
diff --git a/crates/lox_core/src/time/epochs.rs b/crates/lox_core/src/time/epochs.rs
deleted file mode 100644
index 0ec338a8..00000000
--- a/crates/lox_core/src/time/epochs.rs
+++ /dev/null
@@ -1,250 +0,0 @@
-/*
- * 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/.
- */
-
-use num::ToPrimitive;
-use std::fmt;
-use std::fmt::Formatter;
-
-use crate::time::constants;
-use crate::time::constants::i64::{SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE};
-
-use crate::time::dates::Calendar::ProlepticJulian;
-use crate::time::dates::{Date, DateTime, Time};
-
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub enum TimeScale {
- TAI,
- TCB,
- TCG,
- TDB,
- TT,
- UT1,
-}
-
-impl fmt::Display for TimeScale {
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- match self {
- TimeScale::TAI => write!(f, "TAI"),
- TimeScale::TCB => write!(f, "TCB"),
- TimeScale::TCG => write!(f, "TCG"),
- TimeScale::TDB => write!(f, "TDB"),
- TimeScale::TT => write!(f, "TT"),
- TimeScale::UT1 => write!(f, "UT1"),
- }
- }
-}
-
-#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
-pub struct RawEpoch {
- second: i64,
- attosecond: i64,
-}
-
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub enum Epoch {
- TAI(RawEpoch),
- TCB(RawEpoch),
- TCG(RawEpoch),
- TDB(RawEpoch),
- TT(RawEpoch),
- UT1(RawEpoch),
-}
-
-impl Epoch {
- pub fn from_date_and_time(scale: TimeScale, date: Date, time: Time) -> Self {
- let day_in_seconds = date.j2000() * SECONDS_PER_DAY - SECONDS_PER_DAY / 2;
- let hour_in_seconds = time.hour() * SECONDS_PER_HOUR;
- let minute_in_seconds = time.minute() * SECONDS_PER_MINUTE;
- let second = day_in_seconds + hour_in_seconds + minute_in_seconds + time.second();
- let attosecond = time.attosecond();
- let raw = RawEpoch { second, attosecond };
- Self::from_raw(scale, raw)
- }
-
- pub fn from_datetime(scale: TimeScale, dt: DateTime) -> Self {
- Self::from_date_and_time(scale, dt.date(), dt.time())
- }
-
- /// Returns the J2000 epoch in the given timescale.
- pub fn j2000(scale: TimeScale) -> Self {
- Self::from_raw(scale, RawEpoch::default())
- }
-
- /// Returns, as an epoch in the given timescale, midday on the first day of the proleptic Julian
- /// calendar.
- pub fn jd0(scale: TimeScale) -> Self {
- // This represents 4713 BC, since there is no year 0 separating BC and AD.
- let first_proleptic_day = Date::new_unchecked(ProlepticJulian, -4712, 1, 1);
- let midday = Time::new(12, 0, 0).expect("midday should be a valid time");
- Self::from_date_and_time(scale, first_proleptic_day, midday)
- }
-
- fn from_raw(scale: TimeScale, raw: RawEpoch) -> Self {
- match scale {
- TimeScale::TAI => Epoch::TAI(raw),
- TimeScale::TCB => Epoch::TCB(raw),
- TimeScale::TCG => Epoch::TCG(raw),
- TimeScale::TDB => Epoch::TDB(raw),
- TimeScale::TT => Epoch::TT(raw),
- TimeScale::UT1 => Epoch::UT1(raw),
- }
- }
-
- fn raw(&self) -> &RawEpoch {
- match self {
- Epoch::TAI(raw)
- | Epoch::TCB(raw)
- | Epoch::TCG(raw)
- | Epoch::TDB(raw)
- | Epoch::TT(raw)
- | Epoch::UT1(raw) => raw,
- }
- }
-
- pub fn second(&self) -> i64 {
- self.raw().second
- }
-
- pub fn attosecond(&self) -> i64 {
- self.raw().attosecond
- }
-
- pub fn days_since_j2000(&self) -> f64 {
- let d1 = self.second().to_f64().unwrap_or_default() / constants::f64::SECONDS_PER_DAY;
- let d2 = self.attosecond().to_f64().unwrap() / constants::f64::ATTOSECONDS_PER_DAY;
- d2 + d1
- }
-
- pub fn scale(&self) -> &'static str {
- match self {
- Epoch::TAI(_) => "TAI",
- Epoch::TCB(_) => "TCB",
- Epoch::TCG(_) => "TCG",
- Epoch::TDB(_) => "TDB",
- Epoch::TT(_) => "TT",
- Epoch::UT1(_) => "UT1",
- }
- }
-}
-
-impl fmt::Display for Epoch {
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- write!(f, "foo")
- }
-}
-
-#[cfg(test)]
-mod tests {
- use crate::time::epochs::{Epoch, RawEpoch, TimeScale};
-
- #[test]
- fn test_epoch_j2000() {
- [
- (
- TimeScale::TAI,
- Epoch::TAI(RawEpoch {
- second: 0,
- attosecond: 0,
- }),
- ),
- (
- TimeScale::TCB,
- Epoch::TCB(RawEpoch {
- second: 0,
- attosecond: 0,
- }),
- ),
- (
- TimeScale::TCG,
- Epoch::TCG(RawEpoch {
- second: 0,
- attosecond: 0,
- }),
- ),
- (
- TimeScale::TDB,
- Epoch::TDB(RawEpoch {
- second: 0,
- attosecond: 0,
- }),
- ),
- (
- TimeScale::TT,
- Epoch::TT(RawEpoch {
- second: 0,
- attosecond: 0,
- }),
- ),
- (
- TimeScale::UT1,
- Epoch::UT1(RawEpoch {
- second: 0,
- attosecond: 0,
- }),
- ),
- ]
- .iter()
- .for_each(|(scale, expected)| {
- let actual = Epoch::j2000(*scale);
- assert_eq!(*expected, actual);
- });
- }
-
- #[test]
- fn test_epoch_jd0() {
- [
- (
- TimeScale::TAI,
- Epoch::TAI(RawEpoch {
- second: -211813488000,
- attosecond: 0,
- }),
- ),
- (
- TimeScale::TCB,
- Epoch::TCB(RawEpoch {
- second: -211813488000,
- attosecond: 0,
- }),
- ),
- (
- TimeScale::TCG,
- Epoch::TCG(RawEpoch {
- second: -211813488000,
- attosecond: 0,
- }),
- ),
- (
- TimeScale::TDB,
- Epoch::TDB(RawEpoch {
- second: -211813488000,
- attosecond: 0,
- }),
- ),
- (
- TimeScale::TT,
- Epoch::TT(RawEpoch {
- second: -211813488000,
- attosecond: 0,
- }),
- ),
- (
- TimeScale::UT1,
- Epoch::UT1(RawEpoch {
- second: -211813488000,
- attosecond: 0,
- }),
- ),
- ]
- .iter()
- .for_each(|(scale, expected)| {
- let actual = Epoch::jd0(*scale);
- assert_eq!(*expected, actual);
- });
- }
-}
diff --git a/crates/lox_core/src/time/intervals.rs b/crates/lox_core/src/time/intervals.rs
index d2c0151b..b9800894 100644
--- a/crates/lox_core/src/time/intervals.rs
+++ b/crates/lox_core/src/time/intervals.rs
@@ -1,13 +1,13 @@
use crate::time::constants;
-use crate::time::epochs::Epoch;
+use crate::time::continuous::Time;
/// Although strictly TDB, TT is sufficient for most applications.
pub type TDBJulianCenturiesSinceJ2000 = f64;
-pub fn tdb_julian_centuries_since_j2000(epoch: Epoch) -> TDBJulianCenturiesSinceJ2000 {
- match epoch {
- Epoch::TT(_) | Epoch::TDB(_) => {
- epoch.days_since_j2000() / constants::f64::DAYS_PER_JULIAN_CENTURY
+pub fn tdb_julian_centuries_since_j2000(time: Time) -> TDBJulianCenturiesSinceJ2000 {
+ match time {
+ Time::TT(_) | Time::TDB(_) => {
+ time.days_since_j2000() / constants::f64::DAYS_PER_JULIAN_CENTURY
}
_ => todo!("perform the simpler of the conversions to TT or TDB first"),
}
@@ -18,12 +18,13 @@ pub type TTJulianCenturiesSinceJ2000 = f64;
pub type UT1DaysSinceJ2000 = f64;
#[cfg(test)]
-mod epoch_tests {
+mod tests {
use float_eq::assert_float_eq;
+ use crate::time::continuous::{Time, TimeScale};
use crate::time::dates::Calendar::Gregorian;
- use crate::time::dates::{Date, Time};
- use crate::time::epochs::{Epoch, TimeScale};
+ use crate::time::dates::Date;
+ use crate::time::utc::UTC;
use super::tdb_julian_centuries_since_j2000;
@@ -32,24 +33,24 @@ mod epoch_tests {
#[test]
fn test_tdb_julian_centuries_since_j2000_tt() {
- let jd0 = Epoch::jd0(TimeScale::TT);
+ let jd0 = Time::jd0(TimeScale::TT);
assert_float_eq!(
-67.11964407939767,
tdb_julian_centuries_since_j2000(jd0),
rel <= TOLERANCE
);
- let j2000 = Epoch::j2000(TimeScale::TT);
+ let j2000 = Time::j2000(TimeScale::TT);
assert_float_eq!(
0.0,
tdb_julian_centuries_since_j2000(j2000),
rel <= TOLERANCE
);
- let j2100 = Epoch::from_date_and_time(
+ let j2100 = Time::from_date_and_utc_timestamp(
TimeScale::TT,
Date::new_unchecked(Gregorian, 2100, 1, 1),
- Time::new(12, 0, 0).expect("midday should be a valid time"),
+ UTC::new(12, 0, 0).expect("midday should be a valid time"),
);
assert_float_eq!(
1.0,
diff --git a/crates/lox_core/src/time/utc.rs b/crates/lox_core/src/time/utc.rs
new file mode 100644
index 00000000..a193b911
--- /dev/null
+++ b/crates/lox_core/src/time/utc.rs
@@ -0,0 +1,332 @@
+use crate::errors::LoxError;
+use crate::time::constants::u64::{
+ ATTOSECONDS_PER_FEMTOSECOND, ATTOSECONDS_PER_MICROSECOND, ATTOSECONDS_PER_MILLISECOND,
+ ATTOSECONDS_PER_NANOSECOND, ATTOSECONDS_PER_PICOSECOND,
+};
+use crate::time::dates::Date;
+use crate::time::{PerMille, WallClock};
+use num::ToPrimitive;
+use std::fmt::Display;
+
+/// A UTC timestamp with additional support for fractional seconds represented with attosecond
+/// precision.
+///
+/// The `UTC` struct provides the ability to represent leap seconds by setting the `second`
+/// component to 60. However, it has no awareness of whether a user-specified leap second is valid.
+/// It is intended strictly as an IO time format which must be converted to a continuous time format
+/// to be used in calculations.
+#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
+pub struct UTC {
+ hour: u8,
+ minute: u8,
+ second: u8,
+ pub milli: PerMille,
+ pub micro: PerMille,
+ pub nano: PerMille,
+ pub pico: PerMille,
+ pub femto: PerMille,
+ pub atto: PerMille,
+}
+
+impl UTC {
+ pub fn new(hour: u8, minute: u8, second: u8) -> Result {
+ if !(0..24).contains(&hour) || !(0..60).contains(&minute) || !(0..61).contains(&second) {
+ Err(LoxError::InvalidTime(hour, minute, second))
+ } else {
+ Ok(Self {
+ hour,
+ minute,
+ second,
+ ..Default::default()
+ })
+ }
+ }
+
+ pub fn from_fractional_seconds(hour: u8, minute: u8, seconds: f64) -> Result {
+ if !(0.0..61.0).contains(&seconds) {
+ return Err(LoxError::InvalidSeconds(seconds));
+ }
+ let sub = split_seconds(seconds.fract()).unwrap();
+ let second = seconds.round().to_u8().unwrap();
+ Self::new(hour, minute, second)?;
+ Ok(Self {
+ hour,
+ minute,
+ second,
+ milli: PerMille(sub[0] as u16),
+ micro: PerMille(sub[1] as u16),
+ nano: PerMille(sub[2] as u16),
+ pico: PerMille(sub[3] as u16),
+ femto: PerMille(sub[4] as u16),
+ atto: PerMille(sub[5] as u16),
+ })
+ }
+
+ pub fn subsecond_as_attoseconds(&self) -> u64 {
+ let mut attoseconds = self.atto.0 as u64;
+ attoseconds += self.femto.0 as u64 * ATTOSECONDS_PER_FEMTOSECOND;
+ attoseconds += self.pico.0 as u64 * ATTOSECONDS_PER_PICOSECOND;
+ attoseconds += self.nano.0 as u64 * ATTOSECONDS_PER_NANOSECOND;
+ attoseconds += self.micro.0 as u64 * ATTOSECONDS_PER_MICROSECOND;
+ attoseconds += self.milli.0 as u64 * ATTOSECONDS_PER_MILLISECOND;
+ attoseconds
+ }
+}
+
+impl Display for UTC {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{:02}:{:02}:{:02}.{}.{}.{}.{}.{}.{} UTC",
+ self.hour,
+ self.minute,
+ self.second,
+ self.milli,
+ self.micro,
+ self.nano,
+ self.pico,
+ self.femto,
+ self.atto
+ )?;
+ Ok(())
+ }
+}
+
+impl WallClock for UTC {
+ fn hour(&self) -> i64 {
+ self.hour as i64
+ }
+
+ fn minute(&self) -> i64 {
+ self.minute as i64
+ }
+
+ fn second(&self) -> i64 {
+ self.second as i64
+ }
+
+ fn millisecond(&self) -> i64 {
+ self.milli.into()
+ }
+
+ fn microsecond(&self) -> i64 {
+ self.micro.into()
+ }
+
+ fn nanosecond(&self) -> i64 {
+ self.nano.into()
+ }
+
+ fn picosecond(&self) -> i64 {
+ self.pico.into()
+ }
+
+ fn femtosecond(&self) -> i64 {
+ self.femto.into()
+ }
+
+ fn attosecond(&self) -> i64 {
+ self.atto.into()
+ }
+}
+
+/// Split a floating-point second into SI-prefixed integer parts.
+fn split_seconds(seconds: f64) -> Option<[i64; 6]> {
+ if !(0.0..1.0).contains(&seconds) {
+ return None;
+ }
+ let mut atto = (seconds * 1e18).to_i64()?;
+ let mut parts: [i64; 6] = [0; 6];
+ for (i, exponent) in (3..18).step_by(3).rev().enumerate() {
+ let factor = i64::pow(10, exponent);
+ parts[i] = atto / factor;
+ atto -= parts[i] * factor;
+ }
+ parts[5] = atto / 10 * 10;
+ Some(parts)
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub struct UTCDateTime {
+ date: Date,
+ time: UTC,
+}
+
+impl UTCDateTime {
+ pub fn new(date: Date, time: UTC) -> Self {
+ Self { date, time }
+ }
+
+ pub fn date(&self) -> Date {
+ self.date
+ }
+
+ pub fn time(&self) -> UTC {
+ self.time
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::time::dates::Calendar::Gregorian;
+ use proptest::{prop_assert, proptest};
+
+ const TIME: UTC = UTC {
+ hour: 12,
+ minute: 34,
+ second: 56,
+ milli: PerMille(789),
+ micro: PerMille(123),
+ nano: PerMille(456),
+ pico: PerMille(789),
+ femto: PerMille(123),
+ atto: PerMille(456),
+ };
+
+ #[test]
+ fn test_time_display() {
+ assert_eq!("12:34:56.789.123.456.789.123.456 UTC", TIME.to_string());
+ }
+
+ #[test]
+ fn test_utc_wall_clock_hour() {
+ assert_eq!(TIME.hour(), TIME.hour as i64);
+ }
+
+ #[test]
+ fn test_utc_wall_clock_minute() {
+ assert_eq!(TIME.minute(), TIME.minute as i64);
+ }
+
+ #[test]
+ fn test_utc_wall_clock_second() {
+ assert_eq!(TIME.second(), TIME.second as i64);
+ }
+
+ #[test]
+ fn test_utc_wall_clock_millisecond() {
+ assert_eq!(TIME.millisecond(), TIME.milli.into());
+ }
+
+ #[test]
+ fn test_utc_wall_clock_microsecond() {
+ assert_eq!(TIME.microsecond(), TIME.micro.into());
+ }
+
+ #[test]
+ fn test_utc_wall_clock_nanosecond() {
+ assert_eq!(TIME.nanosecond(), TIME.nano.into());
+ }
+
+ #[test]
+ fn test_utc_wall_clock_picosecond() {
+ assert_eq!(TIME.picosecond(), TIME.pico.into());
+ }
+
+ #[test]
+ fn test_utc_wall_clock_femtosecond() {
+ assert_eq!(TIME.femtosecond(), TIME.femto.into());
+ }
+
+ #[test]
+ fn test_utc_wall_clock_attosecond() {
+ assert_eq!(TIME.attosecond(), TIME.atto.into());
+ }
+
+ proptest! {
+ #[test]
+ fn prop_test_split_seconds(s in 0.0..1.0) {
+ prop_assert!(split_seconds(s).is_some())
+ }
+ }
+
+ #[test]
+ fn test_split_seconds() {
+ let s1 = split_seconds(0.123).expect("seconds should be valid");
+ assert_eq!(123, s1[0]);
+ assert_eq!(0, s1[1]);
+ assert_eq!(0, s1[2]);
+ assert_eq!(0, s1[3]);
+ assert_eq!(0, s1[4]);
+ assert_eq!(0, s1[5]);
+ let s2 = split_seconds(0.123_456).expect("seconds should be valid");
+ assert_eq!(123, s2[0]);
+ assert_eq!(456, s2[1]);
+ assert_eq!(0, s2[2]);
+ assert_eq!(0, s2[3]);
+ assert_eq!(0, s2[4]);
+ assert_eq!(0, s2[5]);
+ let s3 = split_seconds(0.123_456_789).expect("seconds should be valid");
+ assert_eq!(123, s3[0]);
+ assert_eq!(456, s3[1]);
+ assert_eq!(789, s3[2]);
+ assert_eq!(0, s3[3]);
+ assert_eq!(0, s3[4]);
+ assert_eq!(0, s3[5]);
+ let s4 = split_seconds(0.123_456_789_123).expect("seconds should be valid");
+ assert_eq!(123, s4[0]);
+ assert_eq!(456, s4[1]);
+ assert_eq!(789, s4[2]);
+ assert_eq!(123, s4[3]);
+ assert_eq!(0, s4[4]);
+ assert_eq!(0, s4[5]);
+ let s5 = split_seconds(0.123_456_789_123_456).expect("seconds should be valid");
+ assert_eq!(123, s5[0]);
+ assert_eq!(456, s5[1]);
+ assert_eq!(789, s5[2]);
+ assert_eq!(123, s5[3]);
+ assert_eq!(456, s5[4]);
+ assert_eq!(0, s5[5]);
+ let s6 = split_seconds(0.123_456_789_123_456_78).expect("seconds should be valid");
+ assert_eq!(123, s6[0]);
+ assert_eq!(456, s6[1]);
+ assert_eq!(789, s6[2]);
+ assert_eq!(123, s6[3]);
+ assert_eq!(456, s6[4]);
+ assert_eq!(780, s6[5]);
+ let s7 = split_seconds(0.000_000_000_000_000_01).expect("seconds should be valid");
+ assert_eq!(0, s7[0]);
+ assert_eq!(0, s7[1]);
+ assert_eq!(0, s7[2]);
+ assert_eq!(0, s7[3]);
+ assert_eq!(0, s7[4]);
+ assert_eq!(10, s7[5]);
+ }
+
+ #[test]
+ fn test_illegal_split_seconds() {
+ assert!(split_seconds(2.0).is_none());
+ assert!(split_seconds(-0.2).is_none());
+ }
+
+ #[test]
+ fn test_utc_datetime_new() {
+ let date = Date::new_unchecked(Gregorian, 2021, 1, 1);
+ let time = UTC::new(12, 34, 56).expect("time should be valid");
+ let expected = UTCDateTime { date, time };
+ let actual = UTCDateTime::new(date, time);
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn test_from_fractional_seconds() {
+ let hour = 0;
+ let minute = 0;
+ let second = 0.123_456_789_123_456_78;
+ let expected = UTC {
+ hour: 0,
+ minute: 0,
+ second: 0,
+ milli: PerMille(123),
+ micro: PerMille(456),
+ nano: PerMille(789),
+ pico: PerMille(123),
+ femto: PerMille(456),
+ atto: PerMille(780),
+ };
+ let actual =
+ UTC::from_fractional_seconds(hour, minute, second).expect("time should be valid");
+ assert_eq!(expected, actual);
+ }
+}
diff --git a/crates/lox_core/tests/dates.rs b/crates/lox_core/tests/dates.rs
index d929c221..c1f15fab 100644
--- a/crates/lox_core/tests/dates.rs
+++ b/crates/lox_core/tests/dates.rs
@@ -6,7 +6,8 @@
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*/
-use lox_core::time::dates::{Date, Time};
+use lox_core::time::dates::Date;
+use lox_core::time::utc::UTC;
use rstest::rstest;
#[rstest]
@@ -47,7 +48,7 @@ fn test_illegal_dates() {
#[test]
fn test_illegal_times() {
- assert!(Time::from_seconds(24, 59, 59.0).is_err());
- assert!(Time::from_seconds(23, 60, 59.0).is_err());
- assert!(Time::from_seconds(23, 59, 61.0).is_err());
+ assert!(UTC::from_fractional_seconds(24, 59, 59.0).is_err());
+ assert!(UTC::from_fractional_seconds(23, 60, 59.0).is_err());
+ assert!(UTC::from_fractional_seconds(23, 59, 61.0).is_err());
}
diff --git a/crates/lox_py/src/coords.rs b/crates/lox_py/src/coords.rs
index a0928c36..7747188b 100644
--- a/crates/lox_py/src/coords.rs
+++ b/crates/lox_py/src/coords.rs
@@ -16,7 +16,7 @@ use lox_core::coords::DVec3;
use crate::bodies::PyBody;
use crate::frames::PyFrame;
-use crate::time::PyEpoch;
+use crate::time::PyTime;
#[pyclass(name = "Cartesian")]
pub struct PyCartesian {
@@ -30,7 +30,7 @@ impl PyCartesian {
#[allow(clippy::too_many_arguments)]
#[new]
fn new(
- time: &PyEpoch,
+ time: &PyTime,
body: PyObject,
frame: &str,
x: f64,
@@ -50,8 +50,8 @@ impl PyCartesian {
})
}
- fn time(&self) -> PyEpoch {
- PyEpoch(self.state.time())
+ fn time(&self) -> PyTime {
+ PyTime(self.state.time())
}
fn reference_frame(&self) -> String {
@@ -95,7 +95,7 @@ impl PyKeplerian {
#[new]
#[allow(clippy::too_many_arguments)]
fn new(
- t: &PyEpoch,
+ t: &PyTime,
body: PyObject,
frame: &str,
semi_major_axis: f64,
@@ -123,8 +123,8 @@ impl PyKeplerian {
})
}
- fn time(&self) -> PyEpoch {
- PyEpoch(self.state.time())
+ fn time(&self) -> PyTime {
+ PyTime(self.state.time())
}
fn reference_frame(&self) -> String {
@@ -180,7 +180,7 @@ mod tests {
#[test]
fn test_cartesian() {
- let epoch = PyEpoch::new(
+ let epoch = PyTime::new(
"TDB",
2023,
3,
@@ -244,7 +244,7 @@ mod tests {
#[test]
fn test_keplerian() {
- let epoch = PyEpoch::new(
+ let epoch = PyTime::new(
"TDB",
2023,
3,
diff --git a/crates/lox_py/src/lib.rs b/crates/lox_py/src/lib.rs
index 4fcea7e9..87c161a8 100644
--- a/crates/lox_py/src/lib.rs
+++ b/crates/lox_py/src/lib.rs
@@ -12,7 +12,7 @@ use thiserror::Error;
use crate::bodies::{PyBarycenter, PyMinorBody, PyPlanet, PySatellite, PySun};
use crate::coords::{PyCartesian, PyKeplerian};
-use crate::time::{PyEpoch, PyTimeScale};
+use crate::time::{PyTime, PyTimeScale};
use lox_core::errors::LoxError;
mod bodies;
@@ -50,7 +50,7 @@ impl From for PyErr {
#[pymodule]
fn lox_space(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::()?;
- m.add_class::()?;
+ m.add_class::()?;
m.add_class::()?;
m.add_class::()?;
m.add_class::()?;
diff --git a/crates/lox_py/src/time.rs b/crates/lox_py/src/time.rs
index 45dd72aa..565bff01 100644
--- a/crates/lox_py/src/time.rs
+++ b/crates/lox_py/src/time.rs
@@ -8,8 +8,10 @@
use pyo3::{pyclass, pymethods};
-use lox_core::time::dates::{Date, Time};
-use lox_core::time::epochs::{Epoch, TimeScale};
+use lox_core::time::continuous::{Time, TimeScale};
+use lox_core::time::dates::Date;
+use lox_core::time::utc::UTC;
+use lox_core::time::PerMille;
use crate::LoxPyError;
@@ -40,12 +42,12 @@ impl PyTimeScale {
}
}
-#[pyclass(name = "Epoch")]
+#[pyclass(name = "Time")]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub struct PyEpoch(pub Epoch);
+pub struct PyTime(pub Time);
#[pymethods]
-impl PyEpoch {
+impl PyTime {
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (
scale,
@@ -68,15 +70,15 @@ impl PyEpoch {
year: i64,
month: i64,
day: i64,
- hour: Option,
- minute: Option,
- second: Option,
- milli: Option,
- micro: Option,
- nano: Option,
- pico: Option,
- femto: Option,
- atto: Option,
+ hour: Option,
+ minute: Option,
+ second: Option,
+ milli: Option,
+ micro: Option,
+ nano: Option,
+ pico: Option,
+ femto: Option,
+ atto: Option,
) -> Result {
let time_scale = PyTimeScale::new(scale)?;
let date = Date::new(year, month, day)?;
@@ -84,26 +86,30 @@ impl PyEpoch {
let hour = hour.unwrap_or(0);
let minute = minute.unwrap_or(0);
let second = second.unwrap_or(0);
- let mut time = Time::new(hour, minute, second)?;
+ let mut utc = UTC::new(hour, minute, second)?;
if let Some(milli) = milli {
- time = time.milli(milli);
+ utc.milli = PerMille::new(milli)?;
}
if let Some(micro) = micro {
- time = time.micro(micro);
+ utc.micro = PerMille::new(micro)?;
}
if let Some(nano) = nano {
- time = time.nano(nano);
+ utc.nano = PerMille::new(nano)?;
}
if let Some(pico) = pico {
- time = time.pico(pico);
+ utc.pico = PerMille::new(pico)?;
}
if let Some(femto) = femto {
- time = time.femto(femto);
+ utc.femto = PerMille::new(femto)?;
}
if let Some(atto) = atto {
- time = time.atto(atto);
+ utc.atto = PerMille::new(atto)?;
}
- Ok(PyEpoch(Epoch::from_date_and_time(time_scale.0, date, time)))
+ Ok(PyTime(Time::from_date_and_utc_timestamp(
+ time_scale.0,
+ date,
+ utc,
+ )))
}
fn days_since_j2000(&self) -> f64 {
@@ -111,7 +117,7 @@ impl PyEpoch {
}
fn scale(&self) -> &str {
- self.0.scale()
+ self.0.scale().into()
}
}
@@ -144,7 +150,7 @@ mod tests {
#[test]
fn test_time() {
- let time = PyEpoch::new(
+ let time = PyTime::new(
"TDB",
2024,
1,
@@ -160,7 +166,7 @@ mod tests {
Some(789),
)
.expect("time should be valid");
- assert_eq!(time.0.attosecond(), 123456789123456789);
+ assert_eq!(time.0.attoseconds(), 123456789123456789);
assert_float_eq!(time.days_since_j2000(), 8765.542374114084, rel <= 1e-8);
assert_eq!(time.scale(), "TDB");
}
diff --git a/crates/lox_py/tests/test_coords.py b/crates/lox_py/tests/test_coords.py
index 33926b0d..7f5c50b2 100644
--- a/crates/lox_py/tests/test_coords.py
+++ b/crates/lox_py/tests/test_coords.py
@@ -8,7 +8,7 @@
def test_coords():
- time = lox.Epoch("TDB", 2016, 5, 30, 12)
+ time = lox.Time("TDB", 2016, 5, 30, 12)
x = 6068.27927
y = -1692.84394
z = -2516.61918