git_iblame/
git2_time_to_chrono_ext.rs

1use anyhow::*;
2use chrono::TimeZone;
3
4/// An extension trait to convert `git2::Time` to `chrono::DateTime`.
5pub trait Git2TimeToChronoExt {
6    /// Convert `git2::Time` to `chrono::DateTime<chrono::FixedOffset>`.
7    /// The time zone offset is the value in the `git2::Time`.
8    /// # Examples
9    /// ```
10    /// use git_iblame::Git2TimeToChronoExt;
11    /// // The Eastern Hemisphere time zone.
12    /// let east_time = git2::Time::new(1745693791, 540);
13    /// let east_datetime = east_time.to_date_time();
14    /// assert!(east_datetime.is_ok());
15    /// assert_eq!(east_datetime.unwrap().to_string(), "2025-04-27 03:56:31 +09:00");
16    /// ```
17    /// ```
18    /// use git_iblame::Git2TimeToChronoExt;
19    /// // The Western Hemisphere time zone.
20    /// let west_time = git2::Time::new(1745196130, -420);
21    /// let west_datetime = west_time.to_date_time();
22    /// assert!(west_datetime.is_ok());
23    /// assert_eq!(west_datetime.unwrap().to_string(), "2025-04-20 17:42:10 -07:00");
24    /// ```
25    fn to_date_time(&self) -> anyhow::Result<chrono::DateTime<chrono::FixedOffset>>;
26
27    /// Convert `git2::Time` to `chrono::DateTime` in the specified time zone.
28    /// # Examples
29    /// ```
30    /// # use git_iblame::Git2TimeToChronoExt;
31    /// # let time = git2::Time::new(1745693791, 540);
32    /// let utc_datetime = time.to_date_time_in(&chrono::Utc);
33    /// ```
34    fn to_date_time_in<Tz: chrono::TimeZone>(&self, tz: &Tz) -> anyhow::Result<chrono::DateTime<Tz>>;
35
36    /// Convert `git2::Time` to `chrono::DateTime` in the local time zone.
37    /// This function is a shorthand of:
38    /// ```
39    /// # use git_iblame::Git2TimeToChronoExt;
40    /// # let time = git2::Time::new(1745693791, 540);
41    /// let local_datetime = time.to_date_time_in(&chrono::Local);
42    /// ```
43    fn to_local_date_time(&self) -> anyhow::Result<chrono::DateTime<chrono::Local>>;
44}
45
46impl Git2TimeToChronoExt for git2::Time {
47    fn to_date_time(&self) -> anyhow::Result<chrono::DateTime<chrono::FixedOffset>> {
48        let tz = chrono::FixedOffset::east_opt(self.offset_minutes() * 60);
49        if tz.is_none() {
50            bail!("Invalid TimeZone {}", self.offset_minutes());
51        }
52        match tz.unwrap().timestamp_opt(self.seconds(), 0) {
53            chrono::MappedLocalTime::Single(datetime) => Ok(datetime),
54            chrono::MappedLocalTime::Ambiguous(_, latest) => Ok(latest),
55            chrono::MappedLocalTime::None => bail!(
56                "Time {} isn't mappable to {}",
57                self.seconds(),
58                self.offset_minutes()
59            ),
60        }
61    }
62
63    fn to_date_time_in<Tz: chrono::TimeZone>(&self, tz: &Tz) -> anyhow::Result<chrono::DateTime<Tz>> {
64        self.to_date_time()
65            .map(|datetime| datetime.with_timezone(tz))
66    }
67
68    fn to_local_date_time(&self) -> anyhow::Result<chrono::DateTime<chrono::Local>> {
69        self.to_date_time_in(&chrono::Local)
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn to_date_time_offset_invalid() {
79        let time = git2::Time::new(0, 100_000);
80        let datetime = time.to_date_time();
81        assert!(datetime.is_err());
82        assert_eq!(datetime.unwrap_err().to_string(), "Invalid TimeZone 100000");
83    }
84}