Skip to main content

jobberdb/
duration.rs

1//! Duration in time
2
3use crate::prelude::*;
4use regex::Regex;
5
6/// Duration in time.
7#[derive(Debug, Clone, PartialEq)]
8pub enum Duration {
9    /// In no time at all.
10    Zero,
11    /// Hours and minutes.
12    HM { hours: i64, minutes: i64 },
13}
14
15impl Duration {
16    /// Create duration with the given amount of days.
17    pub fn days(days: i64) -> Self {
18        Self::HM {
19            hours: days * 24,
20            minutes: 0,
21        }
22    }
23    /// Parse duration from a string.
24    pub fn parse(duration: String) -> Result<Self, Error> {
25        match Self::parse_hm(&duration)
26            .or(Self::parse_hours(&duration).or(Self::parse_hm2(&duration)))
27        {
28            Duration::Zero => Err(Error::DurationFormat(duration)),
29            duration => Ok(duration),
30        }
31    }
32    /// Set duration in this instance if none was set before.
33    pub fn or(self, d: Self) -> Self {
34        match self {
35            Self::Zero => d,
36            _ => self,
37        }
38    }
39    /// Parse time from string in format `HH:MM`.
40    fn parse_hm(duration: &str) -> Self {
41        let re = Regex::new(r"^(\d+):(\d{1,2})$").unwrap();
42        if let Some(cap) = re.captures_iter(duration).next() {
43            return Self::HM {
44                hours: cap[1].parse::<i64>().unwrap(),
45                minutes: cap[2].parse::<i64>().unwrap(),
46            };
47        }
48        Self::Zero
49    }
50    /// Parse duration from fractional hours string.
51    fn parse_hours(duration: &str) -> Self {
52        let re = Regex::new(r"^(\d+)[,.](\d{1,2})$").unwrap();
53        if let Some(cap) = re.captures_iter(duration).next() {
54            return Self::HM {
55                hours: cap[1].parse::<i64>().unwrap(),
56                minutes: (format!(".{}", &cap[2]).parse::<f64>().unwrap() * 60f64)
57                    as i64,
58            };
59        }
60        let re = Regex::new(r"^[,.](\d{1,2})$").unwrap();
61        if let Some(cap) = re.captures_iter(duration).next() {
62            return Self::HM {
63                hours: 0,
64                minutes: (format!(".{}", &cap[1]).parse::<f64>().unwrap() * 60f64)
65                    as i64,
66            };
67        }
68        let re = Regex::new(r"^(\d{1,2})$").unwrap();
69        if let Some(cap) = re.captures_iter(duration).next() {
70            return Self::HM {
71                hours: cap[1].parse::<u32>().unwrap() as i64,
72                minutes: 0,
73            };
74        }
75        Self::Zero
76    }
77    /// Parse alternative Duration with `h` and `m`.
78    fn parse_hm2(duration: &str) -> Self {
79        let re = Regex::new(r"^(\d{1,2})h(\d{1,2})m$").unwrap();
80        if let Some(cap) = re.captures_iter(duration).next() {
81            return Self::HM {
82                hours: cap[1].parse::<u32>().unwrap() as i64,
83                minutes: cap[2].parse::<u32>().unwrap() as i64,
84            };
85        }
86        let re = Regex::new(r"^(\d{1,2})m$").unwrap();
87        if let Some(cap) = re.captures_iter(duration).next() {
88            return Self::HM {
89                hours: 0,
90                minutes: cap[1].parse::<u32>().unwrap() as i64,
91            };
92        }
93        let re = Regex::new(r"^(\d{1,2})h$").unwrap();
94        if let Some(cap) = re.captures_iter(duration).next() {
95            return Self::HM {
96                hours: cap[1].parse::<u32>().unwrap() as i64,
97                minutes: 0,
98            };
99        }
100        Self::Zero
101    }
102    /// Return duration in minutes.
103    pub fn num_minutes(&self) -> i64 {
104        match self {
105            Duration::Zero => 0,
106            Duration::HM { hours, minutes } => hours * 60 + minutes,
107        }
108    }
109}
110impl From<Duration> for chrono::Duration {
111    fn from(val: Duration) -> Self {
112        match val {
113            Duration::Zero => chrono::Duration::zero(),
114            Duration::HM { hours, minutes } => {
115                chrono::Duration::hours(hours) + chrono::Duration::minutes(minutes)
116            }
117        }
118    }
119}
120impl std::fmt::Display for Duration {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        match self {
123            Duration::HM { hours, minutes } => {
124                write!(f, "{}", *hours as f64 + *minutes as f64 / 60f64)
125            }
126            _ => write!(f, "0"),
127        }
128    }
129}
130
131#[test]
132fn test_duration() {
133    assert_eq!(
134        Duration::parse("2:30".to_string()).unwrap(),
135        Duration::HM {
136            hours: 2,
137            minutes: 30
138        }
139    );
140    assert_eq!(
141        Duration::parse("2.5".to_string()).unwrap(),
142        Duration::HM {
143            hours: 2,
144            minutes: 30
145        }
146    );
147    assert_eq!(
148        Duration::parse(".25".to_string()).unwrap(),
149        Duration::HM {
150            hours: 0,
151            minutes: 15
152        }
153    );
154    assert_eq!(
155        Duration::parse(".5".to_string()).unwrap(),
156        Duration::HM {
157            hours: 0,
158            minutes: 30
159        }
160    );
161    assert_eq!(
162        Duration::parse("2".to_string()).unwrap(),
163        Duration::HM {
164            hours: 2,
165            minutes: 0
166        }
167    );
168    assert_eq!(
169        Duration::parse("2h30m".to_string()).unwrap(),
170        Duration::HM {
171            hours: 2,
172            minutes: 30
173        }
174    );
175    assert_eq!(
176        Duration::parse("2h".to_string()).unwrap(),
177        Duration::HM {
178            hours: 2,
179            minutes: 0
180        }
181    );
182    assert_eq!(
183        Duration::parse("15m".to_string()).unwrap(),
184        Duration::HM {
185            hours: 0,
186            minutes: 15
187        }
188    );
189}