celestial_time/scales/
ut1.rs1use 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#[derive(Debug, Clone, Copy, PartialEq)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub struct UT1(JulianDate);
59
60impl UT1 {
61 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 pub fn from_julian_date(jd: JulianDate) -> Self {
76 Self(jd)
77 }
78
79 pub fn j2000() -> Self {
81 Self(JulianDate::j2000())
82 }
83
84 pub fn to_julian_date(&self) -> JulianDate {
86 self.0
87 }
88
89 pub fn add_seconds(&self, seconds: f64) -> Self {
91 Self(self.0.add_seconds(seconds))
92 }
93
94 pub fn add_days(&self, days: f64) -> Self {
96 Self(self.0.add_days(days))
97 }
98}
99
100pub 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
133impl fmt::Display for UT1 {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 write!(f, "UT1 {}", self.0)
137 }
138}
139
140impl From<JulianDate> for UT1 {
142 fn from(jd: JulianDate) -> Self {
143 Self::from_julian_date(jd)
144 }
145}
146
147impl 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}