Skip to main content

git2_time_chrono_ext/
git2_time_chrono_ext.rs

1use crate::Error;
2use chrono::TimeZone;
3
4/// An extension trait to convert [`git2::Time`] to [`chrono::DateTime`].
5/// # Examples
6/// ```no_run
7/// use git2_time_chrono_ext::Git2TimeChronoExt;
8///
9/// // Print `git2::Time` to `stdout`.
10/// fn print_git2_time(time: git2::Time) {
11///   println!("{}", time.to_local_date_time().unwrap());
12/// }
13///
14/// // Convert `git2::Time` to `Stirng` in the specified format.
15/// fn git2_time_to_string(time: git2::Time) -> String {
16///   time.to_local_date_time().unwrap().format("%Y-%m-%d %H:%M").to_string()
17/// }
18/// ```
19pub trait Git2TimeChronoExt {
20    /// Convert [`git2::Time`] to [`chrono::DateTime`]
21    /// retaining the original timezone.
22    /// # Examples
23    /// ```
24    /// use git2_time_chrono_ext::Git2TimeChronoExt;
25    ///
26    /// // The Eastern Hemisphere time zone.
27    /// let east_time = git2::Time::new(1745693791, 540);
28    /// let east_datetime = east_time.to_date_time();
29    /// assert!(east_datetime.is_ok());
30    /// assert_eq!(east_datetime.unwrap().to_string(), "2025-04-27 03:56:31 +09:00");
31    /// ```
32    /// ```
33    /// # use git2_time_chrono_ext::Git2TimeChronoExt;
34    /// // The Western Hemisphere time zone.
35    /// let west_time = git2::Time::new(1745196130, -420);
36    /// let west_datetime = west_time.to_date_time();
37    /// assert!(west_datetime.is_ok());
38    /// assert_eq!(west_datetime.unwrap().to_string(), "2025-04-20 17:42:10 -07:00");
39    /// ```
40    fn to_date_time(&self) -> Result<chrono::DateTime<chrono::FixedOffset>, Error>;
41
42    /// Convert [`git2::Time`] to [`chrono::DateTime`]
43    /// in the specified [`chrono::TimeZone`].
44    /// # Examples
45    /// ```
46    /// use git2_time_chrono_ext::Git2TimeChronoExt;
47    ///
48    /// let time = git2::Time::new(1745196130, -420);
49    /// let utc_datetime = time.to_date_time_in(&chrono::Utc);
50    /// assert_eq!(utc_datetime.unwrap().to_string(), "2025-04-21 00:42:10 UTC");
51    /// ```
52    fn to_date_time_in<Tz: chrono::TimeZone>(&self, tz: &Tz)
53    -> Result<chrono::DateTime<Tz>, Error>;
54
55    /// Convert [`git2::Time`] to [`chrono::DateTime`] in the local time zone.
56    /// This function is a shorthand of:
57    /// ```
58    /// # use git2_time_chrono_ext::Git2TimeChronoExt;
59    /// # fn to_local(time: git2::Time) -> Result<chrono::DateTime<chrono::Local>, git2_time_chrono_ext::Error> {
60    /// time.to_date_time_in(&chrono::Local)
61    /// # }
62    /// ```
63    fn to_local_date_time(&self) -> Result<chrono::DateTime<chrono::Local>, Error>;
64}
65
66impl Git2TimeChronoExt for git2::Time {
67    fn to_date_time(&self) -> Result<chrono::DateTime<chrono::FixedOffset>, Error> {
68        let Some(tz) = chrono::FixedOffset::east_opt(self.offset_minutes() * 60) else {
69            return Err(Error::InvalidTimeZone {
70                offset_minutes: self.offset_minutes(),
71            });
72        };
73        match tz.timestamp_opt(self.seconds(), 0) {
74            chrono::MappedLocalTime::Single(datetime) => Ok(datetime),
75            chrono::MappedLocalTime::Ambiguous(_, latest) => Ok(latest),
76            chrono::MappedLocalTime::None => Err(Error::TimeNotMappable { time: *self }),
77        }
78    }
79
80    fn to_date_time_in<Tz: chrono::TimeZone>(
81        &self,
82        tz: &Tz,
83    ) -> Result<chrono::DateTime<Tz>, Error> {
84        self.to_date_time()
85            .map(|datetime| datetime.with_timezone(tz))
86    }
87
88    fn to_local_date_time(&self) -> Result<chrono::DateTime<chrono::Local>, Error> {
89        self.to_date_time_in(&chrono::Local)
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn to_date_time_offset_invalid() {
99        let time = git2::Time::new(0, 100_000);
100        let result = time.to_date_time();
101        assert!(result.is_err());
102        let err = result.unwrap_err();
103        assert!(matches!(
104            err,
105            Error::InvalidTimeZone {
106                offset_minutes: 100_000
107            }
108        ));
109    }
110
111    #[test]
112    fn to_date_time_not_mappable() {
113        let time = git2::Time::new(i64::MAX, 0);
114        let result = time.to_date_time();
115        assert!(result.is_err());
116        let err = result.unwrap_err();
117        assert!(matches!(err, Error::TimeNotMappable { .. }));
118    }
119}