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