1use super::common::{get_tai_utc_offset, next_calendar_day};
47use crate::constants::UNIX_EPOCH_JD;
48use crate::julian::JulianDate;
49use crate::parsing::parse_iso8601;
50use crate::{TimeError, TimeResult};
51use celestial_core::constants::SECONDS_PER_DAY_F64;
52use std::fmt;
53use std::str::FromStr;
54
55#[derive(Debug, Clone, Copy, PartialEq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61pub struct UTC(JulianDate);
62
63impl UTC {
64 pub fn new(seconds: i64, nanos: u32) -> Self {
69 let days = seconds / celestial_core::constants::SECONDS_PER_DAY;
70 let remainder_seconds = seconds % celestial_core::constants::SECONDS_PER_DAY;
71 let jd1 = UNIX_EPOCH_JD + days as f64;
72 let jd2 = (remainder_seconds as f64
73 + nanos as f64 / celestial_core::constants::NANOSECONDS_PER_SECOND_F64)
74 / SECONDS_PER_DAY_F64;
75 Self(JulianDate::new(jd1, jd2))
76 }
77
78 pub fn from_julian_date(jd: JulianDate) -> Self {
80 Self(jd)
81 }
82
83 pub fn j2000() -> Self {
85 Self(JulianDate::j2000())
86 }
87
88 pub fn to_julian_date(&self) -> JulianDate {
90 self.0
91 }
92
93 pub fn add_seconds(&self, seconds: f64) -> Self {
95 Self(self.0.add_seconds(seconds))
96 }
97
98 pub fn add_days(&self, days: f64) -> Self {
100 Self(self.0.add_days(days))
101 }
102
103 pub fn now() -> Self {
105 use std::time::{SystemTime, UNIX_EPOCH};
106 let duration = SystemTime::now()
107 .duration_since(UNIX_EPOCH)
108 .unwrap_or_default();
109 Self::new(duration.as_secs() as i64, duration.subsec_nanos())
110 }
111
112 pub fn to_iso8601(&self) -> String {
116 use crate::scales::conversions::utc_tai::julian_to_calendar;
117 let jd = self.to_julian_date();
118 if let Ok((year, month, day, frac)) = julian_to_calendar(jd.jd1(), jd.jd2()) {
119 let total_seconds = frac * SECONDS_PER_DAY_F64;
120 let hour = (total_seconds / 3600.0) as u8;
121 let minute = ((total_seconds % 3600.0) / 60.0) as u8;
122 let second = total_seconds % 60.0;
123 format!(
124 "{:04}-{:02}-{:02}T{:02}:{:02}:{:06.3}",
125 year, month, day, hour, minute, second
126 )
127 } else {
128 format!("JD{:.6}", jd.jd1() + jd.jd2())
129 }
130 }
131}
132
133pub fn utc_from_calendar(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: f64) -> UTC {
143 let base_jd = JulianDate::from_calendar(year, month, day, 0, 0, 0.0);
144
145 let mut day_length = SECONDS_PER_DAY_F64;
146
147 let dat0 = get_tai_utc_offset(year, month as i32, day as i32, 0.0);
148 let dat12 = get_tai_utc_offset(year, month as i32, day as i32, 0.5);
149
150 let (next_year, next_month, next_day) = next_calendar_day(year, month as i32, day as i32)
151 .expect("Invalid month in UTC calendar conversion");
152 let dat24 = get_tai_utc_offset(next_year, next_month, next_day, 0.0);
153
154 let dleap = dat24 - (2.0 * dat12 - dat0);
155 day_length += dleap;
156
157 let time_fraction = (60.0 * (60 * hour as i32 + minute as i32) as f64 + second) / day_length;
158
159 UTC::from_julian_date(JulianDate::new(
160 base_jd.jd1(),
161 base_jd.jd2() + time_fraction,
162 ))
163}
164
165impl fmt::Display for UTC {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 write!(f, "UTC {}", self.0)
169 }
170}
171
172impl From<JulianDate> for UTC {
174 fn from(jd: JulianDate) -> Self {
175 Self::from_julian_date(jd)
176 }
177}
178
179impl FromStr for UTC {
181 type Err = TimeError;
182
183 fn from_str(s: &str) -> TimeResult<Self> {
184 let parsed = parse_iso8601(s)?;
185 Ok(Self::from_julian_date(parsed.to_julian_date()))
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use crate::constants::UNIX_EPOCH_JD;
193 use celestial_core::constants::J2000_JD;
194
195 #[test]
196 fn test_utc_constructors() {
197 assert_eq!(UTC::new(0, 0).to_julian_date().to_f64(), UNIX_EPOCH_JD);
198 assert_eq!(UTC::j2000().to_julian_date().to_f64(), J2000_JD);
199 assert_eq!(
200 utc_from_calendar(2000, 1, 1, 12, 0, 0.0)
201 .to_julian_date()
202 .to_f64(),
203 J2000_JD
204 );
205
206 let jd = JulianDate::new(J2000_JD, 0.123456789);
207 let utc_direct = UTC::from_julian_date(jd);
208 let utc_from_trait: UTC = jd.into();
209 assert_eq!(utc_direct, utc_from_trait);
210 }
211
212 #[test]
213 fn test_utc_arithmetic() {
214 let utc = UTC::j2000();
215 assert_eq!(utc.add_days(1.0).to_julian_date().to_f64(), J2000_JD + 1.0);
216 assert_eq!(
217 utc.add_seconds(3600.0).to_julian_date().to_f64(),
218 J2000_JD + 1.0 / 24.0
219 );
220 }
221
222 #[test]
223 fn test_utc_display() {
224 let display_str = format!("{}", UTC::from_julian_date(JulianDate::new(J2000_JD, 0.5)));
225 assert!(display_str.starts_with("UTC"));
226 assert!(display_str.contains("2451545"));
227 }
228
229 #[test]
230 fn test_utc_string_parsing() {
231 assert_eq!(
232 UTC::from_str("2000-01-01T12:00:00")
233 .unwrap()
234 .to_julian_date()
235 .to_f64(),
236 J2000_JD
237 );
238
239 let result = UTC::from_str("2000-01-01T12:00:00.123").unwrap();
240 let expected_jd = J2000_JD + 0.123 / SECONDS_PER_DAY_F64;
241 let diff = (result.to_julian_date().to_f64() - expected_jd).abs();
242 assert!(diff < 1e-14, "fractional seconds diff: {:.2e}", diff);
243
244 assert!(UTC::from_str("invalid-date").is_err());
245 }
246
247 #[test]
248 fn test_utc_new_precision_preservation() {
249 let seconds_50_years = 50 * 365 * celestial_core::constants::SECONDS_PER_DAY as u32;
250 let nanos = 123456789u32;
251
252 let utc = UTC::new(seconds_50_years as i64, nanos);
253 let jd = utc.to_julian_date();
254
255 let expected_days = seconds_50_years / celestial_core::constants::SECONDS_PER_DAY as u32;
256 let remainder_secs = seconds_50_years % celestial_core::constants::SECONDS_PER_DAY as u32;
257 let expected_jd1 = UNIX_EPOCH_JD + expected_days as f64;
258 let expected_jd2 = (remainder_secs as f64
259 + nanos as f64 / celestial_core::constants::NANOSECONDS_PER_SECOND_F64)
260 / SECONDS_PER_DAY_F64;
261
262 assert_eq!(jd.jd1(), expected_jd1);
263 assert_eq!(jd.jd2(), expected_jd2);
264 }
265
266 #[test]
267 fn test_tai_utc_offset_edge_cases() {
268 assert_eq!(get_tai_utc_offset(2000, 1, 1, -0.5), 0.0);
269 assert_eq!(get_tai_utc_offset(2000, 1, 1, 1.5), 0.0);
270 assert_eq!(get_tai_utc_offset(1950, 6, 15, 0.5), 0.0);
271 assert!(get_tai_utc_offset(1960, 1, 1, 0.0) > 0.0);
272 }
273
274 #[test]
275 fn test_next_calendar_day() {
276 assert!(next_calendar_day(2000, 13, 15).is_err());
277
278 let cases: &[(i32, i32, i32, (i32, i32, i32))] = &[
279 (2000, 2, 28, (2000, 2, 29)),
280 (1999, 2, 28, (1999, 3, 1)),
281 (2000, 4, 30, (2000, 5, 1)),
282 (2000, 12, 31, (2001, 1, 1)),
283 ];
284
285 for &(y, m, d, expected) in cases {
286 assert_eq!(next_calendar_day(y, m, d).unwrap(), expected);
287 }
288 }
289
290 #[cfg(feature = "serde")]
291 #[test]
292 fn test_utc_serde_round_trip() {
293 let test_cases = [
294 UTC::j2000(),
295 UTC::new(0, 0),
296 utc_from_calendar(2024, 6, 15, 14, 30, 45.123),
297 utc_from_calendar(1990, 12, 31, 23, 59, 59.0),
298 utc_from_calendar(2015, 6, 30, 23, 59, 59.999),
299 ];
300
301 for original in test_cases {
302 let json = serde_json::to_string(&original).unwrap();
303 let deserialized: UTC = serde_json::from_str(&json).unwrap();
304 assert_eq!(original, deserialized);
305 }
306 }
307}