cftime_rs/
datetime.rs

1/// Wrapper for all the different datetime and calendars
2use crate::datetimes::all_leap::AllLeapDatetime;
3use crate::datetimes::day_360::Day360Datetime;
4use crate::datetimes::julian::JulianDatetime;
5use crate::datetimes::no_leap::NoLeapDatetime;
6use crate::datetimes::proleptic_gregorian::ProlepticGregorianDatetime;
7use crate::datetimes::standard::StandardDatetime;
8use crate::datetimes::traits::CalendarDatetime;
9use crate::datetimes::traits::CalendarDatetimeCreator;
10use crate::duration::CFDuration;
11
12use crate::utils::normalize_nanoseconds;
13use crate::{calendars::Calendar, constants};
14
15/// Represents a calendar CF datetime.
16///
17/// Internally it uses the timestamp in seconds representation
18/// With this implementation it is faster to add a duration to a CFDatetime
19/// However if we ask for a representation of the date calling the format method
20/// it will calculate the year, month, day, hour, minute, second and nanoseconds
21/// from timestamp which can make print a bit slow
22///
23/// # Examples
24///
25/// ## Creating a datetime
26///
27/// ```rust
28/// let cf_datetime = CFDatetime::from_ymd_hms(1970, 1, 1, 0, 0, 0, Calendar::Standard).unwrap();
29/// // Computation of the timestamp
30/// assert_eq!(cf_datetime.timestamp(), 0);
31/// // Idempotence
32/// assert_eq!(cf_datetime.ymd_hms().unwrap(), (1970, 1, 1, 0, 0, 0));
33/// ```
34/// ## Duration between two datetimes
35/// ```rust
36/// let cf_datetime_1 = CFDatetime::from_ymd_hms(1970, 1, 1, 0, 0, 0, Calendar::Standard).unwrap();
37/// let cf_datetime_2 = CFDatetime::from_ymd_hms(1970, 1, 2, 0, 0, 0, Calendar::Standard).unwrap();
38/// let duration = cf_datetime_2 - cf_datetime_1;
39/// assert_eq!(duration.num_days, 1);
40/// ```
41///
42pub struct CFDatetime {
43    inner: Box<dyn CalendarDatetime + Send + Sync>,
44}
45
46/// Immplementation of the CF convention specifications :
47/// - [CF Conventions](https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#time-coordinate)
48impl CFDatetime {
49    /// Returns the calendar
50    pub fn calendar(&self) -> Calendar {
51        self.inner.calendar()
52    }
53    /// Returns the timestamp
54    pub fn timestamp(&self) -> i64 {
55        self.inner.timestamp()
56    }
57
58    /// Returns the year, month, and day of the date.
59    ///
60    /// # Returns
61    ///
62    /// A Result containing a tuple containing the hour, minute, second as `(i64, u8, u8)`.
63    /// or an error of type `crate::errors::Error::InvalidDate` if the date cannot be computed from the timestamp.
64    pub fn ymd(&self) -> Result<(i64, u8, u8), crate::errors::Error> {
65        let (year, month, day, _, _, _) = self.ymd_hms()?;
66
67        Ok((year, month, day))
68    }
69    /// Returns the hour, minute, second of the date.
70    ///
71    /// # Returns
72    ///
73    /// A Result containing a tuple containing the hour, minute, second as `(i64, u8, u8)`.
74    /// or an error of type `crate::errors::Error::InvalidDate` if the date cannot be computed from the timestamp.
75    ///
76    /// hms needs to first compute the date to see if the date is impossible
77    pub fn hms(&self) -> Result<(u8, u8, u8), crate::errors::Error> {
78        let (_, _, _, hour, min, sec) = self.ymd_hms()?;
79        Ok((hour, min, sec))
80    }
81    /// Returns the year, month,  day, hour, minute, second of the date.
82    ///
83    /// # Returns
84    ///
85    /// A Result containing a tuple containing the year, month,  day, hour, minute, second  as
86    /// `(i64, u8, u8, u8, u8, u8)` or an error of type `crate::errors::Error::InvalidDate` if
87    /// the date cannot be computed from the timestamp.
88    pub fn ymd_hms(&self) -> Result<(i64, u8, u8, u8, u8, u8), crate::errors::Error> {
89        self.inner.ymd_hms()
90    }
91    /// Creates a new CFDatetime from the given year, month, day, hour, minute, second, and calendar.
92    ///
93    /// # Returns
94    ///
95    ///  A Result containing a new CFDatetime or an error of type `crate::errors::Error::InvalidDate` if
96    /// the date is not valid in the calendar
97    pub fn from_ymd_hms(
98        year: i64,
99        month: u8,
100        day: u8,
101        hour: u8,
102        minute: u8,
103        second: f32,
104        calendar: Calendar,
105    ) -> Result<Self, crate::errors::Error> {
106        match calendar {
107            Calendar::ProlepticGregorian => Ok(Self {
108                inner: Box::new(ProlepticGregorianDatetime::from_ymd_hms(
109                    year, month, day, hour, minute, second,
110                )?),
111            }),
112            Calendar::Standard => Ok(Self {
113                inner: Box::new(StandardDatetime::from_ymd_hms(
114                    year, month, day, hour, minute, second,
115                )?),
116            }),
117            Calendar::Day360 => Ok(Self {
118                inner: Box::new(Day360Datetime::from_ymd_hms(
119                    year, month, day, hour, minute, second,
120                )?),
121            }),
122            Calendar::Julian => Ok(Self {
123                inner: Box::new(JulianDatetime::from_ymd_hms(
124                    year, month, day, hour, minute, second,
125                )?),
126            }),
127            Calendar::NoLeap => Ok(Self {
128                inner: Box::new(NoLeapDatetime::from_ymd_hms(
129                    year, month, day, hour, minute, second,
130                )?),
131            }),
132            Calendar::AllLeap => Ok(Self {
133                inner: Box::new(AllLeapDatetime::from_ymd_hms(
134                    year, month, day, hour, minute, second,
135                )?),
136            }),
137        }
138    }
139
140    /// Creates a new CFDatetime from the given hour, minute, second, and calendar.
141    /// It sets the year, month, day to 1970, 1, 1
142    ///
143    /// # Returns
144    ///
145    /// A Result containing a new CFDatetime or an error of type `crate::errors::Error::InvalidDate` if
146    /// the date is not valid in the calendar
147    pub fn from_hms(
148        hour: u8,
149        minute: u8,
150        second: f32,
151        calendar: Calendar,
152    ) -> Result<Self, crate::errors::Error> {
153        Self::from_ymd_hms(
154            constants::UNIX_DEFAULT_YEAR,
155            constants::UNIX_DEFAULT_MONTH,
156            constants::UNIX_DEFAULT_DAY,
157            hour,
158            minute,
159            second,
160            calendar,
161        )
162    }
163    /// Creates a new CFDatetime from the given year, month, day and calendar.
164    /// It sets the hour, minute, second to 1970, 1, 1
165    ///
166    /// # Returns
167    ///
168    /// A Result containing a new CFDatetime or an error of type `crate::errors::Error::InvalidDate` if
169    /// the date is not valid in the calendar
170    pub fn from_ymd(
171        year: i64,
172        month: u8,
173        day: u8,
174        calendar: Calendar,
175    ) -> Result<Self, crate::errors::Error> {
176        Self::from_ymd_hms(year, month, day, 0, 0, 0.0, calendar)
177    }
178    /// Creates a new CFDatetime from a given timestamp and calendar atrting from the epoch
179    ///
180    /// # Returns
181    ///
182    /// A Result containing a new CFDatetime or an error of type `crate::errors::Error::InvalidDate` if
183    /// the date is not valid in the calendar
184    pub fn from_timestamp(
185        timestamp: i64,
186        nanoseconds: u32,
187        calendar: Calendar,
188    ) -> Result<Self, crate::errors::Error> {
189        match calendar {
190            Calendar::ProlepticGregorian => Ok(Self {
191                inner: Box::new(ProlepticGregorianDatetime::from_timestamp(
192                    timestamp,
193                    nanoseconds,
194                )),
195            }),
196            Calendar::Standard => Ok(Self {
197                inner: Box::new(StandardDatetime::from_timestamp(timestamp, nanoseconds)),
198            }),
199            Calendar::Day360 => Ok(Self {
200                inner: Box::new(Day360Datetime::from_timestamp(timestamp, nanoseconds)),
201            }),
202            Calendar::Julian => Ok(Self {
203                inner: Box::new(JulianDatetime::from_timestamp(timestamp, nanoseconds)),
204            }),
205            Calendar::NoLeap => Ok(Self {
206                inner: Box::new(NoLeapDatetime::from_timestamp(timestamp, nanoseconds)),
207            }),
208            Calendar::AllLeap => Ok(Self {
209                inner: Box::new(AllLeapDatetime::from_timestamp(timestamp, nanoseconds)),
210            }),
211        }
212    }
213
214    /// Returns the hours of the date.
215    pub fn hours(&self) -> Result<u8, crate::errors::Error> {
216        let (hour, _, _) = self.hms()?;
217        Ok(hour)
218    }
219    /// Returns the minutes of the date.
220    pub fn minutes(&self) -> Result<u8, crate::errors::Error> {
221        let (_, min, _) = self.hms()?;
222        Ok(min)
223    }
224    /// Returns the seconds of the date.
225    pub fn seconds(&self) -> Result<u8, crate::errors::Error> {
226        let (_, _, sec) = self.hms()?;
227        Ok(sec)
228    }
229    /// Returns the nanoseconds of the date.
230    pub fn nanoseconds(&self) -> u32 {
231        self.inner.nanoseconds()
232    }
233    /// Change the calendar of the CFDatetime.
234    ///
235    /// It get the year, month, day, hour, minute, second and nanoseconds by calling the [Self::ymd_hms]
236    /// method and then call the [Self::from_ymd_hms] method with the new calendar. This can be considered
237    /// as safe
238    ///
239    /// # Returns
240    /// A Result containing a new CFDatetime or an error of type `crate::errors::Error::InvalidDate` if
241    /// the date is not valid in the calendar
242    pub fn change_calendar(&self, calendar: Calendar) -> Result<Self, crate::errors::Error> {
243        let (year, month, day, hour, minute, second) = self.ymd_hms()?;
244        let ns = self.nanoseconds();
245        let f_second = second as f32 + ns as f32 / 1e9;
246        Self::from_ymd_hms(year, month, day, hour, minute, f_second, calendar)
247    }
248    /// Change the calendar of the CFDatetime using the timestamp
249    ///
250    /// It get the year, month, day, hour, minute, second and nanoseconds by calling the [Self::timestamp]
251    /// method and then call the [Self::from_timestamp] method with the new calendar.
252    ///
253    /// Be aware that there is highly chance that the two dates do not correspond.
254    /// However their distances from epoch are the same.
255    ///
256    /// # Returns
257    /// A Result containing a new CFDatetime or an error of type `crate::errors::Error::InvalidDate` if
258    /// the date is not valid in the calendar
259    pub fn change_calendar_from_timestamp(
260        &self,
261        calendar: Calendar,
262    ) -> Result<Self, crate::errors::Error> {
263        let timestamp = self.timestamp();
264        let nanoseconds = self.nanoseconds();
265        Self::from_timestamp(timestamp, nanoseconds, calendar)
266    }
267}
268
269impl PartialEq for CFDatetime {
270    fn eq(&self, other: &Self) -> bool {
271        self.calendar() == other.calendar()
272            && self.timestamp() == other.timestamp()
273            && self.nanoseconds() == other.nanoseconds()
274    }
275}
276/// Display a CFDatetime with the following format : `YYYY-MM-DD HH:MM:SS.SSS`
277impl std::fmt::Display for CFDatetime {
278    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
279        let nanoseconds = self.nanoseconds() as f64 / 1_000_000_000.;
280        match self.ymd_hms() {
281            Ok((year, month, day, hour, minute, second)) => {
282                write!(
283                    f,
284                    "{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}",
285                    year, month, day, hour, minute, second, nanoseconds,
286                )
287            }
288            Err(err) => {
289                write!(f, "{:?}", err)
290            }
291        }
292    }
293}
294
295macro_rules! impl_add_duration {
296    ($rhs:ty, $for:ty) => {
297        impl std::ops::Add<$rhs> for $for {
298            type Output = Result<CFDatetime, crate::errors::Error>;
299            fn add(self, rhs: $rhs) -> Self::Output {
300                if self.calendar() != rhs.calendar() {
301                    return Err(crate::errors::Error::DifferentCalendars(
302                        self.calendar().to_string(),
303                        rhs.calendar().to_string(),
304                    ));
305                }
306                let nanoseconds = self.nanoseconds() as i64 + rhs.nanoseconds as i64;
307                let (_remaining_seconds, remaining_nanoseconds) =
308                    normalize_nanoseconds(nanoseconds);
309                let new_timestamp = self.timestamp() + rhs.seconds;
310                CFDatetime::from_timestamp(new_timestamp, remaining_nanoseconds, self.calendar())
311            }
312        }
313    };
314}
315impl_add_duration!(CFDuration, CFDatetime);
316impl_add_duration!(&CFDuration, CFDatetime);
317impl_add_duration!(CFDuration, &CFDatetime);
318impl_add_duration!(&CFDuration, &CFDatetime);
319
320macro_rules! impl_sub_duration {
321    ($rhs:ty, $for:ty) => {
322        impl std::ops::Sub<$rhs> for $for {
323            type Output = Result<CFDatetime, crate::errors::Error>;
324            fn sub(self, rhs: $rhs) -> Self::Output {
325                if self.calendar() != rhs.calendar() {
326                    return Err(crate::errors::Error::DifferentCalendars(
327                        self.calendar().to_string(),
328                        rhs.calendar().to_string(),
329                    ));
330                }
331                let nanoseconds = self.nanoseconds() as i64 - rhs.nanoseconds as i64;
332                let (remaining_seconds, remaining_nanoseconds) = normalize_nanoseconds(nanoseconds);
333                let new_timestamp = (self.timestamp() - rhs.seconds) + remaining_seconds;
334                CFDatetime::from_timestamp(new_timestamp, remaining_nanoseconds, self.calendar())
335            }
336        }
337    };
338}
339impl_sub_duration!(CFDuration, CFDatetime);
340impl_sub_duration!(&CFDuration, CFDatetime);
341impl_sub_duration!(CFDuration, &CFDatetime);
342impl_sub_duration!(&CFDuration, &CFDatetime);
343
344macro_rules! impl_sub_datetime {
345    ($rhs:ty, $for:ty) => {
346        impl std::ops::Sub<$rhs> for $for {
347            type Output = Result<CFDuration, crate::errors::Error>;
348            fn sub(self, rhs: $rhs) -> Self::Output {
349                if self.calendar() != rhs.calendar() {
350                    return Err(crate::errors::Error::DifferentCalendars(
351                        self.calendar().to_string(),
352                        rhs.calendar().to_string(),
353                    ));
354                }
355                let nanoseconds = self.nanoseconds() as i64 - rhs.nanoseconds() as i64;
356                let new_timestamp = self.timestamp() - rhs.timestamp();
357                Ok(CFDuration::new(new_timestamp, nanoseconds, self.calendar()))
358            }
359        }
360    };
361}
362
363impl_sub_datetime!(CFDatetime, CFDatetime);
364impl_sub_datetime!(&CFDatetime, CFDatetime);
365impl_sub_datetime!(CFDatetime, &CFDatetime);
366impl_sub_datetime!(&CFDatetime, &CFDatetime);
367
368#[cfg(test)]
369mod tests {
370    use crate::calendars;
371
372    use super::*;
373    #[test]
374    fn test_timestamp_zero_standard() {
375        let (year, month, day) = (1970, 1, 1);
376        let d = CFDatetime::from_ymd(year, month, day, Calendar::Standard).unwrap();
377        assert_eq!(0, d.timestamp());
378    }
379    #[test]
380    fn test_timestamp_20230101_standard() {
381        let (year, month, day) = (2023, 1, 1);
382        let d = CFDatetime::from_ymd(year, month, day, Calendar::Standard).unwrap();
383        assert_eq!(1672531200, d.timestamp());
384    }
385    #[test]
386    fn test_impossible_date_gregorian() {
387        let (year, month, day) = (1582, 10, 8);
388        let d = CFDatetime::from_ymd(year, month, day, Calendar::Standard);
389        assert!(d.is_err());
390    }
391    #[test]
392    fn test_timestamp_minus_one_all_calendars() {
393        let cals = vec![
394            calendars::Calendar::Standard,
395            calendars::Calendar::ProlepticGregorian,
396            calendars::Calendar::Julian,
397            calendars::Calendar::NoLeap,
398            calendars::Calendar::AllLeap,
399        ];
400        for cal in cals {
401            let d = CFDatetime::from_timestamp(-1, 0, cal);
402            assert_eq!((1969, 12, 31, 23, 59, 59), d.unwrap().ymd_hms().unwrap());
403        }
404    }
405    #[test]
406    fn test_timestamp_limit_gregorian_julian() {
407        // -12219206400 == 1582, 10, 15 in Gregorian calendar
408        let lower_limit_gregorian = CFDatetime::from_timestamp(-12219292800, 0, Calendar::Standard);
409        let upper_limit_julian = CFDatetime::from_timestamp(-12219292801, 0, Calendar::Standard);
410        assert_eq!(
411            lower_limit_gregorian.unwrap().ymd_hms().unwrap(),
412            (1582, 10, 15, 0, 0, 0)
413        );
414        assert_eq!(
415            upper_limit_julian.unwrap().ymd_hms().unwrap(),
416            (1582, 10, 4, 23, 59, 59)
417        );
418    }
419    #[test]
420    fn test_idempotence_all_calendars() {
421        let dates = vec![
422            (1970, 1, 1),
423            (1972, 1, 1), // leap year
424            (1980, 1, 1),
425            (2020, 1, 1),
426            (100_000, 1, 1),
427            (1980, 6, 15),
428            (1969, 1, 1),
429            (-1_000_000, 1, 1),
430            (-100_000, 1, 1),
431            (1960, 1, 1), // leap year
432            (1980, 6, 15),
433            // Encountered issue for 2001-01-03
434            (2001, 1, 3),
435        ];
436        let cals = vec![
437            calendars::Calendar::Day360,
438            calendars::Calendar::Standard,
439            calendars::Calendar::ProlepticGregorian,
440            calendars::Calendar::Julian,
441            calendars::Calendar::NoLeap,
442            calendars::Calendar::AllLeap,
443        ];
444        for cal in cals {
445            for date in dates.clone() {
446                let (year, month, day) = date;
447                let datetime: CFDatetime = CFDatetime::from_ymd(year, month, day, cal).unwrap();
448                let (expected_year, expected_month, expected_day) = datetime.ymd().unwrap();
449                assert_eq!(expected_year, year);
450                assert_eq!(expected_month, month);
451                assert_eq!(expected_day, day);
452            }
453        }
454    }
455    #[test]
456    fn test_add_duration() {
457        let cals = vec![
458            calendars::Calendar::Day360,
459            calendars::Calendar::Standard,
460            calendars::Calendar::ProlepticGregorian,
461            calendars::Calendar::Julian,
462            calendars::Calendar::NoLeap,
463            calendars::Calendar::AllLeap,
464        ];
465        for calendar in cals {
466            let duration_expected = vec![
467                (CFDuration::from_hours(1, calendar), (1970, 1, 1, 1, 0, 0)),
468                (CFDuration::from_minutes(1, calendar), (1970, 1, 1, 0, 1, 0)),
469                (CFDuration::from_seconds(1, calendar), (1970, 1, 1, 0, 0, 1)),
470                (CFDuration::from_days(1, calendar), (1970, 1, 2, 0, 0, 0)),
471            ];
472            for (duration, expected) in duration_expected {
473                let datetime = CFDatetime::from_ymd(1970, 1, 1, calendar).unwrap();
474                let new_datetime = datetime + duration;
475                assert_eq!(new_datetime.unwrap().ymd_hms().unwrap(), expected);
476            }
477        }
478    }
479    #[test]
480    fn test_timestamp() {
481        let timestamp_expected = vec![
482            (0, (1970, 1, 1, 0, 0, 0)),
483            (315532800, (1980, 1, 1, 0, 0, 0)),
484            (631152000, (1990, 1, 1, 0, 0, 0)),
485            (946684800, (2000, 1, 1, 0, 0, 0)),
486            (949363200, (2000, 2, 1, 0, 0, 0)),
487            (957139200, (2000, 5, 1, 0, 0, 0)),
488            (946771200, (2000, 1, 2, 0, 0, 0)),
489            // Encountered issue for this one :
490            (946857600, (2000, 1, 3, 0, 0, 0)),
491        ];
492        for (timestamp, expected) in timestamp_expected {
493            let datetime = CFDatetime::from_timestamp(timestamp, 0, calendars::Calendar::Standard);
494            assert_eq!(datetime.unwrap().ymd_hms().unwrap(), expected);
495        }
496    }
497
498    #[test]
499    fn test_360_day_from_timestamp() {
500        let timestamp_expected: Vec<(i64, (i64, u8, u8))> = vec![
501            (-61236000000, (1, 4, 1)),
502            (-61235913600, (1, 4, 2)),
503            (-61235827200, (1, 4, 3)),
504            (-61235740800, (1, 4, 4)),
505            (-61235654400, (1, 4, 5)),
506            (-61235568000, (1, 4, 6)),
507        ];
508        for (timestamp, expected) in timestamp_expected {
509            let datetime = CFDatetime::from_timestamp(timestamp, 0, calendars::Calendar::Day360);
510            assert_eq!(datetime.unwrap().ymd().unwrap(), expected);
511        }
512    }
513}