Skip to main content

timeago/
lib.rs

1#![deny(missing_docs)]
2//! Given a Duration, lossily format it like in 'N days ago'.
3//!
4//! Parsing it back to Duration is not supported yet (See [`chrono-english`] crate).
5//!
6//! Multiple languages are supported though `Language` trait.
7//! Enable `isolang` feature to gain support of getting Language impl from
8//! `lsolang::Language`.
9//!
10//! You can configure minimum and maximum time units, as well as "precision" of
11//! how many items to emit.
12//!
13//! Fractional results like "1.5 days ago" are not supported.
14//!
15//! There is a special simplified version to get compact 5-character representation: `format_5chars`.
16//!
17//! The main item of timeago is [`Formatter`].
18//!
19//! [`chrono-english`]:https://docs.rs/chrono-english
20//! [`Formatter`]:struct.Formatter.html
21
22use std::time::Duration;
23
24#[cfg(feature = "chrono")]
25extern crate chrono;
26
27/// Interface for connecting natural languages to use for the formatting
28/// See "language" module documentation for details.
29#[allow(missing_docs)]
30pub trait Language {
31    /// What to emit by default if value is too high
32    fn too_low(&self) -> &'static str;
33
34    /// What to emit by default if value is too low
35    fn too_high(&self) -> &'static str;
36
37    /// Chunk of text to put at the end by default
38    fn ago(&self) -> &'static str;
39
40    /// Get word representing the given time unit, for using with `x` number
41    fn get_word(&self, tu: TimeUnit, x: u64) -> &'static str;
42
43    /// For German and such
44    fn place_ago_before(&self) -> bool {
45        false
46    }
47    /// For Thai and such
48    fn override_space_near_ago(&self) -> &str {
49        " "
50    }
51    /// For basque and such
52    fn place_unit_before(&self, _: u64) -> bool {
53        false
54    }
55    fn between_chunks(&self) -> &str {
56        " "
57    }
58    fn between_value_and_word(&self) -> &str {
59        " "
60    }
61
62    /// Make a dynamic copy of this language
63    fn clone_boxed(&self) -> BoxedLanguage;
64}
65
66impl Language for BoxedLanguage {
67    fn clone_boxed(&self) -> BoxedLanguage {
68        (**self).clone_boxed()
69    }
70    fn too_low(&self) -> &'static str {
71        (**self).too_low()
72    }
73    fn too_high(&self) -> &'static str {
74        (**self).too_high()
75    }
76    fn ago(&self) -> &'static str {
77        (**self).ago()
78    }
79    fn get_word(&self, tu: TimeUnit, x: u64) -> &'static str {
80        (**self).get_word(tu, x)
81    }
82    fn place_ago_before(&self) -> bool {
83        (**self).place_ago_before()
84    }
85    fn override_space_near_ago(&self) -> &str {
86        (**self).override_space_near_ago()
87    }
88    fn place_unit_before(&self, x: u64) -> bool {
89        (**self).place_unit_before(x)
90    }
91    fn between_chunks(&self) -> &str {
92        (**self).between_chunks()
93    }
94    fn between_value_and_word(&self) -> &str {
95        (**self).between_value_and_word()
96    }
97}
98
99/// Dynamic version of the `Language` trait
100pub type BoxedLanguage = Box<dyn Language + Send + Sync + 'static>;
101
102/// A collection of natural languages supported out-of-the-box for the formatting.
103///
104/// You can implement a language yourself by deriving
105/// the `Language` trait (pull requests are welcome).
106///
107/// The list of languages is also tracked in `README.md`.
108/// If you spot an error, submit a fix or point it out on [Github issues](https://github.com/vi/timeago/issues/new). If on the other hand you have checked a language and assert that it is done properly, [submit a pull request against `README.md` of this project][er].
109///
110/// You can also choose the language at runtime using the `isolang` cargo feature and [`from_isolang`] function.
111///
112/// Requires `translations` Cargo feature.
113///
114/// [`from_isolang`]:fn.from_isolang.html
115/// [er]:https://github.com/vi/timeago/edit/master/README.md
116#[cfg(feature = "translations")]
117pub mod languages;
118
119#[cfg(all(feature = "isolang", feature = "translations"))]
120pub use languages::from_isolang;
121
122#[cfg(not(feature = "translations"))]
123/// Non-english modes are currently disabled by omission of "translations" cargo feature.
124pub mod languages {
125    /// Non-english modes are currently disabled by omission of "translations" cargo feature.
126    pub mod english;
127}
128
129pub use languages::english::English;
130
131/// Various units of time to specify as maximum or minimum.
132/// Note that calculations are approximate, not calendar-based.
133#[allow(missing_docs)]
134#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
135pub enum TimeUnit {
136    Nanoseconds,
137    Microseconds,
138    Milliseconds,
139    Seconds,
140    Minutes,
141    Hours,
142    Days,
143    Weeks,
144    Months,
145    Years,
146}
147
148impl TimeUnit {
149    /// Get `std::time::Duration` corresponding to minimum duration that is representable by this time unit.
150    pub fn min_duration(&self) -> Duration {
151        use TimeUnit::*;
152        match *self {
153            Nanoseconds => Duration::new(0, 1),
154            Microseconds => Duration::new(0, 1000),
155            Milliseconds => Duration::new(0, 1_000_000),
156            Seconds => Duration::new(1, 0),
157            Minutes => Duration::new(60, 0),
158            Hours => Duration::new(60 * 60, 0),
159            Days => Duration::new(24 * 60 * 60, 0),
160            Weeks => Duration::new(7 * 24 * 60 * 60, 0),
161            Months => Duration::new(S_IN_MNTH, 0),
162            Years => Duration::new(S_IN_MNTH * 12, 0),
163        }
164    }
165
166    /// "Upgrade" minutes to hours, hours to days and so on.
167    pub fn bigger_unit(&self) -> Option<TimeUnit> {
168        use TimeUnit::*;
169        match *self {
170            Nanoseconds => Some(Microseconds),
171            Microseconds => Some(Milliseconds),
172            Milliseconds => Some(Seconds),
173            Seconds => Some(Minutes),
174            Minutes => Some(Hours),
175            Hours => Some(Days),
176            Days => Some(Weeks),
177            Weeks => Some(Months),
178            Months => Some(Years),
179            Years => None,
180        }
181    }
182
183    /// "Downgrade" weeks to days, seconds to milliseconds and so on.
184    pub fn smaller_unit(&self) -> Option<TimeUnit> {
185        use TimeUnit::*;
186        match *self {
187            Nanoseconds => None,
188            Microseconds => Some(Nanoseconds),
189            Milliseconds => Some(Microseconds),
190            Seconds => Some(Milliseconds),
191            Minutes => Some(Seconds),
192            Hours => Some(Minutes),
193            Days => Some(Hours),
194            Weeks => Some(Days),
195            Months => Some(Weeks),
196            Years => Some(Months),
197        }
198    }
199}
200
201/// Main formatter struct. Build it with new() and maybe modify some options, then use convert.
202/// ```
203/// let f = timeago::Formatter::new();
204/// let d = std::time::Duration::from_secs(3600);
205/// assert_eq!(f.convert(d), "1 hour ago");
206/// ```
207pub struct Formatter<L: Language = English> {
208    lang: L,
209    num_items: usize,
210    min_unit: TimeUnit,
211    max_unit: TimeUnit,
212    too_low: Option<&'static str>,
213    too_high: Option<&'static str>,
214    ago: Option<&'static str>,
215    max_duration: Duration,
216}
217
218impl Default for Formatter {
219    fn default() -> Self {
220        Self::new()
221    }
222}
223
224impl Formatter {
225    /// Constructor for some default formatting in English
226    ///
227    /// It emits one chunk, limits to seconds and has no maximum duration.
228    pub fn new() -> Formatter {
229        Formatter::with_language(English)
230    }
231}
232
233impl Clone for Formatter<BoxedLanguage> {
234    fn clone(&self) -> Formatter<BoxedLanguage> {
235        Formatter {
236            lang: self.lang.clone_boxed(),
237            num_items: self.num_items,
238            min_unit: self.min_unit,
239            max_unit: self.max_unit,
240            too_low: self.too_low,
241            too_high: self.too_high,
242            ago: self.ago,
243            max_duration: self.max_duration,
244        }
245    }
246}
247
248impl<L: Language> Formatter<L> {
249    /// Constructor for some default formatting with specified language instance
250    ///
251    /// It emits one item (chunk), limits to seconds and has no maximum duration.
252    pub fn with_language(l: L) -> Self {
253        Formatter {
254            lang: l,
255            num_items: 1,
256            min_unit: TimeUnit::Seconds,
257            max_unit: TimeUnit::Years,
258            too_low: None,
259            too_high: None,
260            ago: None,
261            max_duration: Duration::new(u64::MAX, 999_999_999),
262        }
263    }
264
265    /// Set number of time unit items to emit (for example, 1 item is for "1 year"; 3 items is for "1 year 3 months 17 days"). Zero chunks like "0 minutes" are not emitted, expect of at the end if `too_low` is `"0"`.
266    /// Default is 1.
267    /// ```
268    /// let mut f = timeago::Formatter::new();
269    /// f.num_items(1);
270    /// let d = std::time::Duration::from_secs(3600+60+3);
271    /// assert_eq!(f.convert(d), "1 hour ago");
272    /// f.num_items(2);
273    /// assert_eq!(f.convert(d), "1 hour 1 minute ago");
274    /// f.num_items(3);
275    /// assert_eq!(f.convert(d), "1 hour 1 minute 3 seconds ago");
276    /// f.num_items(4);
277    /// assert_eq!(f.convert(d), "1 hour 1 minute 3 seconds ago");
278    /// ```
279    pub fn num_items(&mut self, x: usize) -> &mut Self {
280        assert!(x > 0);
281        self.num_items = x;
282        self
283    }
284
285    /// Set maximum used unit. Not to be confused with `max_duration`.
286    /// Should not affect appearance of "old" or other `too_high` values.
287    /// ```
288    /// let mut f = timeago::Formatter::new();
289    /// f.max_unit(timeago::TimeUnit::Hours);
290    /// let d = std::time::Duration::from_secs(60);
291    /// assert_eq!(f.convert(d), "1 minute ago");
292    /// let d = std::time::Duration::from_secs(3600);
293    /// assert_eq!(f.convert(d), "1 hour ago");
294    /// let d = std::time::Duration::from_secs(24*3600);
295    /// assert_eq!(f.convert(d), "24 hours ago");
296    /// let d = std::time::Duration::from_secs(30*24*3600);
297    /// assert_eq!(f.convert(d), "720 hours ago");
298    /// ```
299    pub fn max_unit(&mut self, x: TimeUnit) -> &mut Self {
300        self.max_unit = x;
301        self
302    }
303
304    /// Set minimum used unit. Durations below minimally representable by that unit emit `too_low` value like "now", or like "0 days" instead of normal output.
305    /// When `num_items` > 1, it also acts as precision limiter.
306    /// ```
307    /// let mut f = timeago::Formatter::new();
308    /// f.min_unit(timeago::TimeUnit::Minutes);
309    /// let d = std::time::Duration::from_secs(30);
310    /// assert_eq!(f.convert(d), "now");
311    /// let d = std::time::Duration::from_secs(90);
312    /// assert_eq!(f.convert(d), "1 minute ago");
313    /// ```
314    /// ```
315    /// let mut f = timeago::Formatter::new();
316    /// f.num_items(99);
317    /// let d = std::time::Duration::new(1*3600*24 + 2*3600 + 3*60 + 4, 500_000_000);
318    /// assert_eq!(f.convert(d), "1 day 2 hours 3 minutes 4 seconds ago");
319    /// f.min_unit(timeago::TimeUnit::Hours);
320    /// assert_eq!(f.convert(d), "1 day 2 hours ago");
321    /// f.min_unit(timeago::TimeUnit::Microseconds);
322    /// assert_eq!(f.convert(d), "1 day 2 hours 3 minutes 4 seconds 500 milliseconds ago");
323    /// f.min_unit(timeago::TimeUnit::Months);
324    /// assert_eq!(f.convert(d), "now");
325    /// ```
326    pub fn min_unit(&mut self, x: TimeUnit) -> &mut Self {
327        self.min_unit = x;
328        self
329    }
330
331    /// Override what is used instead of "now" for too short durations (not representable with the time unit configures as `min_unit`).
332    /// Setting this to special value `"0"` causes emitting output like "0 days", depending on `min_unit` property.
333    /// Note that `Language`'s `too_low` is not used in this case, except of for `"0"`.
334    /// ```
335    /// let mut f = timeago::Formatter::new();
336    /// f.min_unit(timeago::TimeUnit::Months)
337    ///  .too_low("this month");
338    /// let d = std::time::Duration::from_secs(24*3600);
339    /// assert_eq!(f.convert(d), "this month");
340    /// ```
341    /// ```
342    /// let mut f = timeago::Formatter::new();
343    /// f.min_unit(timeago::TimeUnit::Minutes);
344    /// let d = std::time::Duration::from_secs(30);
345    /// assert_eq!(f.convert(d), "now");
346    /// f.too_low("-");
347    /// assert_eq!(f.convert(d), "-");
348    /// f.too_low("");
349    /// assert_eq!(f.convert(d), "");
350    /// f.too_low("0");
351    /// assert_eq!(f.convert(d), "0 minutes ago");
352    /// ```
353    pub fn too_low(&mut self, x: &'static str) -> &mut Self {
354        self.too_low = Some(x);
355        self
356    }
357
358    /// Override what is used instead of "old" for too high units.
359    /// Note that `Language`'s `too_high` is not used in this case.
360    /// ```
361    /// let mut f = timeago::Formatter::new();
362    /// f.max_duration(std::time::Duration::from_secs(3600*24*30));
363    /// f.too_high("ancient");
364    /// let d = std::time::Duration::from_secs(1000_000_000_000);
365    /// assert_eq!(f.convert(d), "ancient");
366    /// ```
367    pub fn too_high(&mut self, x: &'static str) -> &mut Self {
368        self.too_high = Some(x);
369        self
370    }
371
372    /// Maximum duration before it start giving "old" (or other `too_high` value)
373    /// ```
374    /// let mut f = timeago::Formatter::new();
375    /// f.max_duration(std::time::Duration::new(3600*24*30, 0));
376    /// let d = std::time::Duration::from_secs(1000_000_000);
377    /// assert_eq!(f.convert(d), "old");
378    /// ```
379    pub fn max_duration(&mut self, x: Duration) -> &mut Self {
380        self.max_duration = x;
381        self
382    }
383
384    /// Override what is used instead of "ago".
385    /// Empty string literal `""` is a bit special in the space handling.
386    /// ```
387    /// let mut f = timeago::Formatter::new();
388    /// let d = std::time::Duration::from_secs(60);
389    /// assert_eq!(f.convert(d), "1 minute ago");
390    /// f.ago("later");
391    /// assert_eq!(f.convert(d), "1 minute later");
392    /// f.ago("");
393    /// assert_eq!(f.convert(d), "1 minute");
394    /// ```
395    pub fn ago(&mut self, x: &'static str) -> &mut Self {
396        self.ago = Some(x);
397        self
398    }
399
400    /// Format the timespan between `from` and `to` as a string like "15 days ago".
401    ///
402    /// Requires `chrono` Cargo feature.
403    ///
404    /// `from` should come before `to`, otherwise `"???"` will be returned.
405    ///
406    /// Currently it doesn't actually take the calendar into account and just converts datetimes
407    /// into a plain old `std::time::Duration`, but in future here may be a proper implementation.
408    ///
409    /// ```
410    /// extern crate chrono;
411    /// extern crate timeago;
412    /// let mut f = timeago::Formatter::new();
413    /// f.num_items(2);
414    /// let from = chrono::DateTime::parse_from_rfc3339("2013-12-19T15:00:00+03:00").unwrap();
415    /// let to   = chrono::DateTime::parse_from_rfc3339("2013-12-23T17:00:00+03:00").unwrap();
416    /// assert_eq!(f.convert_chrono(from, to), "4 days 2 hours ago");
417    /// ```
418    #[cfg(feature = "chrono")]
419    pub fn convert_chrono<Tz1, Tz2>(
420        &self,
421        from: chrono::DateTime<Tz1>,
422        to: chrono::DateTime<Tz2>,
423    ) -> String
424    where
425        Tz1: chrono::TimeZone,
426        Tz2: chrono::TimeZone,
427    {
428        let q = to.signed_duration_since(from);
429        if let Ok(dur) = q.to_std() {
430            self.convert(dur)
431        } else {
432            "???".to_owned()
433        }
434    }
435
436    /// Convert specified [`Duration`] to a String representing
437    /// approximation of specified timespan as a string like
438    /// "5 days ago", with specified by other methods settings.
439    /// See module-level doc for more info.
440    /// ```
441    /// let f = timeago::Formatter::new();
442    /// let d = std::time::Duration::from_secs(3600*24);
443    /// assert_eq!(f.convert(d), "1 day ago");
444    /// ```
445    ///
446    /// [`Duration`]:https://doc.rust-lang.org/std/time/struct.Duration.html
447    pub fn convert(&self, d: Duration) -> String {
448        if d > self.max_duration {
449            return self
450                .too_high
451                .unwrap_or_else(|| self.lang.too_high())
452                .to_owned();
453        }
454
455        let mut ret = self.convert_impl(d, self.num_items);
456
457        if ret.is_empty() {
458            let now = self.too_low.unwrap_or_else(|| self.lang.too_low());
459            if now != "0" {
460                return now.to_owned();
461            } else {
462                ret = format!(
463                    "0{}{}",
464                    self.lang.between_value_and_word(),
465                    self.lang.get_word(self.min_unit, 0)
466                );
467            }
468        }
469
470        let ago = self.ago.unwrap_or_else(|| self.lang.ago());
471
472        if ago.is_empty() {
473            ret
474        } else if self.lang.place_ago_before() {
475            format!("{}{}{}", ago, self.lang.override_space_near_ago(), ret)
476        } else {
477            format!("{}{}{}", ret, self.lang.override_space_near_ago(), ago)
478        }
479    }
480
481    fn convert_impl(&self, d: Duration, items_left: usize) -> String {
482        if items_left == 0 {
483            return "".to_owned();
484        }
485
486        let mut dtu = dominant_time_unit(d);
487
488        while dtu > self.max_unit {
489            dtu = dtu.smaller_unit().unwrap();
490        }
491
492        while dtu < self.min_unit {
493            dtu = dtu.bigger_unit().unwrap();
494        }
495
496        let (x, rem) = split_up(d, dtu);
497
498        if x == 0 {
499            return "".to_owned();
500        }
501
502        let recurse_result = self.convert_impl(rem, items_left - 1);
503
504        let word = self.lang.get_word(dtu, x);
505        let between = self.lang.between_value_and_word();
506        let between_chunk = self.lang.between_chunks();
507
508        match (self.lang.place_unit_before(x), recurse_result.is_empty()) {
509            (true, true) => format!("{word}{between}{x}"),
510            (true, false) => format!("{word}{between}{x}{between_chunk}{recurse_result}"),
511            (false, true) => format!("{x}{between}{word}"),
512            (false, false) => format!("{x}{between}{word}{between_chunk}{recurse_result}"),
513        }
514    }
515}
516
517fn dominant_time_unit(d: Duration) -> TimeUnit {
518    use TimeUnit::*;
519
520    match d {
521        x if x < Microseconds.min_duration() => Nanoseconds,
522        x if x < Milliseconds.min_duration() => Microseconds,
523        x if x < Seconds.min_duration() => Milliseconds,
524        x if x < Minutes.min_duration() => Seconds,
525        x if x < Hours.min_duration() => Minutes,
526        x if x < Days.min_duration() => Hours,
527        x if x < Weeks.min_duration() => Days,
528        x if x < Months.min_duration() => Weeks,
529        x if x < Years.min_duration() => Months,
530        _ => Years,
531    }
532}
533
534fn divmod64(a: u64, b: u64) -> (u64, u64) {
535    (a / b, a % b)
536}
537fn divmod32(a: u32, b: u32) -> (u32, u32) {
538    (a / b, a % b)
539}
540
541fn split_up(d: Duration, tu: TimeUnit) -> (u64, Duration) {
542    let s = d.as_secs();
543    let n = d.subsec_nanos();
544
545    let tud = tu.min_duration();
546    let tus = tud.as_secs();
547    let tun = tud.subsec_nanos();
548
549    if tus != 0 {
550        assert!(tun == 0);
551        if s == 0 {
552            (0, d)
553        } else {
554            let (c, s2) = divmod64(s, tus);
555            (c, Duration::new(s2, n))
556        }
557    } else {
558        // subsecond timeunit
559        assert!(tus == 0);
560        if s == 0 {
561            let (c, n2) = divmod32(n, tun);
562            (c.into(), Duration::new(0, n2))
563        } else {
564            assert!(1_000_000_000_u32 % tun == 0);
565            let tuninv = 1_000_000_000 / (u64::from(tun));
566            let pieces = s.saturating_mul(tuninv).saturating_add(u64::from(n / tun));
567
568            let subtract_s = pieces / tuninv;
569            let subtract_ns = ((pieces % tuninv) as u32) * tun;
570
571            let (mut s, mut n) = (s, n);
572
573            if subtract_ns > n {
574                s -= 1;
575                n += 1_000_000_000;
576            }
577
578            let remain_s = s - subtract_s;
579            let remain_ns = n - subtract_ns;
580            (pieces, Duration::new(remain_s, remain_ns))
581        }
582    }
583}
584
585#[cfg(test)]
586mod tests_split_up {
587    use super::*;
588
589    fn ds(secs: u64) -> Duration {
590        Duration::from_secs(secs)
591    }
592    fn dn(secs: u64, nanos: u32) -> Duration {
593        Duration::new(secs, nanos)
594    }
595
596    #[test]
597    fn dominant_time_unit_test() {
598        use TimeUnit::*;
599
600        assert_eq!(dominant_time_unit(ds(3)), Seconds);
601        assert_eq!(dominant_time_unit(ds(60)), Minutes);
602        assert_eq!(dominant_time_unit(dn(0, 250_000_000)), Milliseconds);
603    }
604
605    #[test]
606    fn split_up_test_sane() {
607        use TimeUnit::*;
608
609        assert_eq!(split_up(ds(120), Minutes), (2, ds(0)));
610        assert_eq!(split_up(ds(119), Minutes), (1, ds(59)));
611        assert_eq!(split_up(ds(60), Minutes), (1, ds(0)));
612        assert_eq!(split_up(ds(1), Minutes), (0, ds(1)));
613        assert_eq!(split_up(ds(0), Minutes), (0, ds(0)));
614        assert_eq!(split_up(ds(3600), Minutes), (60, ds(0)));
615        assert_eq!(split_up(ds(3600), Hours), (1, ds(0)));
616        assert_eq!(split_up(ds(3600), Seconds), (3600, ds(0)));
617        assert_eq!(split_up(ds(3600), Milliseconds), (3600_000, ds(0)));
618        assert_eq!(split_up(ds(100000000), Years), (3, ds(5391892)));
619        assert_eq!(split_up(ds(100000000), Months), (38, ds(135886)));
620        assert_eq!(split_up(ds(100000000), Days), (1157, ds(35200)));
621        assert_eq!(split_up(ds(3600), Microseconds), (3600_000_000, ds(0)));
622    }
623    #[test]
624    fn split_up_test_tricky() {
625        use TimeUnit::*;
626
627        assert_eq!(split_up(ds(3600), Nanoseconds), (3600_000_000_000, ds(0)));
628        assert_eq!(
629            split_up(ds(3600_000), Nanoseconds),
630            (3600_000_000_000_000, ds(0))
631        );
632        assert_eq!(
633            split_up(ds(3600_000_000), Nanoseconds),
634            (3600_000_000_000_000_000, ds(0))
635        );
636        assert_eq!(
637            split_up(ds(3600_000_000_000), Nanoseconds),
638            (std::u64::MAX, dn(3581_553_255_926, 290448385))
639        );
640        assert_eq!(
641            split_up(ds(3600_000_000_000), Microseconds),
642            (3600_000_000_000_000_000, ds(0))
643        );
644        assert_eq!(
645            split_up(ds(3600_000_000_000_000), Microseconds),
646            (std::u64::MAX, dn(3581_553_255_926_290, 448385000))
647        );
648        assert_eq!(
649            split_up(ds(3600_000_000_000_000), Milliseconds),
650            (3600_000_000_000_000_000, ds(0))
651        );
652        assert_eq!(
653            split_up(ds(3600_000_000_000_000_000), Milliseconds),
654            (std::u64::MAX, dn(3581_553_255_926_290_448, 385000000))
655        );
656    }
657}
658
659/// A simplified formatter, resulting in short strings like "02Yea" or " now " or "07min".
660/// Designed to always give 5-character strings.
661pub fn format_5chars(d: Duration) -> String {
662    let s = d.as_secs();
663    match s {
664        0 => " now ".into(),
665        x if (1..60).contains(&x) => format!("{x:02}sec"),
666        x if (60..3600).contains(&x) => format!("{:02}min", x / 60),
667        x if (3600..86400).contains(&x) => format!("{:02}hou", x / 3600),
668        x if (86400..S_IN_MNTH).contains(&x) => format!("{:02}day", x / 86400),
669        x if (S_IN_MNTH..(12 * S_IN_MNTH)).contains(&x) => format!("{:02}Mon", x / S_IN_MNTH),
670        x if ((12 * S_IN_MNTH)..=(99 * 12 * S_IN_MNTH)).contains(&x) => {
671            format!("{:02}Yea", x / (12 * S_IN_MNTH))
672        }
673        _ => " OLD ".into(),
674    }
675}
676
677/// Simple formatting style for deprecated `format`.
678#[deprecated(since = "0.1.0", note = "Use Formatter or format_5chars")]
679#[derive(Copy, Clone)]
680pub enum Style {
681    /// Long format, like "2 years ago"
682    LONG,
683    /// Human format, like LONG but makes less than 1 second as `just now`
684    HUMAN,
685    /// Short format, like "02Yea". Should be exactly 5 characters.
686    SHORT,
687}
688
689const S_IN_MNTH: u64 = 2_628_003; // 2628002,88 seconds according to Google
690
691/// Do the formatting. See `Style`'s docstring for formatting options.
692/// If you need just simple mode without bloated featureful implementation,
693/// use version 0.0.2 of this crate
694///
695/// ```
696/// extern crate timeago;
697/// assert_eq!(timeago::format(std::time::Duration::new(3600, 0), timeago::Style::LONG), "1 hour ago");
698/// ```
699#[deprecated(since = "0.1.0", note = "Use Formatter or format_5chars")]
700#[allow(deprecated)]
701pub fn format(d: Duration, style: Style) -> String {
702    match style {
703        Style::LONG => Formatter::new().min_unit(TimeUnit::Nanoseconds).convert(d),
704        Style::HUMAN => {
705            let ret = Formatter::new().convert(d);
706            if ret == "now" {
707                "just now".to_owned()
708            } else {
709                ret
710            }
711        }
712        Style::SHORT => format_5chars(d),
713    }
714}
715
716#[cfg(test)]
717mod tests {
718    #[allow(deprecated)]
719    use super::{format, Style};
720    use std::time::Duration;
721
722    fn dns(secs: u64) -> Duration {
723        Duration::from_secs(secs)
724    }
725    fn dn(secs: u64, nanos: u32) -> Duration {
726        Duration::new(secs, nanos)
727    }
728    #[allow(deprecated)]
729    fn fmtl(d: Duration) -> String {
730        format(d, Style::LONG)
731    }
732    #[allow(deprecated)]
733    fn fmth(d: Duration) -> String {
734        format(d, Style::HUMAN)
735    }
736    #[allow(deprecated)]
737    fn fmts(d: Duration) -> String {
738        format(d, Style::SHORT)
739    }
740
741    #[test]
742    fn test_long() {
743        assert_eq!(fmtl(dns(0)), "now");
744        assert_eq!(fmtl(dn(0, 500_000_000)), "500 milliseconds ago");
745        assert_eq!(fmtl(dns(1)), "1 second ago");
746        assert_eq!(fmtl(dn(1, 500_000_000)), "1 second ago");
747        assert_eq!(fmtl(dns(59)), "59 seconds ago");
748        assert_eq!(fmtl(dns(60)), "1 minute ago");
749        assert_eq!(fmtl(dns(65)), "1 minute ago");
750        assert_eq!(fmtl(dns(119)), "1 minute ago");
751        assert_eq!(fmtl(dns(120)), "2 minutes ago");
752        assert_eq!(fmtl(dns(3599)), "59 minutes ago");
753        assert_eq!(fmtl(dns(3600)), "1 hour ago");
754        assert_eq!(fmtl(dns(1000_000)), "1 week ago");
755        assert_eq!(fmtl(dns(1000_000_000)), "31 years ago");
756    }
757    #[test]
758    fn test_human() {
759        assert_eq!(fmth(dns(0)), "just now");
760        assert_eq!(fmth(dn(0, 500_000_000)), "just now");
761        assert_eq!(fmth(dns(1)), "1 second ago");
762        assert_eq!(fmth(dn(1, 500_000_000)), "1 second ago");
763        assert_eq!(fmth(dns(59)), "59 seconds ago");
764        assert_eq!(fmth(dns(60)), "1 minute ago");
765        assert_eq!(fmth(dns(65)), "1 minute ago");
766        assert_eq!(fmth(dns(119)), "1 minute ago");
767        assert_eq!(fmth(dns(120)), "2 minutes ago");
768        assert_eq!(fmth(dns(3599)), "59 minutes ago");
769        assert_eq!(fmth(dns(3600)), "1 hour ago");
770        assert_eq!(fmth(dns(1000_000)), "1 week ago");
771        assert_eq!(fmth(dns(1000_000_000)), "31 years ago");
772    }
773
774    #[test]
775    fn test_short() {
776        assert_eq!(fmts(dns(0)), " now ");
777        assert_eq!(fmts(dn(0, 500_000_000)), " now ");
778        assert_eq!(fmts(dns(1)), "01sec");
779        assert_eq!(fmts(dn(1, 500_000_000)), "01sec");
780        assert_eq!(fmts(dns(59)), "59sec");
781        assert_eq!(fmts(dns(60)), "01min");
782        assert_eq!(fmts(dns(65)), "01min");
783        assert_eq!(fmts(dns(119)), "01min");
784        assert_eq!(fmts(dns(120)), "02min");
785        assert_eq!(fmts(dns(3599)), "59min");
786        assert_eq!(fmts(dns(3600)), "01hou");
787        assert_eq!(fmts(dns(1000_000)), "11day");
788        assert_eq!(fmts(dns(1000_000_000)), "31Yea");
789    }
790}