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}