chrono_humanize/
humantime.rs

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