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