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 /// This function is useful when you want to handle ambiguous time.
73 /// Please see [`chrono::MappedLocalTime`] for more details.
74 /// # Examples
75 /// ```
76 /// use git2_time_chrono_ext::Git2TimeChronoExt;
77 /// use chrono::MappedLocalTime;
78 ///
79 /// let time = git2::Time::new(1745196130, -420);
80 /// let mapped = time.to_date_time_opt().unwrap();
81 /// if let MappedLocalTime::Single(datetime) = mapped {
82 /// assert_eq!(datetime.to_string(), "2025-04-20 17:42:10 -07:00");
83 /// } else {
84 /// panic!("should be Single");
85 /// }
86 /// ```
87 fn to_date_time_opt(
88 &self,
89 ) -> Result<chrono::MappedLocalTime<chrono::DateTime<chrono::FixedOffset>>, Error>;
90}
91
92impl Git2TimeChronoExt for git2::Time {
93 fn to_date_time_opt(
94 &self,
95 ) -> Result<chrono::MappedLocalTime<chrono::DateTime<chrono::FixedOffset>>, Error> {
96 let Some(tz) = chrono::FixedOffset::east_opt(self.offset_minutes() * 60) else {
97 return Err(Error::InvalidTimeZone {
98 offset_minutes: self.offset_minutes(),
99 });
100 };
101 Ok(tz.timestamp_opt(self.seconds(), 0))
102 }
103
104 fn to_date_time(&self) -> Result<chrono::DateTime<chrono::FixedOffset>, Error> {
105 match self.to_date_time_opt()? {
106 chrono::MappedLocalTime::Single(datetime) => Ok(datetime),
107 chrono::MappedLocalTime::Ambiguous(_, latest) => Ok(latest),
108 chrono::MappedLocalTime::None => Err(Error::TimeNotMappable { time: *self }),
109 }
110 }
111
112 fn to_date_time_in<Tz: chrono::TimeZone>(
113 &self,
114 tz: &Tz,
115 ) -> Result<chrono::DateTime<Tz>, Error> {
116 self.to_date_time()
117 .map(|datetime| datetime.with_timezone(tz))
118 }
119
120 fn to_local_date_time(&self) -> Result<chrono::DateTime<chrono::Local>, Error> {
121 self.to_date_time_in(&chrono::Local)
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn to_date_time_offset_invalid() {
131 let time = git2::Time::new(0, 100_000);
132 let result = time.to_date_time();
133 assert!(result.is_err());
134 let err = result.unwrap_err();
135 assert!(matches!(
136 err,
137 Error::InvalidTimeZone {
138 offset_minutes: 100_000
139 }
140 ));
141 }
142
143 #[test]
144 fn to_date_time_not_mappable() {
145 let time = git2::Time::new(i64::MAX, 0);
146 let result = time.to_date_time();
147 assert!(result.is_err());
148 let err = result.unwrap_err();
149 assert!(matches!(err, Error::TimeNotMappable { .. }));
150 }
151
152 #[test]
153 fn to_date_time_opt_none() {
154 use chrono::MappedLocalTime;
155 let time = git2::Time::new(i64::MAX, 0);
156 let mapped = time.to_date_time_opt().unwrap();
157 assert!(matches!(mapped, MappedLocalTime::None));
158 }
159}