1use std::fmt::{self, Display, Formatter};
14use std::str::FromStr;
15
16use itertools::Itertools;
17use lox_core::i64::consts::{SECONDS_PER_DAY, SECONDS_PER_HALF_DAY};
18use lox_test_utils::approx_eq::{ApproxEq, ApproxEqResults};
19use thiserror::Error;
20
21use crate::calendar_dates::{CalendarDate, Date, DateError};
22use crate::deltas::{TimeDelta, ToDelta};
23use crate::julian_dates::{self, Epoch, JulianDate};
24use crate::time_of_day::{CivilTime, TimeOfDay, TimeOfDayError};
25use crate::utc::leap_seconds::{DefaultLeapSecondsProvider, LeapSecondsProvider};
26
27pub mod leap_seconds;
28pub mod transformations;
29
30#[derive(Debug, Clone, Error, PartialEq, Eq)]
32pub enum UtcError {
33 #[error(transparent)]
34 DateError(#[from] DateError),
35 #[error(transparent)]
36 TimeError(#[from] TimeOfDayError),
37 #[error("no leap second on {0}")]
38 NonLeapSecondDate(Date),
39 #[error("unable to construct UTC datetime")]
40 UtcUndefined,
41 #[error("invalid ISO string `{0}`")]
42 InvalidIsoString(String),
43}
44
45#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48pub struct Utc {
49 date: Date,
50 time: TimeOfDay,
51}
52
53impl Utc {
54 pub fn new(
62 date: Date,
63 time: TimeOfDay,
64 provider: &impl LeapSecondsProvider,
65 ) -> Result<Self, UtcError> {
66 if time.second() == 60 && !provider.is_leap_second_date(date) {
67 return Err(UtcError::NonLeapSecondDate(date));
68 }
69 Ok(Self { date, time })
70 }
71
72 pub fn builder() -> UtcBuilder {
74 UtcBuilder::default()
75 }
76
77 pub fn from_iso_with_provider<T: LeapSecondsProvider>(
88 iso: &str,
89 provider: &T,
90 ) -> Result<Self, UtcError> {
91 let _ = iso.strip_suffix('Z');
92
93 let Some((date, time_and_scale)) = iso.split_once('T') else {
94 return Err(UtcError::InvalidIsoString(iso.to_owned()));
95 };
96
97 let (time, scale_abbrv) = time_and_scale
98 .split_whitespace()
99 .collect_tuple()
100 .unwrap_or((time_and_scale, ""));
101
102 if !scale_abbrv.is_empty() && scale_abbrv != "UTC" {
103 return Err(UtcError::InvalidIsoString(iso.to_owned()));
104 }
105
106 let date: Date = date.parse()?;
107 let time: TimeOfDay = time.parse()?;
108
109 Utc::new(date, time, provider)
110 }
111
112 pub fn from_iso(iso: &str) -> Result<Self, UtcError> {
115 Self::from_iso_with_provider(iso, &DefaultLeapSecondsProvider)
116 }
117
118 pub fn from_delta(delta: TimeDelta) -> Result<Self, UtcError> {
122 let (seconds, subsecond) = delta
123 .as_seconds_and_subsecond()
124 .ok_or(UtcError::UtcUndefined)?;
125 let date = Date::from_seconds_since_j2000(seconds);
126 let time = TimeOfDay::from_seconds_since_j2000(seconds).with_subsecond(subsecond);
127 Ok(Self { date, time })
128 }
129}
130
131impl ToDelta for Utc {
132 fn to_delta(&self) -> TimeDelta {
133 let seconds = self.date.j2000_day_number() * SECONDS_PER_DAY + self.time.second_of_day()
134 - SECONDS_PER_HALF_DAY;
135 TimeDelta::from_seconds_and_subsecond(seconds, self.time.subsecond())
136 }
137}
138
139impl Display for Utc {
140 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
141 let precision = f.precision().unwrap_or(3);
142 write!(f, "{}T{:.*} UTC", self.date(), precision, self.time())
143 }
144}
145
146impl FromStr for Utc {
147 type Err = UtcError;
148
149 fn from_str(iso: &str) -> Result<Self, Self::Err> {
150 Self::from_iso(iso)
151 }
152}
153
154impl CalendarDate for Utc {
155 fn date(&self) -> Date {
156 self.date
157 }
158}
159
160impl CivilTime for Utc {
161 fn time(&self) -> TimeOfDay {
162 self.time
163 }
164}
165
166impl JulianDate for Utc {
167 fn julian_date(&self, epoch: Epoch, unit: julian_dates::Unit) -> f64 {
168 self.to_delta().julian_date(epoch, unit)
169 }
170}
171
172impl ApproxEq for Utc {
173 fn approx_eq(&self, rhs: &Self, atol: f64, rtol: f64) -> ApproxEqResults {
174 self.to_delta().approx_eq(&rhs.to_delta(), atol, rtol)
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct UtcBuilder {
181 date: Result<Date, DateError>,
182 time: Result<TimeOfDay, TimeOfDayError>,
183}
184
185impl Default for UtcBuilder {
186 fn default() -> Self {
188 Self {
189 date: Ok(Date::default()),
190 time: Ok(TimeOfDay::default()),
191 }
192 }
193}
194
195impl UtcBuilder {
196 pub fn with_ymd(self, year: i64, month: u8, day: u8) -> Self {
198 Self {
199 date: Date::new(year, month, day),
200 ..self
201 }
202 }
203
204 pub fn with_hms(self, hour: u8, minute: u8, seconds: f64) -> Self {
206 Self {
207 time: TimeOfDay::from_hms(hour, minute, seconds),
208 ..self
209 }
210 }
211
212 pub fn build_with_provider(self, provider: &impl LeapSecondsProvider) -> Result<Utc, UtcError> {
215 let date = self.date?;
216 let time = self.time?;
217 Utc::new(date, time, provider)
218 }
219
220 pub fn build(self) -> Result<Utc, UtcError> {
222 self.build_with_provider(&DefaultLeapSecondsProvider)
223 }
224}
225
226#[macro_export]
240macro_rules! utc {
241 ($year:literal, $month:literal, $day:literal) => {
242 Utc::builder().with_ymd($year, $month, $day).build()
243 };
244 ($year:literal, $month:literal, $day:literal, $hour:literal) => {
245 Utc::builder()
246 .with_ymd($year, $month, $day)
247 .with_hms($hour, 0, 0.0)
248 .build()
249 };
250 ($year:literal, $month:literal, $day:literal, $hour:literal, $minute:literal) => {
251 Utc::builder()
252 .with_ymd($year, $month, $day)
253 .with_hms($hour, $minute, 0.0)
254 .build()
255 };
256 ($year:literal, $month:literal, $day:literal, $hour:literal, $minute:literal, $second:literal) => {
257 Utc::builder()
258 .with_ymd($year, $month, $day)
259 .with_hms($hour, $minute, $second)
260 .build()
261 };
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use rstest::rstest;
268
269 #[test]
270 fn test_utc_display() {
271 let utc = Utc::default();
272 let expected = "2000-01-01T00:00:00.000 UTC".to_string();
273 let actual = utc.to_string();
274 assert_eq!(expected, actual);
275 let expected = "2000-01-01T00:00:00.000000000000000 UTC".to_string();
276 let actual = format!("{utc:.15}");
277 assert_eq!(expected, actual);
278 }
279
280 #[rstest]
281 #[case(utc!(2000, 1, 1), Utc::builder().with_ymd(2000, 1, 1).build())]
282 #[case(utc!(2000, 1, 1, 12), Utc::builder().with_ymd(2000, 1, 1).with_hms(12, 0, 0.0).build())]
283 #[case(utc!(2000, 1, 1, 12, 13), Utc::builder().with_ymd(2000, 1, 1).with_hms(12, 13, 0.0).build())]
284 #[case(utc!(2000, 1, 1, 12, 13, 14.15), Utc::builder().with_ymd(2000, 1, 1).with_hms(12, 13, 14.15).build())]
285 fn test_utc_macro(
286 #[case] actual: Result<Utc, UtcError>,
287 #[case] expected: Result<Utc, UtcError>,
288 ) {
289 assert_eq!(actual, expected)
290 }
291
292 #[test]
293 fn test_utc_non_leap_second_date() {
294 let actual = Utc::builder()
295 .with_ymd(2000, 1, 1)
296 .with_hms(23, 59, 60.0)
297 .build();
298 let expected = Err(UtcError::NonLeapSecondDate(Date::new(2000, 1, 1).unwrap()));
299 assert_eq!(actual, expected)
300 }
301
302 #[test]
303 fn test_utc_before_1960() {
304 let actual = Utc::builder().with_ymd(1959, 12, 31).build();
305 assert!(actual.is_ok());
306 }
307
308 #[test]
309 fn test_utc_builder_with_provider() {
310 let exp = utc!(2000, 1, 1).unwrap();
311 let act = Utc::builder()
312 .with_ymd(2000, 1, 1)
313 .build_with_provider(&DefaultLeapSecondsProvider)
314 .unwrap();
315 assert_eq!(exp, act)
316 }
317
318 #[rstest]
319 #[case("2000-01-01T00:00:00", Ok(utc!(2000, 1, 1).unwrap()))]
320 #[case("2000-01-01T00:00:00 UTC", Ok(utc!(2000, 1, 1).unwrap()))]
321 #[case("2000-01-01T00:00:00.000Z", Ok(utc!(2000, 1, 1).unwrap()))]
322 #[case("2000-1-01T00:00:00", Err(UtcError::DateError(DateError::InvalidIsoString("2000-1-01".to_string()))))]
323 #[case("2000-01-01T0:00:00", Err(UtcError::TimeError(TimeOfDayError::InvalidIsoString("0:00:00".to_string()))))]
324 #[case("2000-01-01-00:00:00", Err(UtcError::InvalidIsoString("2000-01-01-00:00:00".to_string())))]
325 #[case("2000-01-01T00:00:00 TAI", Err(UtcError::InvalidIsoString("2000-01-01T00:00:00 TAI".to_string())))]
326 fn test_utc_from_str(#[case] iso: &str, #[case] expected: Result<Utc, UtcError>) {
327 let actual: Result<Utc, UtcError> = iso.parse();
328 assert_eq!(actual, expected)
329 }
330}