1use std::borrow::Cow;
2use std::cmp::max;
3use std::cmp::Ordering;
4use std::convert::TryFrom;
5use std::fmt;
6use std::time::SystemTime;
7
8use jiff::ToSpan;
9
10use crate::Humanize;
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: i64 = 60;
45const S_HOUR: i32 = (S_MINUTE * 60) as i32;
46const S_DAY: i32 = S_HOUR * 24;
47const S_WEEK: i32 = S_DAY * 7;
48const S_MONTH: i32 = S_DAY * 30;
49const S_YEAR: i16 = (S_DAY * 365) as i16;
50
51#[derive(Clone, Copy, Debug)]
52enum TimePeriod {
53 Now,
54 Nanos(i64),
55 Micros(i64),
56 Millis(i64),
57 Seconds(i64),
58 Minutes(i64),
59 Hours(i32),
60 Days(i32),
61 Weeks(i32),
62 Months(i32),
63 Years(i16),
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)]
125pub struct HumanTime(jiff::Span);
126
127impl HumanTime {
128 const DAYS_IN_YEAR: i32 = 365;
129 const DAYS_IN_MONTH: i32 = 30;
130
131 pub fn now() -> Self {
134 Self(jiff::Span::default())
135 }
136
137 #[must_use]
139 pub fn to_text_en(self, accuracy: Accuracy, tense: Tense) -> String {
140 let mut periods = match accuracy {
141 Accuracy::Rough => self.rough_period(),
142 Accuracy::Precise => self.precise_period(),
143 };
144
145 let first = periods.remove(0).to_text(accuracy);
146 let last = periods.pop().map(|last| last.to_text(accuracy));
147
148 let mut text = periods.into_iter().fold(first, |acc, p| {
149 format!("{}, {}", acc, p.to_text(accuracy)).into()
150 });
151
152 if let Some(last) = last {
153 text = format!("{} and {}", text, last).into();
154 }
155
156 match tense {
157 Tense::Past => format!("{} ago", text),
158 Tense::Future => format!("in {}", text),
159 Tense::Present => text.into_owned(),
160 }
161 }
162
163 fn tense(self, _accuracy: Accuracy) -> Tense {
164 match self.0.compare(jiff::Span::default()).unwrap() {
166 Ordering::Greater => Tense::Future,
167 Ordering::Less => Tense::Past,
168 _ => Tense::Present,
169 }
170 }
171
172 fn rough_period(self) -> Vec<TimePeriod> {
173 let period = match self.0.total(jiff::Unit::Second).unwrap().abs() as i64 {
174 n if n as i16 > (547 * S_DAY) as i16 => TimePeriod::Years(max(n as i16 / S_YEAR, 2)),
175 n if n as i16 > (345 * S_DAY) as i16 => TimePeriod::Years(1),
176 n if n as i32 > 45 * S_DAY => TimePeriod::Months(max(n as i32 / S_MONTH, 2)),
177 n if n as i32 > 29 * S_DAY => TimePeriod::Months(1),
178 n if n as i32 > 10 * S_DAY + 12 * S_HOUR => {
179 TimePeriod::Weeks(max(n as i32 / S_WEEK, 2))
180 }
181 n if n as i32 > 6 * S_DAY + 12 * S_HOUR => TimePeriod::Weeks(1),
182 n if n as i32 > 36 * S_HOUR => TimePeriod::Days(max(n as i32 / S_DAY, 2)),
183 n if n as i32 > 22 * S_HOUR => TimePeriod::Days(1),
184 n if n > 90 * S_MINUTE => TimePeriod::Hours(max(n as i32 / S_HOUR, 2)),
185 n if n > 45 * S_MINUTE => TimePeriod::Hours(1),
186 n if n > 90 => TimePeriod::Minutes(max(n / S_MINUTE, 2)),
187 n if n > 45 => TimePeriod::Minutes(1),
188 n if n > 10 => TimePeriod::Seconds(n),
189 0..=10 => TimePeriod::Now,
190 _ => TimePeriod::Eternity,
191 };
192
193 vec![period]
194 }
195
196 fn precise_period(self) -> Vec<TimePeriod> {
197 let mut periods = vec![];
198
199 let (years, reminder) = self.split_years();
200 if let Some(years) = years {
201 periods.push(TimePeriod::Years(years as i16));
202 }
203
204 let (months, reminder) = reminder.split_months();
205 if let Some(months) = months {
206 periods.push(TimePeriod::Months(months as i32));
207 }
208
209 let (weeks, reminder) = reminder.split_weeks();
210 if let Some(weeks) = weeks {
211 periods.push(TimePeriod::Weeks(weeks as i32));
212 }
213
214 let (days, reminder) = reminder.split_days();
215 if let Some(days) = days {
216 periods.push(TimePeriod::Days(days as i32));
217 }
218
219 let (hours, reminder) = reminder.split_hours();
220 if let Some(hours) = hours {
221 periods.push(TimePeriod::Hours(hours as i32));
222 }
223
224 let (minutes, reminder) = reminder.split_minutes();
225 if let Some(minutes) = minutes {
226 periods.push(TimePeriod::Minutes(minutes));
227 }
228
229 let (seconds, reminder) = reminder.split_seconds();
230 if let Some(seconds) = seconds {
231 periods.push(TimePeriod::Seconds(seconds));
232 }
233
234 let (millis, reminder) = reminder.split_milliseconds();
235 if let Some(millis) = millis {
236 periods.push(TimePeriod::Millis(millis));
237 }
238
239 let (micros, reminder) = reminder.split_microseconds();
240 if let Some(micros) = micros {
241 periods.push(TimePeriod::Micros(micros));
242 }
243
244 let (nanos, reminder) = reminder.split_nanoseconds();
245 if let Some(nanos) = nanos {
246 periods.push(TimePeriod::Nanos(nanos));
247 }
248
249 debug_assert!(reminder.is_zero());
250
251 if periods.is_empty() {
252 periods.push(TimePeriod::Seconds(0));
253 }
254
255 periods
256 }
257
258 fn split_years(self) -> (Option<i64>, Self) {
260 let years = self.0.get_days() / Self::DAYS_IN_YEAR;
261 let reminder = self
262 .0
263 .checked_sub((years * Self::DAYS_IN_YEAR).days())
264 .unwrap();
265 Self::normalize_split(years as i64, reminder)
266 }
267
268 fn split_months(self) -> (Option<i64>, Self) {
270 let months = self.0.get_days() / Self::DAYS_IN_MONTH;
271 let reminder = self
272 .0
273 .checked_sub((months * Self::DAYS_IN_MONTH).days())
274 .unwrap();
275 Self::normalize_split(months as i64, reminder)
276 }
277
278 fn split_weeks(self) -> (Option<i64>, Self) {
280 let weeks = self.0.get_weeks();
281 let reminder = self.0.checked_sub(weeks.weeks()).unwrap();
282 Self::normalize_split(weeks as i64, reminder)
283 }
284
285 fn split_days(self) -> (Option<i64>, Self) {
287 let days = self.0.get_days();
288 let reminder = self.0.checked_sub(days.days()).unwrap();
289 Self::normalize_split(days as i64, reminder)
290 }
291
292 fn split_hours(self) -> (Option<i64>, Self) {
294 let hours = self.0.get_hours();
295 let reminder = self.0.checked_sub(hours.hours()).unwrap();
296 Self::normalize_split(hours as i64, reminder)
297 }
298
299 fn split_minutes(self) -> (Option<i64>, Self) {
301 let minutes = self.0.get_minutes();
302 let reminder = self.0.checked_sub(minutes.minutes()).unwrap();
303 Self::normalize_split(minutes, reminder)
304 }
305
306 fn split_seconds(self) -> (Option<i64>, Self) {
308 let seconds = self.0.get_seconds();
309 let reminder = self.0.checked_sub(seconds.seconds()).unwrap();
310 Self::normalize_split(seconds, reminder)
311 }
312
313 fn split_milliseconds(self) -> (Option<i64>, Self) {
315 let millis = self.0.get_milliseconds();
316 let reminder = self.0.checked_sub(millis.milliseconds()).unwrap();
317 Self::normalize_split(millis, reminder)
318 }
319
320 fn split_microseconds(self) -> (Option<i64>, Self) {
322 let micros = self.0.get_microseconds();
323 let reminder = self.0.checked_sub(micros.microseconds()).unwrap();
324 Self::normalize_split(micros, reminder)
325 }
326
327 fn split_nanoseconds(self) -> (Option<i64>, Self) {
329 let nanos = self.0.get_nanoseconds();
330 let reminder = self.0.checked_sub(nanos.nanoseconds()).unwrap();
331 Self::normalize_split(nanos, reminder)
332 }
333
334 fn normalize_split(
335 wholes: impl Into<Option<i64>>,
336 reminder: jiff::Span,
337 ) -> (Option<i64>, Self) {
338 let wholes = wholes.into().map(i64::abs).filter(|x| *x > 0);
339 (wholes, Self(reminder))
340 }
341
342 pub fn is_zero(self) -> bool {
343 self.0.is_zero()
344 }
345
346 fn locale_en(&self, accuracy: Accuracy) -> String {
347 let tense = self.tense(accuracy);
348 self.to_text_en(accuracy, tense)
349 }
350}
351
352impl fmt::Display for HumanTime {
353 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
354 let accuracy = if f.alternate() {
355 Accuracy::Precise
356 } else {
357 Accuracy::Rough
358 };
359
360 f.pad(&self.locale_en(accuracy))
361 }
362}
363
364impl From<jiff::Span> for HumanTime {
365 fn from(duration: jiff::Span) -> Self {
366 Self(duration)
367 }
368}
369
370impl From<jiff::Zoned> for HumanTime {
371 fn from(dt: jiff::Zoned) -> Self {
372 Self(dt.since(&jiff::Zoned::now()).unwrap())
373 }
374}
375
376impl From<SystemTime> for HumanTime {
377 fn from(st: SystemTime) -> Self {
378 jiff::Timestamp::try_from(st).unwrap().into()
379 }
380}
381
382impl From<jiff::Timestamp> for HumanTime {
383 fn from(dt: jiff::Timestamp) -> Self {
384 dt.since(jiff::Zoned::now()).unwrap().into()
385 }
386}
387
388impl Humanize for jiff::Span {
389 fn humanize(&self) -> String {
390 format!("{}", HumanTime::from(*self))
391 }
392}
393
394impl Humanize for jiff::Zoned {
395 fn humanize(&self) -> String {
396 format!("{}", HumanTime::from(self.clone()))
397 }
398}
399
400impl Humanize for SystemTime {
401 fn humanize(&self) -> String {
402 HumanTime::from(*self).to_string()
403 }
404}