time_humanize/
humantime.rs

1use std::borrow::Cow;
2use std::cmp::max;
3use std::fmt;
4use std::ops::{Add, Sub};
5use std::time::{Duration, SystemTime};
6
7use std::convert::TryInto;
8
9#[cfg(feature = "time")]
10use time::OffsetDateTime;
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: u64 = 60;
45const S_HOUR: u64 = S_MINUTE * 60;
46const S_DAY: u64 = S_HOUR * 24;
47const S_WEEK: u64 = S_DAY * 7;
48const S_MONTH: u64 = S_DAY * 30;
49const S_YEAR: u64 = S_DAY * 365;
50
51#[derive(Clone, Copy, Debug)]
52enum TimePeriod {
53    Now,
54    Nanos(u64),
55    Micros(u64),
56    Millis(u64),
57    Seconds(u64),
58    Minutes(u64),
59    Hours(u64),
60    Days(u64),
61    Weeks(u64),
62    Months(u64),
63    Years(u64),
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, PartialOrd)]
125pub struct HumanTime {
126    duration: Duration,
127    is_positive: bool,
128}
129
130impl HumanTime {
131    const DAYS_IN_MONTH: u64 = 30;
132
133    /// Create `HumanTime` object that corresponds to the current point in time.
134    ///. Similar to `chrono::Utc::now()`
135    pub fn now() -> Self {
136        Self {
137            duration: Duration::new(0, 0),
138            is_positive: true,
139        }
140    }
141
142    /// Gives English text representation of the `HumanTime` with given `accuracy` and 'tense`
143    #[must_use]
144    pub fn to_text_en(self, accuracy: Accuracy, tense: Tense) -> String {
145        let mut periods = match accuracy {
146            Accuracy::Rough => self.rough_period(),
147            Accuracy::Precise => self.precise_period(),
148        };
149
150        let first = periods.remove(0).to_text(accuracy);
151        let last = periods.pop().map(|last| last.to_text(accuracy));
152
153        let mut text = periods.into_iter().fold(first, |acc, p| {
154            format!("{}, {}", acc, p.to_text(accuracy)).into()
155        });
156
157        if let Some(last) = last {
158            text = format!("{} and {}", text, last).into();
159        }
160
161        match tense {
162            Tense::Past => format!("{} ago", text),
163            Tense::Future => format!("in {}", text),
164            Tense::Present => text.into_owned(),
165        }
166    }
167
168    /// Return `HumanTime` for given seconds from epoch start
169    pub fn from_duration_since_timestamp(timestamp: u64) -> HumanTime {
170        let since_epoch_duration = SystemTime::now()
171            .duration_since(SystemTime::UNIX_EPOCH)
172            .unwrap();
173
174        let ts = Duration::from_secs(timestamp);
175
176        let duration = since_epoch_duration - ts;
177
178        // Can something happen when casting from unsigned to signed?
179        let duration = duration.as_secs() as i64;
180
181        // Cause we calculate since a timestamp till today, we negate the duration
182        HumanTime::from(-duration)
183    }
184
185    /// Returns the unix timestamp till Duration
186    pub fn to_unix_timestamp(&self) -> i64 {
187        let since_epoch_duration = SystemTime::now()
188            .duration_since(SystemTime::UNIX_EPOCH)
189            .unwrap();
190
191        let duration = if self.is_positive {
192            since_epoch_duration + self.duration
193        } else {
194            since_epoch_duration - self.duration
195        };
196
197        duration.as_secs() as i64
198    }
199
200    fn tense(self, accuracy: Accuracy) -> Tense {
201        match self.duration.as_secs() {
202            0..=10 if accuracy.is_rough() => Tense::Present,
203            _ if !self.is_positive => Tense::Past,
204            _ if self.is_positive => Tense::Future,
205            _ => Tense::Present,
206        }
207    }
208
209    fn rough_period(self) -> Vec<TimePeriod> {
210        let period = match self.duration.as_secs() {
211            n if n > 547 * S_DAY => TimePeriod::Years(max(n / S_YEAR, 2)),
212            n if n > 345 * S_DAY => TimePeriod::Years(1),
213            n if n > 45 * S_DAY => TimePeriod::Months(max(n / S_MONTH, 2)),
214            n if n > 29 * S_DAY => TimePeriod::Months(1),
215            n if n > 10 * S_DAY + 12 * S_HOUR => TimePeriod::Weeks(max(n / S_WEEK, 2)),
216            n if n > 6 * S_DAY + 12 * S_HOUR => TimePeriod::Weeks(1),
217            n if n > 36 * S_HOUR => TimePeriod::Days(max(n / S_DAY, 2)),
218            n if n > 22 * S_HOUR => TimePeriod::Days(1),
219            n if n > 90 * S_MINUTE => TimePeriod::Hours(max(n / S_HOUR, 2)),
220            n if n > 45 * S_MINUTE => TimePeriod::Hours(1),
221            n if n > 90 => TimePeriod::Minutes(max(n / S_MINUTE, 2)),
222            n if n > 45 => TimePeriod::Minutes(1),
223            n if n > 10 => TimePeriod::Seconds(n),
224            0..=10 => TimePeriod::Now,
225            _ => TimePeriod::Eternity,
226        };
227
228        vec![period]
229    }
230
231    fn precise_period(self) -> Vec<TimePeriod> {
232        let mut periods = vec![];
233
234        let (years, reminder) = self.split_years();
235        if let Some(years) = years {
236            periods.push(TimePeriod::Years(years));
237        }
238
239        let (months, reminder) = reminder.split_months();
240        if let Some(months) = months {
241            periods.push(TimePeriod::Months(months));
242        }
243
244        let (weeks, reminder) = reminder.split_weeks();
245        if let Some(weeks) = weeks {
246            periods.push(TimePeriod::Weeks(weeks));
247        }
248
249        let (days, reminder) = reminder.split_days();
250        if let Some(days) = days {
251            periods.push(TimePeriod::Days(days));
252        }
253
254        let (hours, reminder) = reminder.split_hours();
255        if let Some(hours) = hours {
256            periods.push(TimePeriod::Hours(hours));
257        }
258
259        let (minutes, reminder) = reminder.split_minutes();
260        if let Some(minutes) = minutes {
261            periods.push(TimePeriod::Minutes(minutes));
262        }
263
264        let (seconds, reminder) = reminder.split_seconds();
265        if let Some(seconds) = seconds {
266            periods.push(TimePeriod::Seconds(seconds));
267        }
268
269        let (millis, reminder) = reminder.split_milliseconds();
270        if let Some(millis) = millis {
271            periods.push(TimePeriod::Millis(millis));
272        }
273
274        let (micros, reminder) = reminder.split_microseconds();
275        if let Some(micros) = micros {
276            periods.push(TimePeriod::Micros(micros));
277        }
278
279        let (nanos, reminder) = reminder.split_nanoseconds();
280        if let Some(nanos) = nanos {
281            periods.push(TimePeriod::Nanos(nanos));
282        }
283
284        debug_assert!(reminder.is_zero());
285
286        if periods.is_empty() {
287            periods.push(TimePeriod::Seconds(0));
288        }
289
290        periods
291    }
292
293    /// Split this `HumanTime` into number of whole years and the reminder
294    fn split_years(self) -> (Option<u64>, Self) {
295        let years = self.duration.as_secs() / S_YEAR;
296        let reminder = self.duration - Duration::new(years * S_YEAR, 0);
297        Self::normalize_split(years, reminder)
298    }
299
300    /// Split this `HumanTime` into number of whole months and the reminder
301    fn split_months(self) -> (Option<u64>, Self) {
302        let months = self.duration.as_secs() / S_MONTH;
303        let reminder = self.duration - Duration::new(months * Self::DAYS_IN_MONTH, 0);
304        Self::normalize_split(months, reminder)
305    }
306
307    /// Split this `HumanTime` into number of whole weeks and the reminder
308    fn split_weeks(self) -> (Option<u64>, Self) {
309        let weeks = self.duration.as_secs() / S_WEEK;
310        let reminder = self.duration - Duration::new(weeks * S_WEEK, 0);
311        Self::normalize_split(weeks, reminder)
312    }
313
314    /// Split this `HumanTime` into number of whole days and the reminder
315    fn split_days(self) -> (Option<u64>, Self) {
316        let days = self.duration.as_secs() / S_DAY;
317        let reminder = self.duration - Duration::new(days * S_DAY, 0);
318        Self::normalize_split(days, reminder)
319    }
320
321    /// Split this `HumanTime` into number of whole hours and the reminder
322    fn split_hours(self) -> (Option<u64>, Self) {
323        let hours = self.duration.as_secs() / S_HOUR;
324        let reminder = self.duration - Duration::new(hours * S_HOUR, 0);
325        Self::normalize_split(hours, reminder)
326    }
327
328    /// Split this `HumanTime` into number of whole minutes and the reminder
329    fn split_minutes(self) -> (Option<u64>, Self) {
330        let minutes = self.duration.as_secs() / S_MINUTE;
331        let reminder = self.duration - Duration::new(minutes * S_MINUTE, 0);
332        Self::normalize_split(minutes, reminder)
333    }
334
335    /// Split this `HumanTime` into number of whole seconds and the reminder
336    fn split_seconds(self) -> (Option<u64>, Self) {
337        let seconds = self.duration.as_secs();
338        let reminder = self.duration - Duration::new(seconds, 0);
339        Self::normalize_split(seconds, reminder)
340    }
341
342    /// Split this `HumanTime` into number of whole milliseconds and the reminder
343    fn split_milliseconds(self) -> (Option<u64>, Self) {
344        let millis = self.duration.as_millis();
345        // We can safely convert u128 to u64, because we got it from the same value
346        let reminder = self.duration - Duration::from_millis(millis.try_into().unwrap());
347        Self::normalize_split(millis.try_into().unwrap(), reminder)
348    }
349
350    /// Split this `HumanTime` into number of whole seconds and the reminder
351    fn split_microseconds(self) -> (Option<u64>, Self) {
352        let micros = self.duration.as_micros();
353        let reminder = self.duration - Duration::from_micros(micros.try_into().unwrap());
354        Self::normalize_split(micros.try_into().unwrap(), reminder)
355    }
356
357    /// Split this `HumanTime` into number of whole seconds and the reminder
358    fn split_nanoseconds(self) -> (Option<u64>, Self) {
359        let nanos = self.duration.as_nanos();
360        let reminder = self.duration - Duration::from_nanos(nanos.try_into().unwrap());
361        Self::normalize_split(nanos.try_into().unwrap(), reminder)
362    }
363
364    fn normalize_split(wholes: u64, reminder: Duration) -> (Option<u64>, Self) {
365        let whole = match wholes == 0 {
366            true => None,
367            false => Some(wholes),
368        };
369
370        (
371            whole,
372            Self {
373                duration: reminder,
374                is_positive: true,
375            },
376        )
377    }
378
379    /// Check if `HumanTime` duration is zero
380    pub fn is_zero(self) -> bool {
381        self.duration.is_zero()
382    }
383
384    /// Return a string represenation for a given `Accuracy`
385    fn locale_en(&self, accuracy: Accuracy) -> String {
386        let tense = self.tense(accuracy);
387        self.to_text_en(accuracy, tense)
388    }
389
390    /// Return duration as seconds, can be negative
391    fn as_secs(&self) -> i64 {
392        if self.is_positive {
393            self.duration.as_secs() as i64
394        } else {
395            -(self.duration.as_secs() as i64)
396        }
397    }
398}
399
400/// Instantiate `HumanTime` from different time metrics
401impl HumanTime {
402    /// Instantiate `HumanTime` for given seconds
403    pub fn from_seconds(seconds: i64) -> HumanTime {
404        HumanTime::from(seconds)
405    }
406
407    /// Instantiate `HumanTime` for given minutes
408    pub fn from_minutes(minutes: i64) -> HumanTime {
409        HumanTime::from(minutes * S_MINUTE as i64)
410    }
411
412    /// Instantiate `HumanTime` for given hours
413    pub fn from_hours(hours: i64) -> HumanTime {
414        HumanTime::from(hours * S_HOUR as i64)
415    }
416
417    /// Instantiate `HumanTime` for given days
418    pub fn from_days(days: i64) -> HumanTime {
419        HumanTime::from(days * S_DAY as i64)
420    }
421
422    /// Instantiate `HumanTime` for given weeks
423    pub fn from_weeks(weeks: i64) -> HumanTime {
424        HumanTime::from(weeks * S_WEEK as i64)
425    }
426
427    /// Instantiate `HumanTime` for given months
428    pub fn from_months(months: i64) -> HumanTime {
429        HumanTime::from(months * S_MONTH as i64)
430    }
431
432    /// Instantiate `HumanTime` for given years
433    pub fn from_years(years: i64) -> HumanTime {
434        HumanTime::from(years * S_YEAR as i64)
435    }
436}
437
438impl fmt::Display for HumanTime {
439    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440        let accuracy = if f.alternate() {
441            Accuracy::Precise
442        } else {
443            Accuracy::Rough
444        };
445
446        f.pad(&self.locale_en(accuracy))
447    }
448}
449
450impl From<Duration> for HumanTime {
451    /// Create `HumanTime` from `Duration`
452    fn from(duration: Duration) -> Self {
453        Self {
454            duration,
455            is_positive: true,
456        }
457    }
458}
459
460impl Add for HumanTime {
461    type Output = Self;
462
463    fn add(self, rhs: Self) -> Self {
464        HumanTime::from(self.as_secs() + rhs.as_secs())
465    }
466}
467
468impl Sub for HumanTime {
469    type Output = Self;
470
471    fn sub(self, rhs: Self) -> Self {
472        HumanTime::from(self.as_secs() - rhs.as_secs())
473    }
474}
475
476impl From<SystemTime> for HumanTime {
477    fn from(st: SystemTime) -> Self {
478        match st.duration_since(SystemTime::now()) {
479            Ok(duration) => HumanTime::from(-(duration.as_secs() as i64)),
480            Err(err) => HumanTime::from(-(err.duration().as_secs() as i64)),
481        }
482    }
483}
484
485impl From<i64> for HumanTime {
486    /// Performs conversion from `i64` to `HumanTime`, from seconds.
487    fn from(duration_in_sec: i64) -> Self {
488        Self {
489            duration: Duration::from_secs(duration_in_sec.unsigned_abs()),
490            is_positive: duration_in_sec >= 0,
491        }
492    }
493}
494
495#[cfg(feature = "time")]
496impl Into<OffsetDateTime> for HumanTime {
497    fn into(self) -> OffsetDateTime {
498        if self.is_positive {
499            OffsetDateTime::UNIX_EPOCH + self.duration
500        } else {
501            OffsetDateTime::UNIX_EPOCH - self.duration
502        }
503    }
504}
505
506/// Display `Duration` as human readable time
507pub trait Humanize {
508    fn humanize(&self) -> String;
509}
510
511impl Humanize for Duration {
512    fn humanize(&self) -> String {
513        format!("{}", HumanTime::from(*self))
514    }
515}
516
517#[cfg(test)]
518mod tests {
519
520    use super::*;
521    use std::time::SystemTime;
522
523    #[cfg(feature = "time")]
524    #[test]
525    fn test_into_offset_date_time() {
526        let dt: OffsetDateTime = HumanTime::from(SystemTime::UNIX_EPOCH).into();
527        let ht = HumanTime::from(SystemTime::now());
528
529        // Left is actual unix timestamp, right is duration till timestamp therefore negate
530        assert_eq!(dt.unix_timestamp(), -ht.to_unix_timestamp())
531    }
532
533    #[test]
534    fn test_duration_from_system_time() {
535        let ht = HumanTime::from(SystemTime::now());
536        assert_eq!("now", format!("{}", ht))
537    }
538
539    #[test]
540    fn test_duration_from_system_time_since_epoch() {
541        let ht = HumanTime::from(SystemTime::UNIX_EPOCH);
542        // Well this will work for one year
543        assert_eq!("51 years ago", format!("{}", ht))
544    }
545
546    #[test]
547    fn test_add_human_time() {
548        let ht1 = HumanTime::from_seconds(30);
549        let ht2 = HumanTime::from_seconds(30);
550
551        let result = ht1 + ht2;
552        assert_eq!(result.duration.as_secs(), 60);
553        assert!(result.is_positive);
554    }
555
556    #[test]
557    fn test_add_human_time_neg() {
558        let ht1 = HumanTime::from_seconds(30);
559        let ht2 = HumanTime::from_seconds(-40);
560
561        let result = ht1 + ht2;
562        assert_eq!(result.duration.as_secs(), 10);
563        assert!(!result.is_positive);
564    }
565
566    #[test]
567    fn test_sub_human_time() {
568        let ht1 = HumanTime::from_seconds(30);
569        let ht2 = HumanTime::from_seconds(30);
570
571        let result = ht1 - ht2;
572        assert_eq!(result.duration.as_secs(), 0);
573        assert!(result.is_positive);
574    }
575
576    #[test]
577    fn test_sub_human_time_neg() {
578        let ht1 = HumanTime::from_seconds(30);
579        let ht2 = HumanTime::from_seconds(-40);
580
581        let result = ht1 + ht2;
582        assert_eq!(result.duration.as_secs(), 10);
583        assert!(!result.is_positive);
584    }
585}