Skip to main content

jiffy/
humantime.rs

1use std::borrow::Cow;
2use std::cmp::max;
3use std::cmp::Ordering;
4use std::convert::TryFrom;
5use std::fmt;
6use std::time::SystemTime;
7
8use jiff::ToSpan;
9
10use crate::Humanize;
11
12/// Indicates the time of the period in relation to the time of the utterance
13#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd)]
14pub enum Tense {
15    Past,
16    Present,
17    Future,
18}
19
20/// The accuracy of the representation
21#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd)]
22pub enum Accuracy {
23    /// Rough approximation, easy to grasp, but not necessarily accurate
24    Rough,
25    /// Concise expression, accurate, but not necessarily easy to grasp
26    Precise,
27}
28
29impl Accuracy {
30    /// Returns whether this accuracy is precise
31    #[must_use]
32    pub fn is_precise(self) -> bool {
33        self == Self::Precise
34    }
35
36    /// Returns whether this accuracy is rough
37    #[must_use]
38    pub fn is_rough(self) -> bool {
39        self == Self::Rough
40    }
41}
42
43// Number of seconds in various time periods
44const S_MINUTE: i64 = 60;
45const S_HOUR: i32 = (S_MINUTE * 60) as i32;
46const S_DAY: i32 = S_HOUR * 24;
47const S_WEEK: i32 = S_DAY * 7;
48const S_MONTH: i32 = S_DAY * 30;
49const S_YEAR: i16 = (S_DAY * 365) as i16;
50
51#[derive(Clone, Copy, Debug)]
52enum TimePeriod {
53    Now,
54    Nanos(i64),
55    Micros(i64),
56    Millis(i64),
57    Seconds(i64),
58    Minutes(i64),
59    Hours(i32),
60    Days(i32),
61    Weeks(i32),
62    Months(i32),
63    Years(i16),
64    Eternity,
65}
66
67impl TimePeriod {
68    fn to_text_precise(self) -> Cow<'static, str> {
69        match self {
70            Self::Now => "now".into(),
71            Self::Nanos(n) => format!("{} ns", n).into(),
72            Self::Micros(n) => format!("{} µs", n).into(),
73            Self::Millis(n) => format!("{} ms", n).into(),
74            Self::Seconds(1) => "1 second".into(),
75            Self::Seconds(n) => format!("{} seconds", n).into(),
76            Self::Minutes(1) => "1 minute".into(),
77            Self::Minutes(n) => format!("{} minutes", n).into(),
78            Self::Hours(1) => "1 hour".into(),
79            Self::Hours(n) => format!("{} hours", n).into(),
80            Self::Days(1) => "1 day".into(),
81            Self::Days(n) => format!("{} days", n).into(),
82            Self::Weeks(1) => "1 week".into(),
83            Self::Weeks(n) => format!("{} weeks", n).into(),
84            Self::Months(1) => "1 month".into(),
85            Self::Months(n) => format!("{} months", n).into(),
86            Self::Years(1) => "1 year".into(),
87            Self::Years(n) => format!("{} years", n).into(),
88            Self::Eternity => "eternity".into(),
89        }
90    }
91
92    fn to_text_rough(self) -> Cow<'static, str> {
93        match self {
94            Self::Now => "now".into(),
95            Self::Nanos(n) => format!("{} ns", n).into(),
96            Self::Micros(n) => format!("{} µs", n).into(),
97            Self::Millis(n) => format!("{} ms", n).into(),
98            Self::Seconds(n) => format!("{} seconds", n).into(),
99            Self::Minutes(1) => "a minute".into(),
100            Self::Minutes(n) => format!("{} minutes", n).into(),
101            Self::Hours(1) => "an hour".into(),
102            Self::Hours(n) => format!("{} hours", n).into(),
103            Self::Days(1) => "a day".into(),
104            Self::Days(n) => format!("{} days", n).into(),
105            Self::Weeks(1) => "a week".into(),
106            Self::Weeks(n) => format!("{} weeks", n).into(),
107            Self::Months(1) => "a month".into(),
108            Self::Months(n) => format!("{} months", n).into(),
109            Self::Years(1) => "a year".into(),
110            Self::Years(n) => format!("{} years", n).into(),
111            Self::Eternity => "eternity".into(),
112        }
113    }
114
115    fn to_text(self, accuracy: Accuracy) -> Cow<'static, str> {
116        match accuracy {
117            Accuracy::Rough => self.to_text_rough(),
118            Accuracy::Precise => self.to_text_precise(),
119        }
120    }
121}
122
123/// `Duration` wrapper that helps expressing the duration in human languages
124#[derive(Clone, Copy, Debug, PartialEq)]
125pub struct HumanTime(jiff::Span);
126
127impl HumanTime {
128    const DAYS_IN_YEAR: i32 = 365;
129    const DAYS_IN_MONTH: i32 = 30;
130
131    /// Create `HumanTime` object that corresponds to the current point in time.
132    ///. Similar to `jiff::Zoned::now()`
133    pub fn now() -> Self {
134        Self(jiff::Span::default())
135    }
136
137    /// Gives English text representation of the `HumanTime` with given `accuracy` and 'tense`
138    #[must_use]
139    pub fn to_text_en(self, accuracy: Accuracy, tense: Tense) -> String {
140        let mut periods = match accuracy {
141            Accuracy::Rough => self.rough_period(),
142            Accuracy::Precise => self.precise_period(),
143        };
144
145        let first = periods.remove(0).to_text(accuracy);
146        let last = periods.pop().map(|last| last.to_text(accuracy));
147
148        let mut text = periods.into_iter().fold(first, |acc, p| {
149            format!("{}, {}", acc, p.to_text(accuracy)).into()
150        });
151
152        if let Some(last) = last {
153            text = format!("{} and {}", text, last).into();
154        }
155
156        match tense {
157            Tense::Past => format!("{} ago", text),
158            Tense::Future => format!("in {}", text),
159            Tense::Present => text.into_owned(),
160        }
161    }
162
163    fn tense(self, _accuracy: Accuracy) -> Tense {
164        // (???)
165        match self.0.compare(jiff::Span::default()).unwrap() {
166            Ordering::Greater => Tense::Future,
167            Ordering::Less => Tense::Past,
168            _ => Tense::Present,
169        }
170    }
171
172    fn rough_period(self) -> Vec<TimePeriod> {
173        let period = match self.0.total(jiff::Unit::Second).unwrap().abs() as i64 {
174            n if n as i16 > (547 * S_DAY) as i16 => TimePeriod::Years(max(n as i16 / S_YEAR, 2)),
175            n if n as i16 > (345 * S_DAY) as i16 => TimePeriod::Years(1),
176            n if n as i32 > 45 * S_DAY => TimePeriod::Months(max(n as i32 / S_MONTH, 2)),
177            n if n as i32 > 29 * S_DAY => TimePeriod::Months(1),
178            n if n as i32 > 10 * S_DAY + 12 * S_HOUR => {
179                TimePeriod::Weeks(max(n as i32 / S_WEEK, 2))
180            }
181            n if n as i32 > 6 * S_DAY + 12 * S_HOUR => TimePeriod::Weeks(1),
182            n if n as i32 > 36 * S_HOUR => TimePeriod::Days(max(n as i32 / S_DAY, 2)),
183            n if n as i32 > 22 * S_HOUR => TimePeriod::Days(1),
184            n if n > 90 * S_MINUTE => TimePeriod::Hours(max(n as i32 / S_HOUR, 2)),
185            n if n > 45 * S_MINUTE => TimePeriod::Hours(1),
186            n if n > 90 => TimePeriod::Minutes(max(n / S_MINUTE, 2)),
187            n if n > 45 => TimePeriod::Minutes(1),
188            n if n > 10 => TimePeriod::Seconds(n),
189            0..=10 => TimePeriod::Now,
190            _ => TimePeriod::Eternity,
191        };
192
193        vec![period]
194    }
195
196    fn precise_period(self) -> Vec<TimePeriod> {
197        let mut periods = vec![];
198
199        let (years, reminder) = self.split_years();
200        if let Some(years) = years {
201            periods.push(TimePeriod::Years(years as i16));
202        }
203
204        let (months, reminder) = reminder.split_months();
205        if let Some(months) = months {
206            periods.push(TimePeriod::Months(months as i32));
207        }
208
209        let (weeks, reminder) = reminder.split_weeks();
210        if let Some(weeks) = weeks {
211            periods.push(TimePeriod::Weeks(weeks as i32));
212        }
213
214        let (days, reminder) = reminder.split_days();
215        if let Some(days) = days {
216            periods.push(TimePeriod::Days(days as i32));
217        }
218
219        let (hours, reminder) = reminder.split_hours();
220        if let Some(hours) = hours {
221            periods.push(TimePeriod::Hours(hours as i32));
222        }
223
224        let (minutes, reminder) = reminder.split_minutes();
225        if let Some(minutes) = minutes {
226            periods.push(TimePeriod::Minutes(minutes));
227        }
228
229        let (seconds, reminder) = reminder.split_seconds();
230        if let Some(seconds) = seconds {
231            periods.push(TimePeriod::Seconds(seconds));
232        }
233
234        let (millis, reminder) = reminder.split_milliseconds();
235        if let Some(millis) = millis {
236            periods.push(TimePeriod::Millis(millis));
237        }
238
239        let (micros, reminder) = reminder.split_microseconds();
240        if let Some(micros) = micros {
241            periods.push(TimePeriod::Micros(micros));
242        }
243
244        let (nanos, reminder) = reminder.split_nanoseconds();
245        if let Some(nanos) = nanos {
246            periods.push(TimePeriod::Nanos(nanos));
247        }
248
249        debug_assert!(reminder.is_zero());
250
251        if periods.is_empty() {
252            periods.push(TimePeriod::Seconds(0));
253        }
254
255        periods
256    }
257
258    /// Split this `HumanTime` into number of whole years and the reminder
259    fn split_years(self) -> (Option<i64>, Self) {
260        let years = self.0.get_days() / Self::DAYS_IN_YEAR;
261        let reminder = self
262            .0
263            .checked_sub((years * Self::DAYS_IN_YEAR).days())
264            .unwrap();
265        Self::normalize_split(years as i64, reminder)
266    }
267
268    /// Split this `HumanTime` into number of whole months and the reminder
269    fn split_months(self) -> (Option<i64>, Self) {
270        let months = self.0.get_days() / Self::DAYS_IN_MONTH;
271        let reminder = self
272            .0
273            .checked_sub((months * Self::DAYS_IN_MONTH).days())
274            .unwrap();
275        Self::normalize_split(months as i64, reminder)
276    }
277
278    /// Split this `HumanTime` into number of whole weeks and the reminder
279    fn split_weeks(self) -> (Option<i64>, Self) {
280        let weeks = self.0.get_weeks();
281        let reminder = self.0.checked_sub(weeks.weeks()).unwrap();
282        Self::normalize_split(weeks as i64, reminder)
283    }
284
285    /// Split this `HumanTime` into number of whole days and the reminder
286    fn split_days(self) -> (Option<i64>, Self) {
287        let days = self.0.get_days();
288        let reminder = self.0.checked_sub(days.days()).unwrap();
289        Self::normalize_split(days as i64, reminder)
290    }
291
292    /// Split this `HumanTime` into number of whole hours and the reminder
293    fn split_hours(self) -> (Option<i64>, Self) {
294        let hours = self.0.get_hours();
295        let reminder = self.0.checked_sub(hours.hours()).unwrap();
296        Self::normalize_split(hours as i64, reminder)
297    }
298
299    /// Split this `HumanTime` into number of whole minutes and the reminder
300    fn split_minutes(self) -> (Option<i64>, Self) {
301        let minutes = self.0.get_minutes();
302        let reminder = self.0.checked_sub(minutes.minutes()).unwrap();
303        Self::normalize_split(minutes, reminder)
304    }
305
306    /// Split this `HumanTime` into number of whole seconds and the reminder
307    fn split_seconds(self) -> (Option<i64>, Self) {
308        let seconds = self.0.get_seconds();
309        let reminder = self.0.checked_sub(seconds.seconds()).unwrap();
310        Self::normalize_split(seconds, reminder)
311    }
312
313    /// Split this `HumanTime` into number of whole milliseconds and the reminder
314    fn split_milliseconds(self) -> (Option<i64>, Self) {
315        let millis = self.0.get_milliseconds();
316        let reminder = self.0.checked_sub(millis.milliseconds()).unwrap();
317        Self::normalize_split(millis, reminder)
318    }
319
320    /// Split this `HumanTime` into number of whole seconds and the reminder
321    fn split_microseconds(self) -> (Option<i64>, Self) {
322        let micros = self.0.get_microseconds();
323        let reminder = self.0.checked_sub(micros.microseconds()).unwrap();
324        Self::normalize_split(micros, reminder)
325    }
326
327    /// Split this `HumanTime` into number of whole seconds and the reminder
328    fn split_nanoseconds(self) -> (Option<i64>, Self) {
329        let nanos = self.0.get_nanoseconds();
330        let reminder = self.0.checked_sub(nanos.nanoseconds()).unwrap();
331        Self::normalize_split(nanos, reminder)
332    }
333
334    fn normalize_split(
335        wholes: impl Into<Option<i64>>,
336        reminder: jiff::Span,
337    ) -> (Option<i64>, Self) {
338        let wholes = wholes.into().map(i64::abs).filter(|x| *x > 0);
339        (wholes, Self(reminder))
340    }
341
342    pub fn is_zero(self) -> bool {
343        self.0.is_zero()
344    }
345
346    fn locale_en(&self, accuracy: Accuracy) -> String {
347        let tense = self.tense(accuracy);
348        self.to_text_en(accuracy, tense)
349    }
350}
351
352impl fmt::Display for HumanTime {
353    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
354        let accuracy = if f.alternate() {
355            Accuracy::Precise
356        } else {
357            Accuracy::Rough
358        };
359
360        f.pad(&self.locale_en(accuracy))
361    }
362}
363
364impl From<jiff::Span> for HumanTime {
365    fn from(duration: jiff::Span) -> Self {
366        Self(duration)
367    }
368}
369
370impl From<jiff::Zoned> for HumanTime {
371    fn from(dt: jiff::Zoned) -> Self {
372        Self(dt.since(&jiff::Zoned::now()).unwrap())
373    }
374}
375
376impl From<SystemTime> for HumanTime {
377    fn from(st: SystemTime) -> Self {
378        jiff::Timestamp::try_from(st).unwrap().into()
379    }
380}
381
382impl From<jiff::Timestamp> for HumanTime {
383    fn from(dt: jiff::Timestamp) -> Self {
384        dt.since(jiff::Zoned::now()).unwrap().into()
385    }
386}
387
388impl Humanize for jiff::Span {
389    fn humanize(&self) -> String {
390        format!("{}", HumanTime::from(*self))
391    }
392}
393
394impl Humanize for jiff::Zoned {
395    fn humanize(&self) -> String {
396        format!("{}", HumanTime::from(self.clone()))
397    }
398}
399
400impl Humanize for SystemTime {
401    fn humanize(&self) -> String {
402        HumanTime::from(*self).to_string()
403    }
404}