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::{Error, 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///
19/// // Convert `git2::Time` to ISO 8601 (RFC 3339) string.
20/// fn git2_time_to_rfc3339(time: &git2::Time) -> Result<String, Error> {
21///   Ok(time.to_date_time_in(&chrono::Utc)?.to_rfc3339())
22/// }
23/// ```
24pub trait Git2TimeChronoExt {
25    /// Convert [`git2::Time`] to [`chrono::DateTime`]
26    /// retaining the original timezone.
27    /// # Examples
28    /// ```
29    /// use git2_time_chrono_ext::Git2TimeChronoExt;
30    ///
31    /// // The Eastern Hemisphere time zone.
32    /// let east_time = git2::Time::new(1745693791, 540);
33    /// let east_datetime = east_time.to_date_time();
34    /// assert!(east_datetime.is_ok());
35    /// assert_eq!(east_datetime.unwrap().to_string(), "2025-04-27 03:56:31 +09:00");
36    /// ```
37    /// ```
38    /// # use git2_time_chrono_ext::Git2TimeChronoExt;
39    /// // The Western Hemisphere time zone.
40    /// let west_time = git2::Time::new(1745196130, -420);
41    /// let west_datetime = west_time.to_date_time();
42    /// assert!(west_datetime.is_ok());
43    /// assert_eq!(west_datetime.unwrap().to_string(), "2025-04-20 17:42:10 -07:00");
44    /// ```
45    fn to_date_time(&self) -> Result<chrono::DateTime<chrono::FixedOffset>, Error>;
46
47    /// Convert [`git2::Time`] to [`chrono::DateTime`]
48    /// in the specified [`chrono::TimeZone`].
49    /// # Examples
50    /// ```
51    /// use git2_time_chrono_ext::Git2TimeChronoExt;
52    ///
53    /// let time = git2::Time::new(1745196130, -420);
54    /// let utc_datetime = time.to_date_time_in(&chrono::Utc);
55    /// assert_eq!(utc_datetime.unwrap().to_string(), "2025-04-21 00:42:10 UTC");
56    /// ```
57    fn to_date_time_in<Tz: chrono::TimeZone>(&self, tz: &Tz)
58    -> Result<chrono::DateTime<Tz>, Error>;
59
60    /// Convert [`git2::Time`] to [`chrono::DateTime`] in the local time zone.
61    /// This function is a shorthand of:
62    /// ```
63    /// # use git2_time_chrono_ext::Git2TimeChronoExt;
64    /// # fn to_local(time: git2::Time) -> Result<chrono::DateTime<chrono::Local>, git2_time_chrono_ext::Error> {
65    /// time.to_date_time_in(&chrono::Local)
66    /// # }
67    /// ```
68    fn to_local_date_time(&self) -> Result<chrono::DateTime<chrono::Local>, Error>;
69
70    /// [`to_date_time`][Git2TimeChronoExt::to_date_time] returns
71    /// the latest time when the given time is ambiguous.
72    ///
73    /// This function is useful when you want to handle ambiguous time.
74    /// Please see [`chrono::MappedLocalTime`] for more details.
75    /// # Examples
76    /// ```
77    /// use git2_time_chrono_ext::Git2TimeChronoExt;
78    /// use chrono::MappedLocalTime;
79    ///
80    /// let time = git2::Time::new(1745196130, -420);
81    /// let mapped = time.to_date_time_opt().unwrap();
82    /// if let MappedLocalTime::Single(datetime) = mapped {
83    ///     assert_eq!(datetime.to_string(), "2025-04-20 17:42:10 -07:00");
84    /// } else {
85    ///     panic!("should be Single");
86    /// }
87    /// ```
88    fn to_date_time_opt(
89        &self,
90    ) -> Result<chrono::MappedLocalTime<chrono::DateTime<chrono::FixedOffset>>, Error>;
91}
92
93impl Git2TimeChronoExt for git2::Time {
94    fn to_date_time_opt(
95        &self,
96    ) -> Result<chrono::MappedLocalTime<chrono::DateTime<chrono::FixedOffset>>, Error> {
97        let Some(tz) = chrono::FixedOffset::east_opt(self.offset_minutes() * 60) else {
98            return Err(Error::InvalidTimeZone {
99                offset_minutes: self.offset_minutes(),
100            });
101        };
102        Ok(tz.timestamp_opt(self.seconds(), 0))
103    }
104
105    fn to_date_time(&self) -> Result<chrono::DateTime<chrono::FixedOffset>, Error> {
106        match self.to_date_time_opt()? {
107            chrono::MappedLocalTime::Single(datetime) => Ok(datetime),
108            chrono::MappedLocalTime::Ambiguous(_, latest) => Ok(latest),
109            chrono::MappedLocalTime::None => Err(Error::TimeNotMappable { time: *self }),
110        }
111    }
112
113    fn to_date_time_in<Tz: chrono::TimeZone>(
114        &self,
115        tz: &Tz,
116    ) -> Result<chrono::DateTime<Tz>, Error> {
117        self.to_date_time()
118            .map(|datetime| datetime.with_timezone(tz))
119    }
120
121    fn to_local_date_time(&self) -> Result<chrono::DateTime<chrono::Local>, Error> {
122        self.to_date_time_in(&chrono::Local)
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn to_date_time_offset_invalid() {
132        let time = git2::Time::new(0, 100_000);
133        let result = time.to_date_time();
134        assert!(result.is_err());
135        let err = result.unwrap_err();
136        assert!(matches!(
137            err,
138            Error::InvalidTimeZone {
139                offset_minutes: 100_000
140            }
141        ));
142    }
143
144    #[test]
145    fn to_date_time_not_mappable() {
146        let time = git2::Time::new(i64::MAX, 0);
147        let result = time.to_date_time();
148        assert!(result.is_err());
149        let err = result.unwrap_err();
150        assert!(matches!(err, Error::TimeNotMappable { .. }));
151    }
152
153    #[test]
154    fn to_date_time_opt_none() {
155        use chrono::MappedLocalTime;
156        let time = git2::Time::new(i64::MAX, 0);
157        let mapped = time.to_date_time_opt().unwrap();
158        assert!(matches!(mapped, MappedLocalTime::None));
159    }
160}