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