duration_human/
parser.rs

1use std::{ops::Add, time::Instant};
2
3use lazy_regex::regex;
4
5use crate::{DurationError, DurationHumanValidator};
6
7type StdDuration = std::time::Duration;
8
9/// Define a Duration in human readable form
10///
11/// ## Examples
12/// ```
13/// # use duration_human::{DurationHuman, DurationError};
14/// let duration = DurationHuman::try_from("80h").unwrap();
15/// assert_eq!(format!("{:#}", duration), "3 days 8h".to_string());
16/// assert_eq!(format!("{}", duration), "80h".to_string());
17/// let duration = DurationHuman::try_from("72h").unwrap();
18/// assert_eq!(format!("{:#}", duration), "3 days".to_string());
19/// assert_eq!(format!("{}", duration), "3 days".to_string());
20/// let duration = DurationHuman::try_from("18446744073709551615ns").unwrap();
21/// assert_eq!(format!("{:#}", duration), "5 centuries 84 years 6 months 2 weeks 1 day 8h 34min 33s 709ms 551μs 615ns".to_string());
22/// // roundtrip
23/// let duration = DurationHuman::try_from("5 centuries 84 years 6 months 2 weeks 1 day 8h 34min 33s 709ms 551μs 615ns").unwrap();
24/// let pretty = format!("{:#}", duration);
25/// let duration_from_pretty = DurationHuman::try_from(pretty.as_str())?;
26/// assert_eq!(duration, duration_from_pretty);
27/// // precision is nano second
28/// let duration = DurationHuman::try_from("604800μs").unwrap();
29/// assert_eq!(format!("{:#}", duration), "604ms 800μs".to_string());
30/// assert_eq!(duration.to_string(), "604800μs".to_string());
31/// let duration = DurationHuman::try_from("604800ms").unwrap();
32/// assert_eq!(format!("{:#}", duration), "10min 4s 800ms".to_string());
33/// assert_eq!(duration.to_string(), "604800ms".to_string());
34/// let duration = DurationHuman::try_from("604800s").unwrap();
35/// assert_eq!(format!("{:#}", duration), "1 week".to_string());
36/// let duration = DurationHuman::try_from("604800s").unwrap();
37/// assert_eq!(format!("{:#}", duration), "1 week".to_string());
38/// assert_eq!(format!("{}", duration), "1 week".to_string());
39/// let duration = DurationHuman::try_from("608430s").unwrap();
40/// assert_eq!(format!("{:#}", duration), "1 week 1h 30s".to_string());
41/// assert_eq!(format!("{}", duration), "608430s".to_string());
42/// # Ok::<(), DurationError>(())
43/// ```
44#[derive(Clone, PartialEq, Eq, PartialOrd, Copy, Debug)]
45pub struct DurationHuman {
46    inner: StdDuration,
47}
48
49impl DurationHuman {
50    pub const MICRO_SEC: u64 = 1_000;
51    pub const MILLI_SEC: u64 = 1_000 * Self::MICRO_SEC;
52    pub const SEC: u64 = 1_000 * Self::MILLI_SEC;
53    pub const MINUTE: u64 = 60 * Self::SEC;
54    pub const HOUR: u64 = 60 * Self::MINUTE;
55    pub const DAY: u64 = 24 * Self::HOUR;
56    pub const WEEK: u64 = 7 * Self::DAY;
57    pub const YEAR: u64 = 31_557_600 * Self::SEC;
58    pub const MONTH: u64 = Self::YEAR / 12;
59    pub const CENTURY: u64 = 100 * Self::YEAR;
60
61    pub const ONE_SECOND: Self = Self::new(Self::SEC);
62    pub const ONE_MILLISECOND: Self = Self::new(Self::MILLI_SEC);
63
64    #[must_use]
65    pub const fn new(nanos: u64) -> Self {
66        Self {
67            inner: std::time::Duration::from_nanos(nanos),
68        }
69    }
70
71    /// Create a new duration from a human redable string
72    ///
73    /// ## Errors
74    /// `DurationError` when the parsing fails
75    pub fn parse(human_readable: &str) -> Result<Self, DurationError> {
76        Self::try_from(human_readable)
77    }
78
79    #[must_use]
80    pub fn is_in(&self, range: &DurationHumanValidator) -> bool {
81        range.contains(self)
82    }
83}
84
85impl Default for DurationHuman {
86    /// Defaults to a 1min duration
87    fn default() -> Self {
88        Self {
89            inner: StdDuration::from_millis(Self::MINUTE),
90        }
91    }
92}
93
94impl Add<Instant> for DurationHuman {
95    type Output = Instant;
96
97    /// Create a new `std::time::Instant` by adding one to this duration
98    ///
99    /// ## Example
100    /// ```
101    /// # use std::time::Instant;
102    /// # use duration_human::{DurationHuman, DurationError};
103    /// let instant = Instant::now();
104    /// let duration = DurationHuman::try_from("420s")?;
105    /// let after = duration + instant;
106    /// let diff = DurationHuman::from(after - instant);
107    /// assert_eq!(format!("{}", diff), format!("7min"));
108    /// # Ok::<(),DurationError>(())
109    /// ```
110    fn add(self, rhs: Instant) -> Self::Output {
111        rhs + self.inner
112    }
113}
114
115impl From<StdDuration> for DurationHuman {
116    fn from(inner: StdDuration) -> Self {
117        Self { inner }
118    }
119}
120
121impl From<&DurationHuman> for StdDuration {
122    /// For non human interaction features, just unwrap the `std::time::Duration`
123    ///
124    /// ## Example
125    /// ```
126    /// # use duration_human::{DurationHuman, DurationError};
127    /// let duration = DurationHuman::try_from("5min")?;
128    /// let duration = std::time::Duration::from(&duration);
129    /// assert_eq!(duration.as_secs_f32(), 300_f32);
130    /// # Ok::<(),DurationError>(())
131    /// ```
132    fn from(duration: &DurationHuman) -> Self {
133        duration.inner
134    }
135}
136
137impl From<u64> for DurationHuman {
138    /// Create a duration in nano seconds
139    fn from(nanos: u64) -> Self {
140        Self::new(nanos)
141    }
142}
143
144impl TryFrom<&str> for DurationHuman {
145    type Error = DurationError;
146
147    fn try_from(value: &str) -> Result<Self, Self::Error> {
148        let matcher = regex!(
149            r"^(?:(\d+)\s*(?:(century|centuries)|(year|month|week|day)(?:s?)|(h|min|s|ms|μs|ns))\s*)*$"
150        );
151
152        let splitter = regex!(
153            r"(\d+)\s*(?:(century|centuries)|(year|month|week|day)(?:s?)|(h|min|s|ms|μs|ns))"
154        );
155
156        if !matcher.is_match(value) {
157            return Err(DurationError::InvalidSyntax);
158        }
159
160        splitter
161            .captures_iter(value)
162            .map(|group| {
163                let value = group[1].parse::<u64>()?;
164
165                if value == 0 {
166                    Ok(DurationPart::default())
167                } else {
168                    let part: &str = group[0].as_ref();
169
170                    #[allow(clippy::unwrap_used)] // somehow the RE has four groups
171                    let unit = group
172                        .get(2)
173                        .or_else(|| group.get(3).or_else(|| group.get(4)))
174                        .unwrap();
175
176                    match unit.as_str() {
177                        "century" | "centuries" => (part, value, Self::CENTURY).try_into(),
178                        "year" => (part, value, Self::YEAR).try_into(),
179                        "month" => (part, value, Self::MONTH).try_into(),
180                        "week" => (part, value, Self::WEEK).try_into(),
181                        "day" => (part, value, Self::DAY).try_into(),
182                        "h" => (part, value, Self::HOUR).try_into(),
183                        "min" => (part, value, Self::MINUTE).try_into(),
184                        "s" => (part, value, Self::SEC).try_into(),
185                        "ms" => (part, value, Self::MILLI_SEC).try_into(),
186                        "μs" => (part, value, Self::MICRO_SEC).try_into(),
187                        "ns" => (part, value, 1).try_into(),
188                        sym => Err(DurationError::UnitMatchAndRegexNotInSync {
189                            sym: sym.to_string(),
190                        }),
191                    }
192                }
193            })
194            .fold(Ok(0), |nanos_sum, part| {
195                nanos_sum.and_then(|nanos_sum| {
196                    part.and_then(|duration_part| duration_part.add(nanos_sum))
197                })
198            })
199            .map(Self::from)
200    }
201}
202
203impl From<DurationHuman> for clap::builder::OsStr {
204    fn from(duration: DurationHuman) -> Self {
205        duration.to_string().into()
206    }
207}
208
209impl From<&DurationHuman> for u64 {
210    /// convert this duration into nano seconds
211    #[allow(clippy::cast_possible_truncation)] // cast is okay, as u64::MAX as milliseconds is more than 500 million years
212    fn from(duration: &DurationHuman) -> Self {
213        duration.inner.as_nanos() as Self
214    }
215}
216
217#[derive(Default)]
218struct DurationPart {
219    part: String,
220    nanos: u64,
221}
222
223impl TryFrom<(&str, u64, u64)> for DurationPart {
224    type Error = DurationError;
225
226    /// Create a `DurationPart` from a value and multiplication factor (both u64)
227    ///
228    /// ## Errors
229    /// if the product would overflow 2^64, the return is `DurationError::IntegerOverflowAt`
230    fn try_from((part, value, factor): (&str, u64, u64)) -> Result<Self, Self::Error> {
231        if factor < 1 {
232            return Ok(Self::default());
233        }
234
235        if value > u64::MAX / factor {
236            return Err(DurationError::IntegerOverflowAt {
237                duration: part.to_string(),
238            });
239        }
240
241        Ok(Self {
242            part: part.to_string(),
243            nanos: value * factor,
244        })
245    }
246}
247
248impl DurationPart {
249    /// Add another nano second value
250    ///
251    /// ## Errors
252    /// if the sum would overflow 2^64, the return is `DurationError::IntegerOverflowAt`
253    fn add(&self, rhs: u64) -> Result<u64, DurationError> {
254        if self.nanos > u64::MAX - rhs {
255            return Err(DurationError::IntegerOverflowAt {
256                duration: self.part.to_string(),
257            });
258        }
259
260        Ok(self.nanos + rhs)
261    }
262}