git_iblame/extensions/
git2_time_to_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 git_iblame::extensions::Git2TimeToChronoExt;
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 Git2TimeToChronoExt {
20    /// Convert `git2::Time` to `chrono::DateTime<chrono::FixedOffset>`.
21    /// The time zone offset is the value in the `git2::Time`.
22    /// # Examples
23    /// ```
24    /// use git_iblame::extensions::Git2TimeToChronoExt;
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 git_iblame::extensions::Git2TimeToChronoExt;
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) -> anyhow::Result<chrono::DateTime<chrono::FixedOffset>>;
41
42    /// Convert `git2::Time` to `chrono::DateTime` in the specified time zone.
43    /// # Examples
44    /// ```
45    /// use git_iblame::extensions::Git2TimeToChronoExt;
46    /// let time = git2::Time::new(1745196130, -420);
47    /// let utc_datetime = time.to_date_time_in(&chrono::Utc);
48    /// assert_eq!(utc_datetime.unwrap().to_string(), "2025-04-21 00:42:10 UTC");
49    /// ```
50    fn to_date_time_in<Tz: chrono::TimeZone>(
51        &self,
52        tz: &Tz,
53    ) -> anyhow::Result<chrono::DateTime<Tz>>;
54
55    /// Convert `git2::Time` to `chrono::DateTime` in the local time zone.
56    /// This function is a shorthand of:
57    /// ```
58    /// # use git_iblame::extensions::Git2TimeToChronoExt;
59    /// # let time = git2::Time::new(1745693791, 540);
60    /// let local_datetime = time.to_date_time_in(&chrono::Local);
61    /// ```
62    fn to_local_date_time(&self) -> anyhow::Result<chrono::DateTime<chrono::Local>>;
63}
64
65impl Git2TimeToChronoExt for git2::Time {
66    fn to_date_time(&self) -> anyhow::Result<chrono::DateTime<chrono::FixedOffset>> {
67        let tz = chrono::FixedOffset::east_opt(self.offset_minutes() * 60);
68        if tz.is_none() {
69            bail!("Invalid TimeZone {}", self.offset_minutes());
70        }
71        match tz.unwrap().timestamp_opt(self.seconds(), 0) {
72            chrono::MappedLocalTime::Single(datetime) => Ok(datetime),
73            chrono::MappedLocalTime::Ambiguous(_, latest) => Ok(latest),
74            chrono::MappedLocalTime::None => bail!(
75                "Time {} isn't mappable to {}",
76                self.seconds(),
77                self.offset_minutes()
78            ),
79        }
80    }
81
82    fn to_date_time_in<Tz: chrono::TimeZone>(
83        &self,
84        tz: &Tz,
85    ) -> anyhow::Result<chrono::DateTime<Tz>> {
86        self.to_date_time()
87            .map(|datetime| datetime.with_timezone(tz))
88    }
89
90    fn to_local_date_time(&self) -> anyhow::Result<chrono::DateTime<chrono::Local>> {
91        self.to_date_time_in(&chrono::Local)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn to_date_time_offset_invalid() {
101        let time = git2::Time::new(0, 100_000);
102        let datetime = time.to_date_time();
103        assert!(datetime.is_err());
104        assert_eq!(datetime.unwrap_err().to_string(), "Invalid TimeZone 100000");
105    }
106}