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 include_seconds(mut self, include_seconds: bool) -> Self {
46 self.include_seconds = include_seconds;
47 self
48 }
49
50 pub fn add_suffix(mut self, add_suffix: bool) -> Self {
51 self.add_suffix = add_suffix;
52 self
53 }
54
55 pub fn hide_prefix(mut self, hide_prefix: bool) -> Self {
56 self.hide_prefix = hide_prefix;
57 self
58 }
59}
60
61impl std::fmt::Display for FormatDistance {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 write!(
64 f,
65 "{}",
66 format_distance(
67 self.date,
68 self.base_date.to_naive(),
69 self.include_seconds,
70 self.add_suffix,
71 self.hide_prefix,
72 )
73 )
74 }
75}
76/// Calculates the distance in seconds between two [`NaiveDateTime`] objects.
77/// It returns a signed integer denoting the difference. If `date` is earlier than `base_date`, the returned value will be negative.
78///
79/// ## Arguments
80///
81/// * `date` - A [NaiveDateTime`] object representing the date of interest
82/// * `base_date` - A [NaiveDateTime`] object representing the base date against which the comparison is made
83fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 {
84 let duration = date.signed_duration_since(base_date);
85 -duration.num_seconds()
86}
87
88/// Generates a string describing the time distance between two dates in a human-readable way.
89fn distance_string(
90 distance: i64,
91 include_seconds: bool,
92 add_suffix: bool,
93 hide_prefix: bool,
94) -> String {
95 let suffix = if distance < 0 { " from now" } else { " ago" };
96
97 let distance = distance.abs();
98
99 let minutes = distance / 60;
100 let hours = distance / 3_600;
101 let days = distance / 86_400;
102 let months = distance / 2_592_000;
103
104 let string = if distance < 5 && include_seconds {
105 if hide_prefix {
106 "5 seconds"
107 } else {
108 "less than 5 seconds"
109 }
110 .to_string()
111 } else if distance < 10 && include_seconds {
112 if hide_prefix {
113 "10 seconds"
114 } else {
115 "less than 10 seconds"
116 }
117 .to_string()
118 } else if distance < 20 && include_seconds {
119 if hide_prefix {
120 "20 seconds"
121 } else {
122 "less than 20 seconds"
123 }
124 .to_string()
125 } else if distance < 40 && include_seconds {
126 "half a minute".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 [`format_distance_from_now`] to compare a NaiveDateTime against now.
237pub fn format_distance(
238 date: DateTimeType,
239 base_date: NaiveDateTime,
240 include_seconds: bool,
241 add_suffix: bool,
242 hide_prefix: bool,
243) -> String {
244 let distance = distance_in_seconds(date.to_naive(), base_date);
245
246 distance_string(distance, include_seconds, add_suffix, hide_prefix)
247}
248
249/// Get the time difference between a date and now as relative human readable string.
250///
251/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc.
252pub fn format_distance_from_now(
253 datetime: DateTimeType,
254 include_seconds: bool,
255 add_suffix: bool,
256 hide_prefix: bool,
257) -> String {
258 let now = chrono::offset::Local::now().naive_local();
259
260 format_distance(datetime, now, include_seconds, add_suffix, hide_prefix)
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use chrono::NaiveDateTime;
267
268 #[test]
269 fn test_format_distance() {
270 let date = DateTimeType::Naive(
271 #[allow(deprecated)]
272 NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"),
273 );
274 let base_date = DateTimeType::Naive(
275 #[allow(deprecated)]
276 NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"),
277 );
278
279 assert_eq!(
280 "about 2 hours",
281 format_distance(date, base_date.to_naive(), false, false, false)
282 );
283 }
284
285 #[test]
286 fn test_format_distance_with_suffix() {
287 let date = DateTimeType::Naive(
288 #[allow(deprecated)]
289 NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"),
290 );
291 let base_date = DateTimeType::Naive(
292 #[allow(deprecated)]
293 NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"),
294 );
295
296 assert_eq!(
297 "about 2 hours from now",
298 format_distance(date, base_date.to_naive(), false, true, false)
299 );
300 }
301
302 #[test]
303 fn test_format_distance_from_hms() {
304 let date = DateTimeType::Naive(
305 NaiveDateTime::parse_from_str("1969-07-20T11:22:33Z", "%Y-%m-%dT%H:%M:%SZ")
306 .expect("Invalid NaiveDateTime for date"),
307 );
308 let base_date = DateTimeType::Naive(
309 NaiveDateTime::parse_from_str("2024-02-01T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ")
310 .expect("Invalid NaiveDateTime for base_date"),
311 );
312
313 assert_eq!(
314 "over 54 years ago",
315 format_distance(date, base_date.to_naive(), false, true, false)
316 );
317 }
318
319 #[test]
320 fn test_format_distance_string() {
321 assert_eq!(
322 distance_string(3, false, false, false),
323 "less than a minute"
324 );
325 assert_eq!(
326 distance_string(7, false, false, false),
327 "less than a minute"
328 );
329 assert_eq!(
330 distance_string(13, false, false, false),
331 "less than a minute"
332 );
333 assert_eq!(
334 distance_string(21, false, false, false),
335 "less than a minute"
336 );
337 assert_eq!(distance_string(45, false, false, false), "1 minute");
338 assert_eq!(distance_string(61, false, false, false), "1 minute");
339 assert_eq!(distance_string(1920, false, false, false), "32 minutes");
340 assert_eq!(distance_string(3902, false, false, false), "about 1 hour");
341 assert_eq!(distance_string(18002, false, false, false), "about 5 hours");
342 assert_eq!(distance_string(86470, false, false, false), "1 day");
343 assert_eq!(distance_string(345880, false, false, false), "4 days");
344 assert_eq!(
345 distance_string(2764800, false, false, false),
346 "about 1 month"
347 );
348 assert_eq!(
349 distance_string(5184000, false, false, false),
350 "about 2 months"
351 );
352 assert_eq!(distance_string(10368000, false, false, false), "4 months");
353 assert_eq!(
354 distance_string(34694000, false, false, false),
355 "about 1 year"
356 );
357 assert_eq!(
358 distance_string(47310000, false, false, false),
359 "over 1 year"
360 );
361 assert_eq!(
362 distance_string(61503000, false, false, false),
363 "almost 2 years"
364 );
365 assert_eq!(
366 distance_string(160854000, false, false, false),
367 "about 5 years"
368 );
369 assert_eq!(
370 distance_string(236550000, false, false, false),
371 "over 7 years"
372 );
373 assert_eq!(
374 distance_string(249166000, false, false, false),
375 "almost 8 years"
376 );
377 }
378
379 #[test]
380 fn test_format_distance_string_include_seconds() {
381 assert_eq!(
382 distance_string(3, true, false, false),
383 "less than 5 seconds"
384 );
385 assert_eq!(
386 distance_string(7, true, false, false),
387 "less than 10 seconds"
388 );
389 assert_eq!(
390 distance_string(13, true, false, false),
391 "less than 20 seconds"
392 );
393 assert_eq!(distance_string(21, true, false, false), "half a minute");
394 assert_eq!(
395 distance_string(45, true, false, false),
396 "less than a minute"
397 );
398 assert_eq!(distance_string(61, true, false, false), "1 minute");
399 }
400}