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