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}