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#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd)]
14pub enum Tense {
15 Past,
16 Present,
17 Future,
18}
19
20#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd)]
22pub enum Accuracy {
23 Rough,
25 Precise,
27}
28
29impl Accuracy {
30 #[must_use]
32 pub fn is_precise(self) -> bool {
33 self == Self::Precise
34 }
35
36 #[must_use]
38 pub fn is_rough(self) -> bool {
39 self == Self::Rough
40 }
41}
42
43const 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#[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 pub fn now() -> Self {
136 Self {
137 duration: Duration::new(0, 0),
138 is_positive: true,
139 }
140 }
141
142 #[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 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 let duration = duration.as_secs() as i64;
180
181 HumanTime::from(-duration)
183 }
184
185 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 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 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 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 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 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 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 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 fn split_milliseconds(self) -> (Option<u64>, Self) {
344 let millis = self.duration.as_millis();
345 let reminder = self.duration - Duration::from_millis(millis.try_into().unwrap());
347 Self::normalize_split(millis.try_into().unwrap(), reminder)
348 }
349
350 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 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 pub fn is_zero(self) -> bool {
381 self.duration.is_zero()
382 }
383
384 fn locale_en(&self, accuracy: Accuracy) -> String {
386 let tense = self.tense(accuracy);
387 self.to_text_en(accuracy, tense)
388 }
389
390 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
400impl HumanTime {
402 pub fn from_seconds(seconds: i64) -> HumanTime {
404 HumanTime::from(seconds)
405 }
406
407 pub fn from_minutes(minutes: i64) -> HumanTime {
409 HumanTime::from(minutes * S_MINUTE as i64)
410 }
411
412 pub fn from_hours(hours: i64) -> HumanTime {
414 HumanTime::from(hours * S_HOUR as i64)
415 }
416
417 pub fn from_days(days: i64) -> HumanTime {
419 HumanTime::from(days * S_DAY as i64)
420 }
421
422 pub fn from_weeks(weeks: i64) -> HumanTime {
424 HumanTime::from(weeks * S_WEEK as i64)
425 }
426
427 pub fn from_months(months: i64) -> HumanTime {
429 HumanTime::from(months * S_MONTH as i64)
430 }
431
432 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 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 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
506pub 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 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 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}