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}