Skip to main content

compact_reltime/
lib.rs

1#![doc(html_logo_url = "https://codeberg.org/pezcore/compact-reltime/raw/branch/main/icon.svg")]
2#![doc = include_str!("../README.md")]
3#![doc = include_str!("../usage.md")]
4
5use std::fmt::{Display, Formatter, Result};
6use std::io::Write;
7
8/// The maximum duration supported by the crate in seconds; 99 years and 11 months
9pub const MAX_DURATION: u64 = 99 * Units::Years as u64 + 11 * Units::Months as u64;
10
11/// A duration that formats with human-friendly units and precision via [`Display`].
12///
13/// This type represents a duration of time with 1 second resolution, and is primarily useful
14/// via its implementation of [`Display`]. The `Display` format is a short string representing the
15/// approximate duration in terms of one or two distinct units of time which are automatically
16/// selected based on the duration where the longer the duration, the larger the units selected to
17/// represent it are. In general the string format is lossy, it approximates the duration by
18/// rounding it down to the lowest increment of the minor units selected to represent it.
19///
20/// # Unit elision
21/// In some cases, the quantity of the smallest unit selected to represent the duration is zero, and
22/// in these cases the 0-valued unit component is omitted. For example, consider a duration of
23/// 605,300 seconds, which is exactly 1 week 8 minutes and 20 seconds: the units selected to
24/// represent this duration are weeks and days, therefore the decomposition is 1w + 0d. Since the
25/// days component of this decomposition is zero, it is omitted from the formatted string and the
26/// final formatting for this duration is just `1w`.
27///
28/// # Examples
29///
30/// ```rust
31/// use compact_reltime::Reltime;
32///
33/// let reltime = Reltime::new(135243).unwrap();
34/// assert_eq!(reltime.to_string(), "37h34m");
35/// ```
36#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub struct Reltime(u64);
38
39/// Possible units for a component of a relative time.
40///
41/// Intended to be used with [`Reltime::format`] and [`Reltime::format_with_timetable`] to control
42/// units are selected to decompose a [`Reltime`] value for formatting. See those method for
43/// details.
44#[repr(u64)]
45#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
46pub enum Units {
47    Seconds = 1,
48    Minutes = 60,
49    Hours = 60 * Self::Minutes as u64,
50    Days = 24 * Self::Hours as u64,
51    Weeks = 7 * Self::Days as u64,
52    Months = 31 * Self::Days as u64,
53    Years = 365 * Self::Days as u64,
54}
55
56impl From<Units> for char {
57    fn from(u: Units) -> Self {
58        match u {
59            Units::Seconds => 's',
60            Units::Minutes => 'm',
61            Units::Hours => 'h',
62            Units::Days => 'd',
63            Units::Weeks => 'w',
64            Units::Months => 'M',
65            Units::Years => 'y',
66        }
67    }
68}
69
70impl Display for Units {
71    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
72        write!(f, "{}", char::from(*self))
73    }
74}
75
76#[derive(Debug, Copy, Clone)]
77struct Part {
78    amount: u8,
79    units: Units,
80}
81
82impl Display for Part {
83    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
84        match self.amount {
85            0 => Ok(()),
86            n => write!(f, "{}{}", n, self.units),
87        }
88    }
89}
90
91/// A formatted time duration
92///
93/// This type represents a time duration which has been decomposed into the sum of 2 components of
94/// distinct units. The only intended purpose of this type is to be rendered via [`Display`]. Values
95/// of this type are created by [`Reltime::format`].
96///
97/// This type is similar to [`Reltime`] but differs from it in that the specific decomposition of
98/// the duration into unit components is already determined in values of this type, but values of
99/// [`Reltime`] have not yet been decomposed.
100#[derive(Debug, Copy, Clone)]
101pub struct FormattedReltime(Part, Part);
102impl Display for FormattedReltime {
103    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
104        let mut buf = [0u8; 8];
105        write!(buf.as_mut_slice(), "{}{}", self.0, self.1).unwrap();
106        let len = buf.iter().position(|&x| x == 0).unwrap_or(8);
107        f.pad(str::from_utf8(&buf[..len]).unwrap())
108    }
109}
110
111/// Format using human-friendly units and precision.
112///
113/// This implementation uses a built-in default time table to determine the units used to represent
114/// the duration. For direct control over how the duration is decomposed into units, see
115/// [`Reltime::format`].
116impl Display for Reltime {
117    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
118        const MAP: [(u64, Units, Units); 6] = [
119            (90, Units::Seconds, Units::Seconds),
120            (90 * Units::Minutes as u64, Units::Minutes, Units::Seconds),
121            (72 * Units::Hours as u64, Units::Hours, Units::Minutes),
122            (10 * Units::Days as u64, Units::Days, Units::Hours),
123            (8 * Units::Weeks as u64, Units::Weeks, Units::Days),
124            (18 * Units::Months as u64, Units::Months, Units::Weeks),
125        ];
126        self.format_with_timetable(&MAP).fmt(f)
127    }
128}
129
130/// A type for constructing threshold tables for [`Reltime::format_with_timetable`]. A value of this
131/// type, `x` means that a duration less than `x.0` seconds will be docomposed into components with
132/// major units `x.1` and minor units `x.2`, unless another `ThreshSpec` value with a lower `.0`
133/// component is also greater than the duration.
134pub type ThreshSpec = (u64, Units, Units);
135
136/// Error type signaling that a [`Reltime`] which exceeds the supported duration was attempted to be
137/// initialized.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub struct TooLong;
140
141impl Reltime {
142    /// Create a new value from a duration of `seconds` seconds. Duration in seconds must be less
143    /// than or equal to [`MAX_DURATION`] seconds
144    pub fn new(seconds: u64) -> std::result::Result<Self, TooLong> {
145        if seconds > MAX_DURATION { Err(TooLong) } else { Ok(Self(seconds)) }
146    }
147
148    /// get the underlying duration in seconds
149    pub fn seconds(self) -> u64 {
150        self.0
151    }
152
153    #[inline]
154    fn decomp(self, u_big: Units, u_smol: Units) -> FormattedReltime {
155        let (q_big, rem) = (self.0 / u_big as u64, self.0 % u_big as u64);
156        let q_smol = rem / u_smol as u64;
157        FormattedReltime(
158            Part { amount: q_big as _, units: u_big },
159            Part { amount: q_smol as _, units: u_smol },
160        )
161    }
162
163    /// Format a duration using a custom decomposition
164    ///
165    /// Duration is formatted as a sum of terms with units defined by `f` which takes the duration
166    /// in seconds and returns the major and minor units to decompose it into.
167    pub fn format(self, f: impl FnOnce(u64) -> (Units, Units)) -> FormattedReltime {
168        let (u_big, u_smol) = f(self.0);
169        self.decomp(u_big, u_smol)
170    }
171
172    /// Create a formatted relative time representation of this duration using a custom time table
173    /// to determine the units used for decomposition.
174    ///
175    /// This method allows callers more fine-grained control over the units selected for decomposing
176    /// the duration into its unit components. The two units used to decompose the duration are
177    /// those in the element of `threshmap` whose first member is the lowest such among all those
178    /// which have a first element higher than the this duration in seconds.
179    ///
180    /// note: `threshmap` must uphold the following conditions otherwise the return value of this
181    /// method is meaninless:
182    ///
183    /// 1. `x.1 >= x.2` for all `x` in `threshmap`
184    /// 2. `threshmap[0].0`, `threshmap[1].0`, `threshmap[2].0`... must be monotonically increasing
185    pub fn format_with_timetable(self, threshmap: &[ThreshSpec]) -> FormattedReltime {
186        const HIGH_UNITS: (u64, Units, Units) = (0, Units::Years, Units::Months);
187        let (Ok(idx) | Err(idx)) = threshmap.binary_search_by(|x| x.0.cmp(&self.0));
188        let &(_, u_big, u_smol) = threshmap.get(idx).unwrap_or(&HIGH_UNITS);
189        self.decomp(u_big, u_smol)
190    }
191}
192
193impl TryFrom<std::time::Duration> for Reltime {
194    type Error = TooLong;
195
196    fn try_from(value: std::time::Duration) -> std::result::Result<Self, Self::Error> {
197        Self::new(value.as_secs())
198    }
199}
200
201impl From<Reltime> for std::time::Duration {
202    fn from(r: Reltime) -> Self {
203        std::time::Duration::from_secs(r.seconds())
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn one() {
213        let reltime = Reltime(135243);
214        assert_eq!(reltime.to_string(), "37h34m");
215        let reltime = Reltime(3204898);
216        assert_eq!(reltime.to_string(), "5w2d");
217    }
218
219    #[test]
220    fn error_too_long() {
221        let reltime = Reltime::new(999_999_999_999);
222        assert_eq!(reltime, Err(TooLong));
223    }
224}