git2_time_chrono_ext/git2_time_chrono_ext.rs
1use anyhow::*;
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<chrono::FixedOffset>`].
21 ///
22 /// This is useful when the original timezone in the [`git2::Time`] is needed.
23 /// # Examples
24 /// ```
25 /// use git2_time_chrono_ext::Git2TimeChronoExt;
26 ///
27 /// // The Eastern Hemisphere time zone.
28 /// let east_time = git2::Time::new(1745693791, 540);
29 /// let east_datetime = east_time.to_date_time();
30 /// assert!(east_datetime.is_ok());
31 /// assert_eq!(east_datetime.unwrap().to_string(), "2025-04-27 03:56:31 +09:00");
32 /// ```
33 /// ```
34 /// # use git2_time_chrono_ext::Git2TimeChronoExt;
35 /// // The Western Hemisphere time zone.
36 /// let west_time = git2::Time::new(1745196130, -420);
37 /// let west_datetime = west_time.to_date_time();
38 /// assert!(west_datetime.is_ok());
39 /// assert_eq!(west_datetime.unwrap().to_string(), "2025-04-20 17:42:10 -07:00");
40 /// ```
41 fn to_date_time(&self) -> anyhow::Result<chrono::DateTime<chrono::FixedOffset>>;
42
43 /// Convert [`git2::Time`] to [`chrono::DateTime`] in the specified time zone.
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>(
53 &self,
54 tz: &Tz,
55 ) -> anyhow::Result<chrono::DateTime<Tz>>;
56
57 /// Convert [`git2::Time`] to [`chrono::DateTime`] in the local time zone.
58 /// This function is a shorthand of:
59 /// ```
60 /// # use git2_time_chrono_ext::Git2TimeChronoExt;
61 /// # fn to_local(time: git2::Time) -> anyhow::Result<chrono::DateTime<chrono::Local>> {
62 /// time.to_date_time_in(&chrono::Local)
63 /// # }
64 /// ```
65 fn to_local_date_time(&self) -> anyhow::Result<chrono::DateTime<chrono::Local>>;
66}
67
68impl Git2TimeChronoExt for git2::Time {
69 fn to_date_time(&self) -> anyhow::Result<chrono::DateTime<chrono::FixedOffset>> {
70 let Some(tz) = chrono::FixedOffset::east_opt(self.offset_minutes() * 60) else {
71 bail!("Invalid TimeZone {}", self.offset_minutes());
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 => bail!(
77 "Time {} isn't mappable to {}",
78 self.seconds(),
79 self.offset_minutes()
80 ),
81 }
82 }
83
84 fn to_date_time_in<Tz: chrono::TimeZone>(
85 &self,
86 tz: &Tz,
87 ) -> anyhow::Result<chrono::DateTime<Tz>> {
88 self.to_date_time()
89 .map(|datetime| datetime.with_timezone(tz))
90 }
91
92 fn to_local_date_time(&self) -> anyhow::Result<chrono::DateTime<chrono::Local>> {
93 self.to_date_time_in(&chrono::Local)
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn to_date_time_offset_invalid() {
103 let time = git2::Time::new(0, 100_000);
104 let datetime = time.to_date_time();
105 assert!(datetime.is_err());
106 assert_eq!(datetime.unwrap_err().to_string(), "Invalid TimeZone 100000");
107 }
108}