1use chrono::{DateTime, Local, NaiveDateTime};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4pub enum DateTimeType {
5 Naive(NaiveDateTime),
6 Local(DateTime<Local>),
7}
8
9impl DateTimeType {
10 /// Converts the DateTimeType to a NaiveDateTime.
11 ///
12 /// If the DateTimeType is already a NaiveDateTime, it will be returned as is.
13 /// If the DateTimeType is a DateTime<Local>, it will be converted to a NaiveDateTime.
14 pub fn to_naive(&self) -> NaiveDateTime {
15 match self {
16 DateTimeType::Naive(naive) => *naive,
17 DateTimeType::Local(local) => local.naive_local(),
18 }
19 }
20}
21
22pub struct FormatDistance {
23 date: DateTimeType,
24 base_date: DateTimeType,
25 include_seconds: bool,
26 add_suffix: bool,
27 hide_prefix: bool,
28}
29
30impl FormatDistance {
31 pub fn new(date: DateTimeType, base_date: DateTimeType) -> Self {
32 Self {
33 date,
34 base_date,
35 include_seconds: false,
36 add_suffix: false,
37 hide_prefix: false,
38 }
39 }
40
41 pub fn from_now(date: DateTimeType) -> Self {
42 Self::new(date, DateTimeType::Local(Local::now()))
43 }
44
45 pub fn to_string(self) -> String {
46 format_distance(
47 self.date,
48 self.base_date.to_naive(),
49 self.include_seconds,
50 self.add_suffix,
51 self.hide_prefix,
52 )
53 }
54
55 pub fn include_seconds(mut self, include_seconds: bool) -> Self {
56 self.include_seconds = include_seconds;
57 self
58 }
59
60 pub fn add_suffix(mut self, add_suffix: bool) -> Self {
61 self.add_suffix = add_suffix;
62 self
63 }
64
65 pub fn hide_prefix(mut self, hide_prefix: bool) -> Self {
66 self.hide_prefix = hide_prefix;
67 self
68 }
69}
70
71/// Calculates the distance in seconds between two NaiveDateTime objects.
72/// It returns a signed integer denoting the difference. If `date` is earlier than `base_date`, the returned value will be negative.
73///
74/// ## Arguments
75///
76/// * `date` - A NaiveDateTime object representing the date of interest
77/// * `base_date` - A NaiveDateTime object representing the base date against which the comparison is made
78fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 {
79 let duration = date.signed_duration_since(base_date);
80 -duration.num_seconds()
81}
82
83/// Generates a string describing the time distance between two dates in a human-readable way.
84fn distance_string(
85 distance: i64,
86 include_seconds: bool,
87 add_suffix: bool,
88 hide_prefix: bool,
89) -> String {
90 let suffix = if distance < 0 { " from now" } else { " ago" };
91
92 let distance = distance.abs();
93
94 let minutes = distance / 60;
95 let hours = distance / 3_600;
96 let days = distance / 86_400;
97 let months = distance / 2_592_000;
98
99 let string = if distance < 5 && include_seconds {
100 if hide_prefix {
101 "5 seconds"
102 } else {
103 "less than 5 seconds"
104 }
105 .to_string()
106 } else if distance < 10 && include_seconds {
107 if hide_prefix {
108 "10 seconds"
109 } else {
110 "less than 10 seconds"
111 }
112 .to_string()
113 } else if distance < 20 && include_seconds {
114 if hide_prefix {
115 "20 seconds"
116 } else {
117 "less than 20 seconds"
118 }
119 .to_string()
120 } else if distance < 40 && include_seconds {
121 if hide_prefix {
122 "half a minute"
123 } else {
124 "half a minute"
125 }
126 .to_string()
127 } else if distance < 60 && include_seconds {
128 if hide_prefix {
129 "a minute"
130 } else {
131 "less than a minute"
132 }
133 .to_string()
134 } else if distance < 90 && include_seconds {
135 "1 minute".to_string()
136 } else if distance < 30 {
137 if hide_prefix {
138 "a minute"
139 } else {
140 "less than a minute"
141 }
142 .to_string()
143 } else if distance < 90 {
144 "1 minute".to_string()
145 } else if distance < 2_700 {
146 format!("{} minutes", minutes)
147 } else if distance < 5_400 {
148 if hide_prefix {
149 "1 hour"
150 } else {
151 "about 1 hour"
152 }
153 .to_string()
154 } else if distance < 86_400 {
155 if hide_prefix {
156 format!("{} hours", hours)
157 } else {
158 format!("about {} hours", hours)
159 }
160 .to_string()
161 } else if distance < 172_800 {
162 "1 day".to_string()
163 } else if distance < 2_592_000 {
164 format!("{} days", days)
165 } else if distance < 5_184_000 {
166 if hide_prefix {
167 "1 month"
168 } else {
169 "about 1 month"
170 }
171 .to_string()
172 } else if distance < 7_776_000 {
173 if hide_prefix {
174 "2 months"
175 } else {
176 "about 2 months"
177 }
178 .to_string()
179 } else if distance < 31_540_000 {
180 format!("{} months", months)
181 } else if distance < 39_425_000 {
182 if hide_prefix {
183 "1 year"
184 } else {
185 "about 1 year"
186 }
187 .to_string()
188 } else if distance < 55_195_000 {
189 if hide_prefix { "1 year" } else { "over 1 year" }.to_string()
190 } else if distance < 63_080_000 {
191 if hide_prefix {
192 "2 years"
193 } else {
194 "almost 2 years"
195 }
196 .to_string()
197 } else {
198 let years = distance / 31_536_000;
199 let remaining_months = (distance % 31_536_000) / 2_592_000;
200
201 if remaining_months < 3 {
202 if hide_prefix {
203 format!("{} years", years)
204 } else {
205 format!("about {} years", years)
206 }
207 .to_string()
208 } else if remaining_months < 9 {
209 if hide_prefix {
210 format!("{} years", years)
211 } else {
212 format!("over {} years", years)
213 }
214 .to_string()
215 } else {
216 if hide_prefix {
217 format!("{} years", years + 1)
218 } else {
219 format!("almost {} years", years + 1)
220 }
221 .to_string()
222 }
223 };
224
225 if add_suffix {
226 format!("{}{}", string, suffix)
227 } else {
228 string
229 }
230}
231
232/// Get the time difference between two dates into a relative human readable string.
233///
234/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc.
235///
236/// Use [naive_format_distance_from_now] to compare a NaiveDateTime against now.
237///
238/// # Arguments
239///
240/// * `date` - The NaiveDateTime to compare.
241/// * `base_date` - The NaiveDateTime to compare against.
242/// * `include_seconds` - A boolean. If true, distances less than a minute are more detailed
243/// * `add_suffix` - A boolean. If true, result indicates if the time is in the past or future
244///
245/// # Example
246///
247/// ```rust
248/// use chrono::DateTime;
249/// use ui::utils::format_distance;
250///
251/// fn time_between_moon_landings() -> String {
252/// let date = DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z").unwrap().naive_local();
253/// let base_date = DateTime::parse_from_rfc3339("1972-12-14T00:00:00Z").unwrap().naive_local();
254/// format!("There was {} between the first and last crewed moon landings.", naive_format_distance(date, base_date, false, false))
255/// }
256/// ```
257///
258/// Output: `"There was about 3 years between the first and last crewed moon landings."`
259pub fn format_distance(
260 date: DateTimeType,
261 base_date: NaiveDateTime,
262 include_seconds: bool,
263 add_suffix: bool,
264 hide_prefix: bool,
265) -> String {
266 let distance = distance_in_seconds(date.to_naive(), base_date);
267
268 distance_string(distance, include_seconds, add_suffix, hide_prefix)
269}
270
271/// Get the time difference between a date and now as relative human readable string.
272///
273/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc.
274///
275/// # Arguments
276///
277/// * `datetime` - The NaiveDateTime to compare with the current time.
278/// * `include_seconds` - A boolean. If true, distances less than a minute are more detailed
279/// * `add_suffix` - A boolean. If true, result indicates if the time is in the past or future
280///
281/// # Example
282///
283/// ```rust
284/// use chrono::DateTime;
285/// use ui::utils::naive_format_distance_from_now;
286///
287/// fn time_since_first_moon_landing() -> String {
288/// let date = DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z").unwrap().naive_local();
289/// format!("It's been {} since Apollo 11 first landed on the moon.", naive_format_distance_from_now(date, false, false))
290/// }
291/// ```
292///
293/// Output: `It's been over 54 years since Apollo 11 first landed on the moon.`
294pub fn format_distance_from_now(
295 datetime: DateTimeType,
296 include_seconds: bool,
297 add_suffix: bool,
298 hide_prefix: bool,
299) -> String {
300 let now = chrono::offset::Local::now().naive_local();
301
302 format_distance(datetime, now, include_seconds, add_suffix, hide_prefix)
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use chrono::NaiveDateTime;
309
310 #[test]
311 fn test_format_distance() {
312 let date = DateTimeType::Naive(
313 NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"),
314 );
315 let base_date = DateTimeType::Naive(
316 NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"),
317 );
318
319 assert_eq!(
320 "about 2 hours",
321 format_distance(date, base_date.to_naive(), false, false, false)
322 );
323 }
324
325 #[test]
326 fn test_format_distance_with_suffix() {
327 let date = DateTimeType::Naive(
328 NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"),
329 );
330 let base_date = DateTimeType::Naive(
331 NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"),
332 );
333
334 assert_eq!(
335 "about 2 hours from now",
336 format_distance(date, base_date.to_naive(), false, true, false)
337 );
338 }
339
340 #[test]
341 fn test_format_distance_from_now() {
342 let date = DateTimeType::Naive(
343 NaiveDateTime::parse_from_str("1969-07-20T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ")
344 .expect("Invalid NaiveDateTime for date"),
345 );
346
347 assert_eq!(
348 "over 54 years ago",
349 format_distance_from_now(date, false, true, false)
350 );
351 }
352
353 #[test]
354 fn test_format_distance_string() {
355 assert_eq!(
356 distance_string(3, false, false, false),
357 "less than a minute"
358 );
359 assert_eq!(
360 distance_string(7, false, false, false),
361 "less than a minute"
362 );
363 assert_eq!(
364 distance_string(13, false, false, false),
365 "less than a minute"
366 );
367 assert_eq!(
368 distance_string(21, false, false, false),
369 "less than a minute"
370 );
371 assert_eq!(distance_string(45, false, false, false), "1 minute");
372 assert_eq!(distance_string(61, false, false, false), "1 minute");
373 assert_eq!(distance_string(1920, false, false, false), "32 minutes");
374 assert_eq!(distance_string(3902, false, false, false), "about 1 hour");
375 assert_eq!(distance_string(18002, false, false, false), "about 5 hours");
376 assert_eq!(distance_string(86470, false, false, false), "1 day");
377 assert_eq!(distance_string(345880, false, false, false), "4 days");
378 assert_eq!(
379 distance_string(2764800, false, false, false),
380 "about 1 month"
381 );
382 assert_eq!(
383 distance_string(5184000, false, false, false),
384 "about 2 months"
385 );
386 assert_eq!(distance_string(10368000, false, false, false), "4 months");
387 assert_eq!(
388 distance_string(34694000, false, false, false),
389 "about 1 year"
390 );
391 assert_eq!(
392 distance_string(47310000, false, false, false),
393 "over 1 year"
394 );
395 assert_eq!(
396 distance_string(61503000, false, false, false),
397 "almost 2 years"
398 );
399 assert_eq!(
400 distance_string(160854000, false, false, false),
401 "about 5 years"
402 );
403 assert_eq!(
404 distance_string(236550000, false, false, false),
405 "over 7 years"
406 );
407 assert_eq!(
408 distance_string(249166000, false, false, false),
409 "almost 8 years"
410 );
411 }
412
413 #[test]
414 fn test_format_distance_string_include_seconds() {
415 assert_eq!(
416 distance_string(3, true, false, false),
417 "less than 5 seconds"
418 );
419 assert_eq!(
420 distance_string(7, true, false, false),
421 "less than 10 seconds"
422 );
423 assert_eq!(
424 distance_string(13, true, false, false),
425 "less than 20 seconds"
426 );
427 assert_eq!(distance_string(21, true, false, false), "half a minute");
428 assert_eq!(
429 distance_string(45, true, false, false),
430 "less than a minute"
431 );
432 assert_eq!(distance_string(61, true, false, false), "1 minute");
433 }
434}