Skip to main content

celestial_time/scales/
ut1.rs

1//! Universal Time UT1 time scale.
2//!
3//! UT1 is the principal form of Universal Time, defined by Earth's rotation angle.
4//! Unlike atomic time scales (TAI, TT), UT1 tracks the actual rotational position
5//! of the Earth, making it essential for astronomical observations and coordinate
6//! transformations.
7//!
8//! # Background
9//!
10//! Earth's rotation is irregular due to tidal friction, core-mantle coupling, and
11//! atmospheric effects. UT1 accumulates an unpredictable offset from UTC (typically
12//! |DUT1| < 0.9s). The offset UT1-UTC is published by IERS in Bulletin A/B.
13//!
14//! ```text
15//! UT1 = UTC + DUT1    (where DUT1 from IERS observations)
16//! ```
17//!
18//! # Usage
19//!
20//! ```
21//! use celestial_time::{JulianDate, UT1};
22//! use celestial_time::scales::ut1::ut1_from_calendar;
23//!
24//! // From Unix timestamp components
25//! let ut1 = UT1::new(0, 0);  // Unix epoch in UT1
26//!
27//! // From calendar date
28//! let ut1 = ut1_from_calendar(2000, 1, 1, 12, 0, 0.0);
29//!
30//! // From Julian Date
31//! let ut1 = UT1::from_julian_date(JulianDate::j2000());
32//! ```
33//!
34//! # Relationship to Other Scales
35//!
36//! UT1 is required for:
37//! - Sidereal time calculations (GMST, GAST, ERA)
38//! - Earth orientation parameters
39//! - Topocentric coordinate transformations
40//!
41//! Conversion from UTC requires external Earth Orientation Parameters (EOP) data.
42
43use crate::constants::UNIX_EPOCH_JD;
44use crate::julian::JulianDate;
45use celestial_core::constants::MJD_ZERO_POINT;
46
47use crate::parsing::parse_iso8601;
48use crate::{TimeError, TimeResult};
49use celestial_core::constants::{NANOSECONDS_PER_SECOND_F64, SECONDS_PER_DAY, SECONDS_PER_DAY_F64};
50use std::fmt;
51use std::str::FromStr;
52
53/// Universal Time UT1, based on Earth's rotation angle.
54///
55/// Internally stores time as a split Julian Date for full precision.
56#[derive(Debug, Clone, Copy, PartialEq)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub struct UT1(JulianDate);
59
60impl UT1 {
61    /// Creates UT1 from Unix timestamp components.
62    ///
63    /// Converts seconds and nanoseconds since Unix epoch (1970-01-01 00:00:00)
64    /// to a split Julian Date representation.
65    pub fn new(seconds: i64, nanos: u32) -> Self {
66        let days = seconds / SECONDS_PER_DAY;
67        let remainder_seconds = seconds % SECONDS_PER_DAY;
68        let jd1 = UNIX_EPOCH_JD + days as f64;
69        let jd2 = (remainder_seconds as f64 + nanos as f64 / NANOSECONDS_PER_SECOND_F64)
70            / SECONDS_PER_DAY_F64;
71        Self(JulianDate::new(jd1, jd2))
72    }
73
74    /// Creates UT1 from a Julian Date.
75    pub fn from_julian_date(jd: JulianDate) -> Self {
76        Self(jd)
77    }
78
79    /// Returns UT1 at the J2000.0 epoch (2000-01-01T12:00:00).
80    pub fn j2000() -> Self {
81        Self(JulianDate::j2000())
82    }
83
84    /// Returns the underlying Julian Date.
85    pub fn to_julian_date(&self) -> JulianDate {
86        self.0
87    }
88
89    /// Adds seconds to this UT1 instant. Negative values subtract.
90    pub fn add_seconds(&self, seconds: f64) -> Self {
91        Self(self.0.add_seconds(seconds))
92    }
93
94    /// Adds days to this UT1 instant. Negative values subtract.
95    pub fn add_days(&self, days: f64) -> Self {
96        Self(self.0.add_days(days))
97    }
98}
99
100/// Creates UT1 from Gregorian calendar components.
101///
102/// Uses a proleptic Gregorian calendar algorithm. The date is converted to
103/// Modified Julian Date, then to split Julian Date with the time fraction
104/// stored separately for precision.
105///
106/// # Arguments
107///
108/// * `year` - Gregorian year (negative for BCE)
109/// * `month` - Month 1-12
110/// * `day` - Day of month 1-31
111/// * `hour` - Hour 0-23
112/// * `minute` - Minute 0-59
113/// * `second` - Second with fractional part
114pub fn ut1_from_calendar(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: f64) -> UT1 {
115    let my = (month as i32 - 14) / 12;
116    let iypmy = year + my;
117
118    let mjd_zero = MJD_ZERO_POINT;
119
120    let modified_jd = ((1461 * (iypmy + 4800)) / 4 + (367 * (month as i32 - 2 - 12 * my)) / 12
121        - (3 * ((iypmy + 4900) / 100)) / 4
122        + day as i32
123        - 2432076) as f64;
124
125    let time_fraction =
126        (60.0 * (60 * hour as i32 + minute as i32) as f64 + second) / SECONDS_PER_DAY_F64;
127    let jd1 = mjd_zero + modified_jd;
128    let jd2 = time_fraction;
129
130    UT1::from_julian_date(JulianDate::new(jd1, jd2))
131}
132
133/// Formats as "UT1 {julian_date}".
134impl fmt::Display for UT1 {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        write!(f, "UT1 {}", self.0)
137    }
138}
139
140/// Converts a Julian Date to UT1.
141impl From<JulianDate> for UT1 {
142    fn from(jd: JulianDate) -> Self {
143        Self::from_julian_date(jd)
144    }
145}
146
147/// Parses UT1 from an ISO 8601 string.
148///
149/// Accepts formats like "2000-01-01T12:00:00" or "2000-01-01T12:00:00.123".
150impl FromStr for UT1 {
151    type Err = TimeError;
152
153    fn from_str(s: &str) -> TimeResult<Self> {
154        let parsed = parse_iso8601(s)?;
155        Ok(Self::from_julian_date(parsed.to_julian_date()))
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use crate::constants::UNIX_EPOCH_JD;
163    use celestial_core::constants::J2000_JD;
164
165    #[test]
166    fn test_ut1_constructors() {
167        assert_eq!(UT1::new(0, 0).to_julian_date().to_f64(), UNIX_EPOCH_JD);
168        assert_eq!(UT1::j2000().to_julian_date().to_f64(), J2000_JD);
169        assert_eq!(
170            ut1_from_calendar(2000, 1, 1, 12, 0, 0.0)
171                .to_julian_date()
172                .to_f64(),
173            J2000_JD
174        );
175
176        let jd = JulianDate::new(J2000_JD, 0.123456789);
177        let ut1_direct = UT1::from_julian_date(jd);
178        let ut1_from_trait: UT1 = jd.into();
179        assert_eq!(ut1_direct, ut1_from_trait);
180    }
181
182    #[test]
183    fn test_ut1_arithmetic() {
184        let ut1 = UT1::j2000();
185        assert_eq!(ut1.add_days(1.0).to_julian_date().to_f64(), J2000_JD + 1.0);
186        assert_eq!(
187            ut1.add_seconds(3600.0).to_julian_date().to_f64(),
188            J2000_JD + 1.0 / 24.0
189        );
190    }
191
192    #[test]
193    fn test_ut1_display() {
194        let display_str = format!("{}", UT1::from_julian_date(JulianDate::new(J2000_JD, 0.5)));
195        assert!(display_str.starts_with("UT1"));
196        assert!(display_str.contains("2451545"));
197    }
198
199    #[test]
200    fn test_ut1_string_parsing() {
201        assert_eq!(
202            UT1::from_str("2000-01-01T12:00:00")
203                .unwrap()
204                .to_julian_date()
205                .to_f64(),
206            UT1::j2000().to_julian_date().to_f64()
207        );
208
209        let result = UT1::from_str("2000-01-01T12:00:00.123").unwrap();
210        let expected_jd = J2000_JD + 0.123 / SECONDS_PER_DAY_F64;
211        let diff = (result.to_julian_date().to_f64() - expected_jd).abs();
212        assert!(diff < 1e-14, "fractional seconds diff: {:.2e}", diff);
213
214        assert!(UT1::from_str("invalid-date").is_err());
215    }
216}