1#![deny(missing_docs)]
2use std::time::Duration;
23
24#[cfg(feature = "chrono")]
25extern crate chrono;
26
27#[allow(missing_docs)]
30pub trait Language {
31 fn too_low(&self) -> &'static str;
33
34 fn too_high(&self) -> &'static str;
36
37 fn ago(&self) -> &'static str;
39
40 fn get_word(&self, tu: TimeUnit, x: u64) -> &'static str;
42
43 fn place_ago_before(&self) -> bool {
45 false
46 }
47 fn override_space_near_ago(&self) -> &str {
49 " "
50 }
51 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 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
99pub type BoxedLanguage = Box<dyn Language + Send + Sync + 'static>;
101
102#[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"))]
123pub mod languages {
125 pub mod english;
127}
128
129pub use languages::english::English;
130
131#[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 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 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 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
201pub 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 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 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 pub fn num_items(&mut self, x: usize) -> &mut Self {
280 assert!(x > 0);
281 self.num_items = x;
282 self
283 }
284
285 pub fn max_unit(&mut self, x: TimeUnit) -> &mut Self {
300 self.max_unit = x;
301 self
302 }
303
304 pub fn min_unit(&mut self, x: TimeUnit) -> &mut Self {
327 self.min_unit = x;
328 self
329 }
330
331 pub fn too_low(&mut self, x: &'static str) -> &mut Self {
354 self.too_low = Some(x);
355 self
356 }
357
358 pub fn too_high(&mut self, x: &'static str) -> &mut Self {
368 self.too_high = Some(x);
369 self
370 }
371
372 pub fn max_duration(&mut self, x: Duration) -> &mut Self {
380 self.max_duration = x;
381 self
382 }
383
384 pub fn ago(&mut self, x: &'static str) -> &mut Self {
396 self.ago = Some(x);
397 self
398 }
399
400 #[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 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 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
659pub 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#[deprecated(since = "0.1.0", note = "Use Formatter or format_5chars")]
679#[derive(Copy, Clone)]
680pub enum Style {
681 LONG,
683 HUMAN,
685 SHORT,
687}
688
689const S_IN_MNTH: u64 = 2_628_003; #[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}