rust_persian_tools/time_diff/
mod.rs1use chrono::{DateTime, Local, NaiveDateTime, TimeZone};
2use thiserror::Error;
3
4use crate::digits::{DigitsEn2Ar, DigitsEn2Fa};
5
6pub(crate) const MINUTE: i64 = 60;
7pub(crate) const HOUR: i64 = MINUTE * 60;
8pub(crate) const DAY: i64 = HOUR * 24;
9pub(crate) const MONTH: i64 = DAY * 30;
10pub(crate) const YEAR: i64 = DAY * 365;
11
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13#[derive(Error, Clone, Copy, Debug, Hash, PartialEq, Eq)]
14pub enum TimeAgoError {
15 #[error("Wrong datetime format !")]
16 InvalidDateTimeFormat,
17 #[error("Unexpected error happened !")]
18 Unknown,
19}
20
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22#[derive(Clone, Debug, Hash, PartialEq, Eq)]
23pub enum Timestamp {
24 String(String),
25 Integer(i64),
26}
27
28impl From<String> for Timestamp {
29 fn from(datetime_str: String) -> Self {
30 Timestamp::String(datetime_str)
31 }
32}
33
34impl From<&str> for Timestamp {
35 fn from(datetime_str: &str) -> Self {
36 Timestamp::String(datetime_str.to_string())
37 }
38}
39
40impl From<i64> for Timestamp {
41 fn from(timestamp: i64) -> Self {
42 Timestamp::Integer(timestamp)
43 }
44}
45
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
61pub struct TimeDiff {
62 pub years: u32,
63 pub months: u8,
64 pub days: u8,
65 pub hours: u8,
66 pub minutes: u8,
67 pub seconds: u8,
68 pub is_future: bool,
69}
70
71impl TimeDiff {
72 pub fn long_form(&self) -> String {
73 let mut periods: Vec<String> = Vec::new();
74
75 let pre_or_next = self.pre_or_next();
76
77 if self.years > 0 {
78 periods.push(format!("{} سال", self.years))
79 }
80 if self.months > 0 {
81 periods.push(format!("{} ماه", self.months))
82 }
83 if self.days > 0 {
84 periods.push(format!("{} روز", self.days))
85 }
86 if self.hours > 0 {
87 periods.push(format!("{} ساعت", self.hours))
88 }
89 if self.minutes > 0 {
90 periods.push(format!("{} دقیقه", self.minutes))
91 }
92 if self.seconds > 0 {
93 periods.push(format!("{} ثانیه", self.seconds))
94 }
95
96 format!("{} {}", periods.join(" و "), pre_or_next)
97 }
98
99 pub fn short_form(&self) -> String {
100 let pre_or_next = self.pre_or_next();
101
102 if self.years != 0 {
103 format!("{} {} {} {}", "حدود", &self.years, "سال", pre_or_next)
104 } else if self.months != 0 {
105 format!("{} {} {} {}", "حدود", &self.months, "ماه", pre_or_next)
106 } else if self.days > 7 {
107 format!("{} {} {} {}", "حدود", &self.days / 7, "هفته", pre_or_next)
108 } else if self.days != 0 {
109 format!("{} {} {} {}", "حدود", &self.days, "روز", pre_or_next)
110 } else if self.hours != 0 {
111 format!("{} {} {}", &self.hours, "ساعت", pre_or_next)
112 } else if self.minutes != 0 {
113 format!("{} {} {}", &self.minutes, "دقیقه", pre_or_next)
114 } else if self.seconds != 0 {
115 format!("{} {} {}", &self.seconds, "ثانیه", pre_or_next)
116 } else {
117 "اکنون".to_string()
118 }
119 }
120
121 pub fn short_form_fa_digits(&self) -> String {
122 self.short_form().digits_en_to_fa()
123 }
124
125 pub fn long_form_fa_digits(&self) -> String {
126 self.long_form().digits_en_to_fa()
127 }
128
129 pub fn short_form_ar_digits(&self) -> String {
130 self.short_form().digits_en_to_ar()
131 }
132
133 pub fn long_form_ar_digits(&self) -> String {
134 self.long_form().digits_en_to_ar()
135 }
136
137 pub fn pre_or_next(&self) -> String {
138 if self.is_future {
139 "بعد".to_owned()
140 } else {
141 "قبل".to_owned()
142 }
143 }
144}
145
146pub fn convert_to_timestamp(datetime: impl AsRef<str>) -> Result<i64, TimeAgoError> {
170 let datetime = datetime.as_ref();
171 let date_obj = get_date_time(datetime)?;
172
173 Ok(date_obj.timestamp())
174}
175
176pub fn get_date_time(datetime: impl AsRef<str>) -> Result<DateTime<Local>, TimeAgoError> {
198 let datetime = datetime.as_ref();
199
200 let formats = [
201 "%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S%:z", "%Y-%m-%dT%H:%M:%S%.3f%:z", "%a, %d %b %Y %H:%M:%S %z", ];
207
208 for format in formats {
209 if let Ok(parsed) = NaiveDateTime::parse_from_str(datetime, format) {
210 let datetime_with_timezone = Local.from_local_datetime(&parsed).earliest();
212 return match datetime_with_timezone {
213 Some(local_date_time) => Ok(local_date_time),
214 None => Err(TimeAgoError::Unknown),
215 };
216 }
217 }
218
219 Err(TimeAgoError::InvalidDateTimeFormat)
220}
221
222pub fn get_current_timestamp() -> i64 {
229 let now = Local::now();
230 now.timestamp()
231}
232
233pub fn time_diff_now(datetime: impl Into<Timestamp>) -> Result<TimeDiff, TimeAgoError> {
290 let ts_now = get_current_timestamp();
291 let ts = match datetime.into() {
292 Timestamp::String(datetime_str) => convert_to_timestamp(datetime_str)?,
293 Timestamp::Integer(timestamp) => timestamp,
294 };
295
296 let timestamp_diff = ts - ts_now;
297
298 Ok(get_time_diff(timestamp_diff))
299}
300
301pub fn time_diff_between(
370 start: impl Into<Timestamp>,
371 end: impl Into<Timestamp>,
372) -> Result<TimeDiff, TimeAgoError> {
373 let ts_start = match start.into() {
374 Timestamp::String(datetime_str) => convert_to_timestamp(datetime_str)?,
375 Timestamp::Integer(timestamp) => timestamp,
376 };
377
378 let ts_end = match end.into() {
379 Timestamp::String(datetime_str) => convert_to_timestamp(datetime_str)?,
380 Timestamp::Integer(timestamp) => timestamp,
381 };
382
383 let timestamp_diff = ts_end - ts_start;
384
385 Ok(get_time_diff(timestamp_diff))
386}
387
388fn get_time_diff(timestamp_diff: i64) -> TimeDiff {
389 let is_future = timestamp_diff > 0;
390
391 let mut timestamp_diff = timestamp_diff.abs();
392
393 let years: u32 = (timestamp_diff / YEAR) as u32;
394 timestamp_diff %= YEAR;
395
396 let months: u8 = ((timestamp_diff / MONTH) % MONTH) as u8;
397 timestamp_diff %= MONTH;
398
399 let days: u8 = ((timestamp_diff / DAY) % DAY) as u8;
400 timestamp_diff %= DAY;
401
402 let hours: u8 = ((timestamp_diff / HOUR) % HOUR) as u8;
403 timestamp_diff %= HOUR;
404
405 let minutes: u8 = ((timestamp_diff / MINUTE) % MINUTE) as u8;
406 timestamp_diff %= MINUTE;
407
408 let seconds: u8 = timestamp_diff as u8;
409
410 TimeDiff {
411 years,
412 months,
413 days,
414 hours,
415 minutes,
416 seconds,
417 is_future,
418 }
419}
420
421#[cfg(test)]
422mod tests {
423
424 use super::*;
425 use chrono::Duration;
426
427 #[test]
428 fn test_time_diff_now() {
429 let current_time = Local::now();
430 let formatted_time = current_time.format("%Y-%m-%dT%H:%M:%S%:z").to_string();
432
433 assert!(
434 time_diff_now(formatted_time).is_ok_and(|datetime| datetime.short_form() == "اکنون")
435 );
436 }
437
438 #[test]
439 fn test_time_diff_10_min_ago() {
440 let current_time = Local::now();
441 let ten_minutes_ago = current_time - Duration::try_minutes(10).unwrap();
442 let formatted_time = ten_minutes_ago.format("%Y-%m-%d %H:%M:%S").to_string();
443
444 assert!(time_diff_now(formatted_time)
446 .is_ok_and(|datetime| datetime.short_form() == "10 دقیقه قبل"));
447 }
448
449 #[test]
450 fn test_time_diff_between_to_datetime() {
451 let current_time = Local::now();
452 let start = current_time
453 + Duration::try_weeks(320).unwrap()
454 + Duration::try_hours(7).unwrap()
455 + Duration::try_minutes(13).unwrap()
456 + Duration::try_seconds(37).unwrap();
457
458 let end =
459 (current_time + Duration::try_weeks(150).unwrap() + Duration::try_hours(4).unwrap())
460 .timestamp();
461
462 let formatted_time = start.format("%Y-%m-%d %H:%M:%S").to_string();
463 assert_eq!(
464 time_diff_between(formatted_time, end).unwrap(),
465 TimeDiff {
466 years: 3,
467 months: 3,
468 days: 5,
469 hours: 3,
470 minutes: 13,
471 seconds: 37,
472 is_future: false,
473 }
474 );
475 }
476
477 #[test]
478 fn test_time_diff_between_to_datetime_with_long_format_persian_digits() {
479 let current_time = Local::now();
480 let start = current_time
481 + Duration::try_weeks(320).unwrap()
482 + Duration::try_hours(7).unwrap()
483 + Duration::try_minutes(13).unwrap()
484 + Duration::try_seconds(37).unwrap();
485
486 let end =
487 (current_time + Duration::try_weeks(150).unwrap() + Duration::try_hours(4).unwrap())
488 .timestamp();
489
490 let formatted_time = start.format("%Y-%m-%d %H:%M:%S").to_string();
491 assert_eq!(
492 time_diff_between(formatted_time, end)
493 .unwrap()
494 .long_form_fa_digits(),
495 "۳ سال و ۳ ماه و ۵ روز و ۳ ساعت و ۱۳ دقیقه و ۳۷ ثانیه قبل"
496 );
497 }
498
499 #[test]
500 fn test_time_diff_next_2_weeks() {
501 let current_time = Local::now();
502 let ten_minutes_ago = current_time + Duration::try_weeks(2).unwrap();
503 let formatted_time = ten_minutes_ago
504 .format("%a, %d %b %Y %H:%M:%S %z")
505 .to_string();
506
507 assert!(time_diff_now(formatted_time)
508 .is_ok_and(|datetime| datetime.short_form() == "حدود 2 هفته بعد"));
509 }
510
511 #[test]
512 fn test_time_diff_next_3_months() {
513 let current_time = Local::now();
514 let ten_minutes_ago = current_time + Duration::try_days(31 * 3).unwrap();
515 let formatted_time = ten_minutes_ago
516 .format("%Y-%m-%dT%H:%M:%S%.3f%:z")
517 .to_string();
518
519 assert!(time_diff_now(formatted_time)
520 .is_ok_and(|datetime| datetime.short_form() == "حدود 3 ماه بعد"));
521 }
522
523 #[test]
524 fn test_time_diff_as_struct() {
525 let current_time = Local::now();
526 let due_date = current_time
527 + Duration::try_weeks(320).unwrap()
528 + Duration::try_hours(7).unwrap()
529 + Duration::try_minutes(13).unwrap()
530 + Duration::try_seconds(37).unwrap();
531 let formatted_time = due_date.format("%Y-%m-%d %H:%M:%S").to_string();
532
533 assert_eq!(
534 time_diff_now(formatted_time).unwrap(),
535 TimeDiff {
536 years: 6,
537 months: 1,
538 days: 20,
539 hours: 7,
540 minutes: 13,
541 seconds: 37,
542 is_future: true,
543 }
544 );
545 }
546
547 #[test]
548 fn test_time_diff_as_long_form() {
549 let current_time = Local::now();
550 let due_date = current_time
551 + Duration::try_weeks(340).unwrap()
552 + Duration::try_minutes(12).unwrap()
553 + Duration::try_seconds(37).unwrap();
554 let formatted_time = due_date.format("%Y-%m-%d %H:%M:%S").to_string();
555
556 assert_eq!(
557 time_diff_now(formatted_time).unwrap().long_form(),
558 String::from("6 سال و 6 ماه و 10 روز و 12 دقیقه و 37 ثانیه بعد")
559 );
560 }
561
562 #[test]
563 fn test_check_valid_date_time() {
564 assert!(get_date_time("2019/03/18 12:22:14").is_ok());
565 assert!(get_date_time("20192/03/18 12:22:14").is_err());
566 }
567}