Skip to main content

irox_time/
datetime.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2023 IROX Contributors
3
4//!
5//! Contains [`UTCDateTime`] and associated elements to represent a [`Date`] and [`Time`] in UTC
6//!
7
8extern crate alloc;
9use crate::epoch::{UnixTimestamp, UNIX_EPOCH};
10use crate::format::iso8601::{ISO8601Format, BASIC_DATE_TIME_OF_DAY, ISO8601_DATE_TIME};
11use crate::format::{Format, FormatError, FormatParser};
12use crate::gregorian::Date;
13use crate::julian::JulianDate;
14use crate::Time;
15pub use alloc::string::String;
16use core::fmt::{Display, Formatter};
17use core::ops::{Add, AddAssign, Sub};
18use irox_tools::cfg_feature_serde;
19use irox_units::bounds::GreaterThanEqualToValueError;
20use irox_units::units::duration::Duration;
21
22///
23/// Represents a Gregorian Date and Time in UTC
24#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
25pub struct UTCDateTime {
26    pub(crate) date: Date,
27    pub(crate) time: Time,
28}
29
30impl Display for UTCDateTime {
31    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
32        f.write_fmt(format_args!("{}", self.format(&BASIC_DATE_TIME_OF_DAY)))
33    }
34}
35
36impl UTCDateTime {
37    ///
38    /// New UTC Date and Time
39    #[must_use]
40    pub fn new(date: Date, time: Time) -> UTCDateTime {
41        UTCDateTime { date, time }
42    }
43
44    ///
45    /// New UTC Date and Time from the specified values
46    pub fn try_from_values(
47        year: i32,
48        month: u8,
49        day: u8,
50        hour: u8,
51        minute: u8,
52        seconds: u8,
53    ) -> Result<UTCDateTime, GreaterThanEqualToValueError<u8>> {
54        let date = Date::try_from_values(year, month, day)?;
55        let time = Time::from_hms(hour, minute, seconds)?;
56        Ok(UTCDateTime::new(date, time))
57    }
58
59    ///
60    /// New UTC date and Time from the specified values (fractional seconds)
61    pub fn try_from_values_f64(
62        year: i32,
63        month: u8,
64        day: u8,
65        hour: u8,
66        minute: u8,
67        seconds: f64,
68    ) -> Result<UTCDateTime, GreaterThanEqualToValueError<f64>> {
69        let date = Date::try_from_values(year, month, day)?;
70        let time = Time::from_hms_f64(hour, minute, seconds)?;
71        Ok(UTCDateTime::new(date, time))
72    }
73
74    ///
75    /// Returns the Gregorian Date portion of this UTCDateTime
76    #[must_use]
77    pub fn get_date(&self) -> Date {
78        self.date
79    }
80
81    ///
82    /// Returns the Time portion of this UTCDateTime
83    #[must_use]
84    pub fn get_time(&self) -> Time {
85        self.time
86    }
87
88    ///
89    /// Returns the current instant in time as reported by the local system
90    /// clock.
91    #[must_use]
92    #[cfg(feature = "std")]
93    pub fn now() -> UTCDateTime {
94        UnixTimestamp::now().into()
95    }
96
97    #[must_use]
98    pub fn format<T: Format<UTCDateTime>>(&self, format: &T) -> String {
99        format.format(self)
100    }
101
102    /// Formats this date as a extended ISO8601 Date & Time, `2023-12-31T05:10:25Z`
103    #[must_use]
104    pub fn format_iso8601_extended(&self) -> String {
105        ISO8601_DATE_TIME.format(self)
106    }
107    /// Formats this date as a basic ISO8601 Date & Time, `20231231T051025Z`, suitable for filenames
108    #[must_use]
109    pub fn format_iso8601_basic(&self) -> String {
110        BASIC_DATE_TIME_OF_DAY.format(self)
111    }
112    /// Attempts to parse the provided string as either a [`crate::format::iso8601::BasicDateTimeOfDay`] or a [`crate::format::iso8601::ExtendedDateTimeFormat`]
113    pub fn try_from_iso8601(val: &str) -> Result<Self, FormatError> {
114        ISO8601_DATE_TIME.try_from(val)
115    }
116}
117
118impl ISO8601Format for UTCDateTime {
119    fn format_iso8601_extended(&self) -> String {
120        UTCDateTime::format_iso8601_extended(self)
121    }
122
123    fn format_iso8601_basic(&self) -> String {
124        UTCDateTime::format_iso8601_basic(self)
125    }
126
127    fn try_from_iso8601(val: &str) -> Result<Self, FormatError>
128    where
129        Self: Sized,
130    {
131        UTCDateTime::try_from_iso8601(val)
132    }
133}
134
135impl From<&UnixTimestamp> for UTCDateTime {
136    fn from(value: &UnixTimestamp) -> Self {
137        let date = value.as_date();
138        let remaining_seconds = value.get_offset().as_seconds_f64()
139            - date.as_unix_timestamp().get_offset().as_seconds_f64();
140
141        let time = Time::from_seconds_f64(remaining_seconds).unwrap_or_default();
142
143        UTCDateTime { date, time }
144    }
145}
146impl From<UnixTimestamp> for UTCDateTime {
147    fn from(value: UnixTimestamp) -> Self {
148        let date = value.as_date();
149        let remaining_seconds = value.get_offset().as_seconds_f64()
150            - date.as_unix_timestamp().get_offset().as_seconds_f64();
151
152        let time = Time::from_seconds_f64(remaining_seconds).unwrap_or_default();
153
154        UTCDateTime { date, time }
155    }
156}
157
158impl From<UTCDateTime> for UnixTimestamp {
159    fn from(value: UTCDateTime) -> Self {
160        let mut date_dur = value.date - UNIX_EPOCH.get_gregorian_date();
161        date_dur += Into::<Duration>::into(value.time);
162        Self::from_offset(date_dur)
163    }
164}
165impl From<&UTCDateTime> for UnixTimestamp {
166    fn from(value: &UTCDateTime) -> Self {
167        let mut date_dur = value.date - UNIX_EPOCH.get_gregorian_date();
168        date_dur += Into::<Duration>::into(value.time);
169        Self::from_offset(date_dur)
170    }
171}
172
173impl From<UTCDateTime> for JulianDate {
174    fn from(value: UTCDateTime) -> Self {
175        let mut date: JulianDate = value.date.into();
176        let time: Duration = value.time.into();
177        date += time;
178        date
179    }
180}
181
182impl From<&UTCDateTime> for JulianDate {
183    fn from(value: &UTCDateTime) -> Self {
184        let mut date: JulianDate = value.date.into();
185        let time: Duration = value.time.into();
186        date += time;
187        date
188    }
189}
190
191impl Sub<Self> for UTCDateTime {
192    type Output = Duration;
193
194    fn sub(self, rhs: Self) -> Self::Output {
195        let ts1: JulianDate = self.into();
196        let ts2: JulianDate = rhs.into();
197
198        ts1 - ts2
199    }
200}
201impl Sub<&Self> for UTCDateTime {
202    type Output = Duration;
203
204    fn sub(self, rhs: &Self) -> Self::Output {
205        let ts1: JulianDate = self.into();
206        let ts2: JulianDate = rhs.into();
207
208        ts1 - ts2
209    }
210}
211
212impl Add<Duration> for UTCDateTime {
213    type Output = UTCDateTime;
214
215    fn add(self, rhs: Duration) -> Self::Output {
216        let (time, excess) = self.time.wrapping_add(rhs);
217        let date = self.date + excess;
218        UTCDateTime { date, time }
219    }
220}
221impl Add<&Duration> for UTCDateTime {
222    type Output = UTCDateTime;
223
224    fn add(self, rhs: &Duration) -> Self::Output {
225        let (time, excess) = self.time.wrapping_add(*rhs);
226        let date = self.date + excess;
227        UTCDateTime { date, time }
228    }
229}
230
231impl AddAssign<Duration> for UTCDateTime {
232    fn add_assign(&mut self, rhs: Duration) {
233        let (time, excess) = self.time.wrapping_add(rhs);
234        self.time = time;
235        self.date += excess;
236    }
237}
238impl AddAssign<&Duration> for UTCDateTime {
239    fn add_assign(&mut self, rhs: &Duration) {
240        let (time, excess) = self.time.wrapping_add(*rhs);
241        self.time = time;
242        self.date += excess;
243    }
244}
245
246cfg_feature_serde! {
247    struct UTCDateTimeVisitor;
248    impl serde::de::Visitor<'_> for UTCDateTimeVisitor {
249        type Value = UTCDateTime;
250
251        fn expecting(&self, fmt: &mut Formatter<'_>) -> Result<(), core::fmt::Error> {
252            write!(fmt, "The visitor expects to receive a string formatted as a ISO 8601 DateTime")
253        }
254        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> where E: serde::de::Error {
255            UTCDateTime::try_from_iso8601(v).map_err(serde::de::Error::custom)
256        }
257    }
258    impl<'de> serde::Deserialize<'de> for UTCDateTime {
259        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de> {
260            deserializer.deserialize_str(UTCDateTimeVisitor)
261        }
262    }
263    impl serde::Serialize for UTCDateTime {
264        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer {
265            serializer.serialize_str(&self.format_iso8601_extended())
266        }
267    }
268}
269
270#[cfg(all(test, feature = "serde", feature = "std"))]
271mod tests {
272    use crate::datetime::UTCDateTime;
273    use crate::epoch::UnixTimestamp;
274    use irox_units::units::duration::Duration;
275
276    #[test]
277    #[cfg(all(feature = "serde", feature = "std"))]
278    pub fn serde_test() -> Result<(), crate::FormatError> {
279        #[derive(serde::Serialize, serde::Deserialize, Eq, PartialEq, Debug)]
280        struct Test {
281            a: UTCDateTime,
282        }
283        impl Default for Test {
284            fn default() -> Self {
285                Self {
286                    a: UTCDateTime::default(),
287                }
288            }
289        }
290        let a = Test {
291            a: UnixTimestamp::from_offset(Duration::from_days(1)).into(),
292        };
293        let s = serde_json::to_string(&a).unwrap_or_default();
294        assert_eq!(s, "{\"a\":\"1970-01-02T00:00:00Z\"}");
295        let b: Test = serde_json::from_str(&s).unwrap();
296        assert_eq!(a, b);
297        Ok(())
298    }
299}