1#![cfg_attr(not(feature = "std"), no_std)]
2#![doc = include_str!("../readme.md")]
3
4#[cfg(feature = "alloc")]
5extern crate alloc;
6
7mod arithmetic_impls;
8#[cfg(feature = "clap")]
9mod clap_arg;
10mod formatting;
11#[cfg(feature = "serde")]
12mod serde_impl;
13#[cfg(test)]
14mod tests;
15
16#[cfg(all(feature = "alloc", not(feature = "std")))]
17use alloc::boxed::Box;
18#[doc(no_inline)]
19pub use core::time::Duration as StdDuration;
20use core::{
21 fmt::{
22 self,
23 Debug,
24 Display,
25 Formatter,
26 },
27 str::FromStr,
28};
29
30pub use formatting::ExactDisplay;
31use nom::{
32 branch::alt,
33 bytes::complete::{
34 tag,
35 tag_no_case,
36 },
37 character::complete::{
38 digit1,
39 one_of,
40 space0,
41 },
42 combinator::{
43 opt,
44 recognize,
45 success,
46 value,
47 },
48 sequence::{
49 pair,
50 separated_pair,
51 },
52};
53#[doc(no_inline)]
54pub use rust_decimal::{
55 self,
56 Decimal,
57};
58
59#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Debug, Hash, Default)]
63pub struct Duration(u128);
64
65const MICROSECOND: u128 = 1000;
66const MILLISECOND: u128 = MICROSECOND * 1000;
67const SECOND: u128 = MILLISECOND * 1000;
68const MINUTE: u128 = SECOND * 60;
69const HOUR: u128 = MINUTE * 60;
70const DAY: u128 = HOUR * 24;
71const WEEK: u128 = DAY * 7;
72const YEAR: u128 = SECOND * 31_557_600;
73
74#[derive(Clone, Debug, Eq, PartialEq)]
78pub enum Error {
79 InvalidDuration,
81 ValueTooBig,
84 MissingUnit,
89 IsNegative(Decimal),
91 #[cfg(feature = "alloc")]
93 InvalidUnit(Box<str>),
94 #[cfg(not(feature = "alloc"))]
95 InvalidUnit,
97}
98
99impl Display for Error {
100 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
101 match self {
102 Self::InvalidDuration => write!(f, "invalid duration"),
103 Self::ValueTooBig => write!(f, "the duration value is too big to store"),
104 Self::MissingUnit => write!(f, "missing unit after number"),
105 Self::IsNegative(d) => write!(f, "durations cannot be negative ({d})"),
106 #[cfg(feature = "alloc")]
107 Self::InvalidUnit(s) => write!(f, "invalid duration unit `{s}`"),
108 #[cfg(not(feature = "alloc"))]
109 Self::InvalidUnit => write!(f, "invalid duration unit`"),
110 }
111 }
112}
113
114#[cfg(feature = "std")]
115impl std::error::Error for Error {}
116
117fn to_dec(n: u128) -> Option<Decimal> {
120 if n > 79228162514264337593543950335 {
123 None
124 } else {
125 Some(Decimal::from(n))
126 }
127}
128
129fn parse_unit(input: &str) -> Result<(&str, u128), Error> {
130 if input.trim().is_empty() {
131 return Err(Error::MissingUnit);
132 }
133
134 let (rem, unit) = alt((
135 value(
136 1,
137 alt((
138 tag_no_case("nanoseconds"),
139 tag_no_case("nanosecond"),
140 tag_no_case("nanos"),
141 tag_no_case("ns"),
142 )),
143 ),
144 value(
145 MICROSECOND,
146 alt((
147 tag_no_case("microseconds"),
148 tag_no_case("microsecond"),
149 tag_no_case("micros"),
150 tag_no_case("us"),
151 tag_no_case("µs"),
152 )),
153 ),
154 value(
155 MILLISECOND,
156 alt((
157 tag_no_case("milliseconds"),
158 tag_no_case("millisecond"),
159 tag_no_case("millis"),
160 tag_no_case("ms"),
161 )),
162 ),
163 value(
164 SECOND,
165 alt((
166 tag_no_case("seconds"),
167 tag_no_case("second"),
168 tag_no_case("secs"),
169 tag_no_case("sec"),
170 tag_no_case("s"),
171 )),
172 ),
173 value(
174 MINUTE,
175 alt((
176 tag_no_case("minutes"),
177 tag_no_case("minute"),
178 tag_no_case("mins"),
179 tag_no_case("min"),
180 tag_no_case("m"),
181 )),
182 ),
183 value(
184 HOUR,
185 alt((
186 tag_no_case("hours"),
187 tag_no_case("hour"),
188 tag_no_case("hrs"),
189 tag_no_case("hr"),
190 tag_no_case("h"),
191 )),
192 ),
193 value(
194 DAY,
195 alt((tag_no_case("days"), tag_no_case("day"), tag_no_case("d"))),
196 ),
197 value(
198 WEEK,
199 alt((tag_no_case("weeks"), tag_no_case("week"), tag_no_case("w"))),
200 ),
201 value(
202 YEAR,
203 alt((
204 tag_no_case("years"),
205 tag_no_case("year"),
206 tag_no_case("yrs"),
207 tag_no_case("yr"),
208 tag_no_case("y"),
209 )),
210 ),
211 ))(input)
212 .map_err(|_: nom::Err<nom::error::Error<_>>| {
213 #[cfg(not(feature = "alloc"))]
214 return Error::InvalidUnit;
215 #[cfg(feature = "alloc")]
216 Error::InvalidUnit(
217 input
218 .split_whitespace()
219 .next()
220 .unwrap_or_else(|| input.trim())
221 .into(),
222 )
223 })?;
224
225 if rem.starts_with(|c: char| c.is_alphabetic()) {
226 #[cfg(feature = "alloc")]
227 return Err(Error::InvalidUnit(
228 input.split_whitespace().next().unwrap_or(input).into(),
229 ));
230
231 #[cfg(not(feature = "alloc"))]
232 Err(Error::InvalidUnit)
233 } else {
234 Ok((rem, unit))
235 }
236}
237
238#[doc = include_str!("fn.parse.md")]
239pub fn parse(input: &str) -> Result<Duration, Error> {
240 if input.trim().is_empty() {
241 return Err(Error::InvalidDuration);
242 }
243 if let Ok(d) = input.parse::<Decimal>() {
244 if d.is_sign_negative() {
245 return Err(Error::IsNegative(d));
246 }
247 return d
248 .checked_mul(Decimal::from(MILLISECOND))
249 .map(|d| Duration(u128::try_from(d).unwrap()))
250 .ok_or(Error::ValueTooBig);
251 }
252
253 let parse_decimal = alt((
254 recognize(separated_pair(digit1, tag("."), digit1)),
255 recognize(pair(digit1, tag("."))),
256 recognize(pair(tag("."), digit1)),
257 digit1,
258 ));
259
260 let mut parse_decimal = recognize(pair(opt(one_of("-+")), parse_decimal));
261
262 let mut sep = alt::<_, _, nom::error::Error<_>, _>((
263 recognize(pair(tag(","), space0)),
264 space0,
265 success(""),
266 ));
267
268 let mut s = input;
269 let mut n = 0_u128;
270
271 for i in 0.. {
272 if i != 0 {
273 (s, _) = sep(s).unwrap();
274 }
275
276 if s.is_empty() {
277 break;
278 }
279
280 let (rem, d) =
281 parse_decimal(s).map_err(|_: nom::Err<nom::error::Error<_>>| Error::InvalidDuration)?;
282
283 let d = d.parse::<Decimal>().map_err(|e| match e {
284 rust_decimal::Error::ExceedsMaximumPossibleValue
285 | rust_decimal::Error::LessThanMinimumPossibleValue => Error::ValueTooBig,
286 _ => Error::InvalidDuration,
287 })?;
288
289 if d.is_sign_negative() {
290 return Err(Error::IsNegative(d));
291 }
292
293 let rem = rem.trim_start_matches(|c: char| c == ' ' || c == '\t');
294 let (rem, unit) = parse_unit(rem)?;
295 let d = Decimal::from(unit)
296 .checked_mul(d)
297 .ok_or(Error::ValueTooBig)?;
298 n = n
299 .checked_add(d.try_into().unwrap())
300 .ok_or(Error::ValueTooBig)?;
301 s = rem;
302 }
303
304 Ok(Duration(n))
305}
306
307pub fn parse_std(input: &str) -> Result<StdDuration, Error> {
311 parse(input).map(|d| d.to_std())
312}
313
314pub fn pretty(d: StdDuration) -> Duration {
316 Duration::from(d)
317}
318
319impl FromStr for Duration {
322 type Err = Error;
323
324 fn from_str(s: &str) -> Result<Self, Self::Err> {
325 parse(s)
326 }
327}
328
329impl From<StdDuration> for Duration {
330 fn from(d: StdDuration) -> Self {
331 Self::from_std(d)
332 }
333}
334
335impl From<Duration> for StdDuration {
336 fn from(d: Duration) -> Self {
337 d.to_std()
338 }
339}
340
341impl Duration {
343 pub const HOUR: Self = Self(HOUR);
344 pub const MICROSECOND: Self = Self(MICROSECOND);
345 pub const MILLISECOND: Self = Self(MILLISECOND);
346 pub const MINUTE: Self = Self(MINUTE);
347 pub const NANOSECOND: Self = Self(1);
348 pub const SECOND: Self = Self(SECOND);
349}
350
351impl Duration {
354 pub const fn from_nanos(ns: u128) -> Self {
356 Self(ns)
357 }
358
359 pub const fn from_micros(us: u128) -> Self {
365 Self(us * MICROSECOND)
366 }
367
368 pub const fn from_millis(ms: u128) -> Self {
374 Self(ms * MILLISECOND)
375 }
376
377 pub const fn from_secs(secs: u128) -> Self {
383 Self(secs * SECOND)
384 }
385
386 pub fn to_std(self) -> StdDuration {
391 self.try_to_std()
392 .expect("the value is too big to converty to std::time::Duration")
393 }
394
395 pub fn try_to_std(self) -> Option<StdDuration> {
399 u64::try_from(self.0)
400 .map(StdDuration::from_nanos)
401 .or_else(|_| u64::try_from(self.as_millis()).map(StdDuration::from_millis))
402 .or_else(|_| u64::try_from(self.as_secs()).map(StdDuration::from_secs))
403 .ok()
404 }
405
406 pub const fn from_std(d: StdDuration) -> Self {
408 Self(d.as_nanos())
409 }
410
411 pub const fn as_nanos(self) -> u128 {
413 self.0
414 }
415
416 pub const fn as_micros(self) -> u128 {
419 self.0 / MICROSECOND
420 }
421
422 pub fn as_micros_dec(self) -> Decimal {
424 to_dec(self.0).map_or_else(
425 || Decimal::from(self.as_micros()),
426 |n| n / Decimal::ONE_THOUSAND,
427 )
428 }
429
430 pub const fn as_millis(self) -> u128 {
433 self.0 / MILLISECOND
434 }
435
436 pub fn as_millis_dec(self) -> Decimal {
438 to_dec(self.0).map_or_else(
439 || Decimal::from(self.as_millis()),
440 |d| d / Decimal::from(MILLISECOND),
441 )
442 }
443
444 pub const fn as_secs(self) -> u128 {
446 self.0 / SECOND
447 }
448
449 pub fn as_secs_dec(self) -> Decimal {
451 to_dec(self.0).map_or_else(
452 || Decimal::from(self.as_secs()),
453 |d| d / Decimal::from(SECOND),
454 )
455 }
456
457 pub const fn is_zero(self) -> bool {
459 self.0 == 0
460 }
461
462 pub fn format_exact(self) -> ExactDisplay {
464 ExactDisplay(self.0)
465 }
466}
467
468impl PartialEq<Duration> for StdDuration {
470 fn eq(&self, rhs: &Duration) -> bool {
471 self.as_nanos() == rhs.as_nanos()
472 }
473}
474
475impl PartialEq<StdDuration> for Duration {
476 fn eq(&self, rhs: &StdDuration) -> bool {
477 self.as_nanos() == rhs.as_nanos()
478 }
479}