dur/
lib.rs

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/// A human readable duration backed by a [u128].
60///
61/// The underlying [u128] represents the duration in nanoseconds.
62#[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// Error
75
76/// The parse error.
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub enum Error {
79	/// Catch-all for values that aren't proper durations.
80	InvalidDuration,
81	/// The value being parsed is too big in nanoseconds in total to fit in a
82	/// [u128] or bigger than [Decimal::MAX] in case of a single unit.
83	ValueTooBig,
84	/// The value being parsed is missing a unit.
85	///
86	/// Note that values without any unit and only one number, such as `"42"`
87	/// are not errors and are parsed as milliseconds.
88	MissingUnit,
89	/// The value being parsed contains negative durations.
90	IsNegative(Decimal),
91	/// The value contains an unrecognized duration unit.
92	#[cfg(feature = "alloc")]
93	InvalidUnit(Box<str>),
94	#[cfg(not(feature = "alloc"))]
95	/// The value contains an unrecognized duration unit.
96	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
117// Parsing
118
119fn to_dec(n: u128) -> Option<Decimal> {
120	// Decimal::try_from and Decimal::from both panic with values greater than
121	// Decimal::MAX as below
122	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
307/// Parse the human-readable duration string into an [StdDuration].
308///
309/// See [parse] for usage.
310pub fn parse_std(input: &str) -> Result<StdDuration, Error> {
311	parse(input).map(|d| d.to_std())
312}
313
314/// Constructs a new [Duration]. Equivalent to [Duration::from]
315pub fn pretty(d: StdDuration) -> Duration {
316	Duration::from(d)
317}
318
319// Conversions
320
321impl 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
341// Constants
342impl 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
351// Impls
352
353impl Duration {
354	/// Creates a new `Duration` from the specified number of nanoseconds.
355	pub const fn from_nanos(ns: u128) -> Self {
356		Self(ns)
357	}
358
359	/// Creates a new `Duration` from the specified number of microseconds.
360	///
361	/// #### Overflow Behavior
362	/// IF the value in nanoseconds overflows a [u128], the behavior is the same
363	/// as with [u128] overflow with multiplication.
364	pub const fn from_micros(us: u128) -> Self {
365		Self(us * MICROSECOND)
366	}
367
368	/// Creates a new `Duration` from the specified number of milliseconds.
369	///
370	/// #### Overflow Behavior
371	/// IF the value in nanoseconds overflows a [u128], the behavior is the same
372	/// as with [u128] overflow with multiplication.
373	pub const fn from_millis(ms: u128) -> Self {
374		Self(ms * MILLISECOND)
375	}
376
377	/// Creates a new `Duration` from the specified number of seconds.
378	///
379	/// #### Overflow Behavior
380	/// IF the value in nanoseconds overflows a [u128], the behavior is the same
381	/// as with [u128] overflow with multiplication.
382	pub const fn from_secs(secs: u128) -> Self {
383		Self(secs * SECOND)
384	}
385
386	/// Convert to [StdDuration]. equivalent to calling [Into::into].
387	///
388	/// #### Panics
389	/// Panics if `self` is too big for an [StdDuration].
390	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	/// Tries to convert `self` into an [StdDuration].
396	///
397	/// Returns [None] if the value is too big for [StdDuration].
398	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	/// Convert from [StdDuration]. Equivalent to [Duration::from].
407	pub const fn from_std(d: StdDuration) -> Self {
408		Self(d.as_nanos())
409	}
410
411	/// Returns the total number of nanoseconds contained by this Duration.
412	pub const fn as_nanos(self) -> u128 {
413		self.0
414	}
415
416	/// Returns the total number of whole microseconds contained by this
417	/// Duration.
418	pub const fn as_micros(self) -> u128 {
419		self.0 / MICROSECOND
420	}
421
422	/// Returns this duration in microseconds as a [Decimal].
423	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	/// Returns the total number of whole milliseconds contained by this
431	/// Duration.
432	pub const fn as_millis(self) -> u128 {
433		self.0 / MILLISECOND
434	}
435
436	/// Returns this duration in milliseconds as a [Decimal].
437	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	/// Returns the total number of whole seconds contained by this Duration.
445	pub const fn as_secs(self) -> u128 {
446		self.0 / SECOND
447	}
448
449	/// Returns this duration in seconds as a [Decimal].
450	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	/// Returns true if this Duration is 0.
458	pub const fn is_zero(self) -> bool {
459		self.0 == 0
460	}
461
462	/// Returns a struct with a lossless [Display] implementation.
463	pub fn format_exact(self) -> ExactDisplay {
464		ExactDisplay(self.0)
465	}
466}
467
468// Trait impls
469impl 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}