diff --git a/Cargo.toml b/Cargo.toml index bd9002c..b45ecc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,13 +43,12 @@ full = [ "number-to-words", "get-bank-name-by-card-number", "extract-card-number", - "time-ago", + "time-diff", "get-place-by-iran-national-id", "half-space", "legal-id", "words-to-number", "sheba", - "remaining-time", ] # For now, by default we enable all features: @@ -71,13 +70,12 @@ bill = ["dep:num", "dep:num-derive", "dep:num-traits", "dep:thiserror"] number-to-words = ["dep:thiserror", "commas"] get-bank-name-by-card-number = ["dep:thiserror"] extract-card-number = [] -time-ago = ["dep:thiserror", "dep:chrono"] +time-diff = ["dep:thiserror", "dep:chrono"] get-place-by-iran-national-id = ["dep:thiserror"] half-space = [] legal-id = ["dep:thiserror"] words-to-number = ["dep:thiserror", "commas", "digits", "remove-ordinal-suffix"] sheba = ["dep:thiserror"] -remaining-time = ["time-ago"] [package.metadata.docs.rs] all-features = true diff --git a/Makefile b/Makefile index 8dfc8e9..89ee87a 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ all: build check test docs fmt: cargo fmt -build: full default add-ordinal-suffix commas digits find-capital-by-province persian-chars national-id remove-ordinal-suffix url-fix verity-card-number time-ago phone-number bill number-to-words get-bank-name-by-card-number extract-card-number get-place-by-iran-national-id half-space legal-id words-to-number sheba remaining-time +build: full default add-ordinal-suffix commas digits find-capital-by-province persian-chars national-id remove-ordinal-suffix url-fix verity-card-number phone-number bill number-to-words get-bank-name-by-card-number extract-card-number get-place-by-iran-national-id half-space legal-id words-to-number sheba time-diff check: clippy lint @@ -84,11 +84,6 @@ verity-card-number: cargo build --no-default-features --features=verity-card-number @ ls -sh target/debug/*.rlib -time-ago: - @ echo "" - cargo build --no-default-features --features=time-ago - @ ls -sh target/debug/*.rlib - number-plate: @ echo "" cargo build --no-default-features --features=number-plate @@ -146,8 +141,7 @@ sheba: cargo build --no-default-features --features=sheba @ ls -sh target/debug/*.rlib -remaining-time: +time-diff: @ echo "" - cargo build --no-default-features --features=remaining-time + cargo build --no-default-features --features=time-diff @ ls -sh target/debug/*.rlib - diff --git a/README.md b/README.md index 4ae070a..18fa4ff 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Rust🦀 implementation of [Persian-Tools](https://github.com/persian-tools/pers - phone_number - remove_ordinal_suffix - sheba -- time_ago +- time_diff - url_fix - verity_card_number - words_to_number diff --git a/src/lib.rs b/src/lib.rs index fa0360b..677079e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,13 +14,12 @@ feature = "number-to-words", feature = "get-bank-name-by-card-number", feature = "extract-card-number", - feature = "time-ago", feature = "get-place-by-iran-national-id", feature = "half-space", feature = "legal-id", feature = "words-to-number", feature = "sheba", - feature = "remaining-time", + feature = "time-diff", )))] compile_error!("No available Cargo feature is included"); @@ -72,8 +71,8 @@ pub mod get_bank_name_by_card_number; #[cfg(feature = "extract-card-number")] pub mod extract_card_number; -#[cfg(feature = "time-ago")] -pub mod time_ago; +#[cfg(feature = "time-diff")] +pub mod time_diff; #[cfg(feature = "get-place-by-iran-national-id")] pub mod get_place_by_iran_national_id; @@ -89,6 +88,3 @@ pub mod words_to_number; #[cfg(feature = "sheba")] pub mod sheba; - -#[cfg(feature = "remaining-time")] -pub mod remaining_time; diff --git a/src/remaining_time/mod.rs b/src/remaining_time/mod.rs deleted file mode 100644 index 6021401..0000000 --- a/src/remaining_time/mod.rs +++ /dev/null @@ -1,192 +0,0 @@ -use std::fmt; - -use crate::time_ago::{ - convert_to_timestamp, get_current_timestamp, TimeAgoError, DAY, HOUR, MINUTE, MONTH, YEAR, -}; - -#[derive(Debug, PartialEq)] -pub struct RemainingTime { - pub years: u32, - pub months: u8, - pub days: u8, - pub hours: u8, - pub minutes: u8, - pub seconds: u8, - pub is_finished: bool, -} - -impl fmt::Display for RemainingTime { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Format the struct fields as needed - - let mut periods: Vec = Vec::new(); - - if self.years > 0 { - periods.push(format!("{} سال", self.years)) - } - if self.months > 0 { - periods.push(format!("{} ماه", self.months)) - } - if self.days > 0 { - periods.push(format!("{} روز", self.days)) - } - if self.hours > 0 { - periods.push(format!("{} ساعت", self.hours)) - } - if self.minutes > 0 { - periods.push(format!("{} دقیقه", self.minutes)) - } - if self.seconds > 0 { - periods.push(format!("{} ثانیه", self.seconds)) - } - - write!(f, "{}", periods.join(" و ")) - } -} - -/// returns [RemainingTime] as result if the datetime has a valid format -/// -/// in [RemainingTime] you can use its field like ```remaining_time.hours``` or ```remaining_time.days``` -/// also Display trais is implmented for [RemainingTime] so if you could try try ```println("{}" , remaining_time)``` and a string like : ```۱ سال و ۱ ماه و ۲ روز و ۳ ساعت و ۵ دقیقه و ۸ ثانیه``` -/// -/// # Warning -/// This function is desgined to only works for these date time formats : -/// -/// -/// - `%Y-%m-%d %H:%M:%S`: Sortable format -/// - `%Y/%m/%d %H:%M:%S`: Sortable format -/// - `%Y-%m-%dT%H:%M:%S%:z`: ISO 8601 with timezone offset -/// - `%Y-%m-%dT%H:%M:%S%.3f%:z`: ISO 8601 with milliseconds and timezone offset -/// - `%a, %d %b %Y %H:%M:%S %z`: RFC 2822 Format -/// -/// -/// timezone is set with the current timezone of the OS. -/// -/// ``` -/// use rust_persian_tools::remaining_time::{remaining_time , RemainingTime}; -/// use chrono::{Duration, Local}; -/// -/// let current_time = Local::now(); -/// let due_date = current_time -/// + Duration::weeks(320) -/// + Duration::hours(7) -/// + Duration::minutes(13) -/// + Duration::seconds(37); -/// let formatted_time = due_date.format("%Y-%m-%d %H:%M:%S").to_string(); -/// -/// assert_eq!( -/// remaining_time(&formatted_time).unwrap(), -/// RemainingTime { -/// years: 6, -/// months: 1, -/// days: 20, -/// hours: 7, -/// minutes: 13, -/// seconds: 37, -/// is_finished: false, -/// } -/// ); -/// -/// assert_eq!( -/// format!("{}", remaining_time(&formatted_time).unwrap()), -/// String::from("6 سال و 1 ماه و 20 روز و 7 ساعت و 13 دقیقه و 37 ثانیه") -/// ); -/// -/// -/// ``` -pub fn remaining_time(datetime: impl AsRef) -> Result { - let datetime = datetime.as_ref(); - - let due_date = convert_to_timestamp(datetime)?; - let now = get_current_timestamp(); - - let mut remaining_timestamp = due_date - now; - - if remaining_timestamp <= 0 { - // if its due - return Ok(RemainingTime { - years: 0, - months: 0, - days: 0, - hours: 0, - minutes: 0, - seconds: 0, - is_finished: true, - }); - } - - let years: u32 = (remaining_timestamp / YEAR) as u32; - remaining_timestamp %= YEAR; - - let months: u8 = ((remaining_timestamp / MONTH) % MONTH) as u8; - remaining_timestamp %= MONTH; - - let days: u8 = ((remaining_timestamp / DAY) % DAY) as u8; - remaining_timestamp %= DAY; - - let hours: u8 = ((remaining_timestamp / HOUR) % HOUR) as u8; - remaining_timestamp %= HOUR; - - let minutes: u8 = ((remaining_timestamp / MINUTE) % MINUTE) as u8; - remaining_timestamp %= MINUTE; - - let seconds: u8 = remaining_timestamp as u8; - - Ok(RemainingTime { - years, - months, - days, - hours, - minutes, - seconds, - is_finished: false, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::{Duration, Local}; - - #[test] - fn remaining_time_test() { - let current_time = Local::now(); - let due_date = current_time - + Duration::weeks(320) - + Duration::hours(7) - + Duration::minutes(13) - + Duration::seconds(37); - let formatted_time = due_date.format("%Y-%m-%d %H:%M:%S").to_string(); - - assert_eq!( - remaining_time(formatted_time).unwrap(), - RemainingTime { - years: 6, - months: 1, - days: 20, - hours: 7, - minutes: 13, - seconds: 37, - is_finished: false, - } - ); - } - - #[test] - fn remaining_time_as_string_test() { - let current_time = Local::now(); - let due_date = - current_time + Duration::weeks(340) + Duration::minutes(12) + Duration::seconds(37); - let formatted_time = due_date.format("%Y-%m-%d %H:%M:%S").to_string(); - - assert_eq!( - format!("{}", remaining_time(formatted_time).unwrap()), - String::from("6 سال و 6 ماه و 10 روز و 12 دقیقه و 37 ثانیه") - ); - } - - #[test] - fn remaining_time_fail_test() { - assert!(remaining_time("123:12312").is_err()); - } -} diff --git a/src/time_ago/mod.rs b/src/time_ago/mod.rs deleted file mode 100644 index 2f0f2f1..0000000 --- a/src/time_ago/mod.rs +++ /dev/null @@ -1,224 +0,0 @@ -use chrono::{DateTime, Local, NaiveDateTime, TimeZone}; -use thiserror::Error; - -pub(crate) const MINUTE: i64 = 60; -pub(crate) const HOUR: i64 = MINUTE * 60; -pub(crate) const DAY: i64 = HOUR * 24; -pub(crate) const WEEK: i64 = DAY * 7; -pub(crate) const MONTH: i64 = DAY * 30; -pub(crate) const YEAR: i64 = DAY * 365; - -#[derive(Error, Debug)] -pub enum TimeAgoError { - #[error("Wrong datetime format !")] - InvalidDateTimeFormat, - #[error("Unexpected error happened !")] - Unknown, -} -/// Converts a valid datetime to timestamp -/// -/// # Warning -/// This function is desgined to only works for these date time formats : -/// -/// -/// - `%Y-%m-%d %H:%M:%S`: Sortable format -/// - `%Y/%m/%d %H:%M:%S`: Sortable format -/// - `%Y-%m-%dT%H:%M:%S%:z`: ISO 8601 with timezone offset -/// - `%Y-%m-%dT%H:%M:%S%.3f%:z`: ISO 8601 with milliseconds and timezone offset -/// - `%a, %d %b %Y %H:%M:%S %z`: RFC 2822 Format -/// -/// -/// timezone is set with the current timezone of the OS. -/// -/// # Examples -/// -/// ``` -/// use rust_persian_tools::time_ago::convert_to_timestamp; -/// -/// assert!(convert_to_timestamp("2023/12/30 12:21:13").is_ok()); -/// assert!(convert_to_timestamp("2023/12/30 25:21:13").is_err()); -/// ``` -pub fn convert_to_timestamp(datetime: impl AsRef) -> Result { - let datetime = datetime.as_ref(); - let date_obj = get_date_time(datetime)?; - - Ok(date_obj.timestamp()) -} - -/// Converts datetime to Chrono `DateTime` -/// -/// # Warning -/// This function is desgined to only works for these date time formats : -/// -/// - `%Y-%m-%d %H:%M:%S`: Sortable format -/// - `%Y/%m/%d %H:%M:%S`: Sortable format -/// - `%Y-%m-%dT%H:%M:%S%:z`: ISO 8601 with timezone offset -/// - `%Y-%m-%dT%H:%M:%S%.3f%:z`: ISO 8601 with milliseconds and timezone offset -/// - `%a, %d %b %Y %H:%M:%S %z`: RFC 2822 Format -/// -/// timezone is set with the current timezone of the OS. -/// -/// # Examples -/// -/// ``` -/// use rust_persian_tools::time_ago::get_date_time; -/// -/// assert!(get_date_time("2019/03/18 12:22:14").is_ok()); -/// assert!(get_date_time("20192/03/18 12:22:14").is_err()); -/// ``` -pub fn get_date_time(datetime: impl AsRef) -> Result, TimeAgoError> { - let datetime = datetime.as_ref(); - - let formats = [ - "%Y-%m-%d %H:%M:%S", // Sortable format - "%Y/%m/%d %H:%M:%S", // Sortable format - "%Y-%m-%dT%H:%M:%S%:z", // ISO 8601 with timezone offset - "%Y-%m-%dT%H:%M:%S%.3f%:z", // ISO 8601 with milliseconds and timezone offset - "%a, %d %b %Y %H:%M:%S %z", // RFC 2822 Format - ]; - - for format in formats { - if let Ok(parsed) = NaiveDateTime::parse_from_str(datetime, format) { - // Successfully parsed, convert to timestamp - let datetime_with_timezone = Local.from_local_datetime(&parsed).earliest(); - return match datetime_with_timezone { - Some(local_date_time) => Ok(local_date_time), - None => Err(TimeAgoError::Unknown), - }; - } - } - - Err(TimeAgoError::InvalidDateTimeFormat) -} - -/// Returns current timestamp -/// -/// # Warning -/// -/// timezone is set with the current timezone of the OS. -/// -pub fn get_current_timestamp() -> i64 { - let now = Local::now(); - now.timestamp() -} - -/// Returns a string based on how much time is remaining or passed based on the givin datetime -/// -/// # Warning -/// This function is desgined to only works for these date time formats : -/// -/// - `%Y-%m-%d %H:%M:%S`: Sortable format -/// - `%Y/%m/%d %H:%M:%S`: Sortable format -/// - `%Y-%m-%dT%H:%M:%S%:z`: ISO 8601 with timezone offset -/// - `%Y-%m-%dT%H:%M:%S%.3f%:z`: ISO 8601 with milliseconds and timezone offset -/// - `%a, %d %b %Y %H:%M:%S %z`: RFC 2822 Format -/// -/// timezone is set with the current timezone of the OS. -/// -/// # Examples -/// -/// ``` -/// use rust_persian_tools::time_ago::time_ago; -/// use chrono::{Duration,Local}; -/// -/// let current_time = Local::now(); -/// let ten_minutes_ago = current_time - Duration::minutes(10); -/// let formatted_time = ten_minutes_ago.format("%Y-%m-%d %H:%M:%S").to_string(); // create datetime string from 10 minutes ago -/// assert!(time_ago(Some(&formatted_time)).is_ok_and(|datetime| datetime == "10 دقیقه قبل")); -/// ``` -pub fn time_ago(datetime: Option>) -> Result { - if datetime.is_none() { - return Ok("اکنون".to_string()); - } - - let binding = datetime.unwrap(); - let datetime = binding.as_ref(); - - let ts_now = get_current_timestamp(); - let ts = convert_to_timestamp(datetime)?; - - let elapsed = ts_now - ts; - - if (-1..=1).contains(&elapsed) { - return Ok("اکنون".to_string()); - } - - let pre_or_next = if elapsed > 0 { "قبل" } else { "بعد" }; - - let elapsed = elapsed.abs(); - - if elapsed < MINUTE { - let left = (elapsed as f64).round(); - Ok(format!("{} {} {}", left, "ثانیه", pre_or_next)) - } else if elapsed < HOUR { - let left = ((elapsed / MINUTE) as f64).round(); - Ok(format!("{} {} {}", left, "دقیقه", pre_or_next)) - } else if elapsed < DAY { - let left = ((elapsed / HOUR) as f64).round(); - Ok(format!("{} {} {}", left, "ساعت", pre_or_next)) - } else if elapsed < WEEK { - let left = ((elapsed / DAY) as f64).round(); - Ok(format!("{} {} {} {}", "حدود", left, "روز", pre_or_next)) - } else if elapsed < MONTH { - let left = ((elapsed / WEEK) as f64).round(); - Ok(format!("{} {} {} {}", "حدود", left, "هفته", pre_or_next)) - } else if elapsed < YEAR { - let left = ((elapsed / MONTH) as f64).round(); - Ok(format!("{} {} {} {}", "حدود", left, "ماه", pre_or_next)) - } else { - let left = ((elapsed / YEAR) as f64).round(); - Ok(format!("{} {} {} {}", "حدود", left, "سال", pre_or_next)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::Duration; - - #[test] - fn test_time_ago_now() { - let current_time = Local::now(); - // let ten_minutes_ago = current_time - Duration::minutes(10); - let formatted_time = current_time.format("%Y-%m-%dT%H:%M:%S%:z").to_string(); - - assert!(time_ago(Some(&formatted_time)).is_ok_and(|datetime| datetime == "اکنون")); - } - - #[test] - fn test_time_ago_10_min_ago() { - let current_time = Local::now(); - let ten_minutes_ago = current_time - Duration::minutes(10); - let formatted_time = ten_minutes_ago.format("%Y-%m-%d %H:%M:%S").to_string(); - - assert!(time_ago(Some(&formatted_time)).is_ok_and(|datetime| datetime == "10 دقیقه قبل")); - } - - #[test] - fn test_time_ago_next_2_weeks() { - let current_time = Local::now(); - let ten_minutes_ago = current_time + Duration::weeks(2); - let formatted_time = ten_minutes_ago - .format("%a, %d %b %Y %H:%M:%S %z") - .to_string(); - - assert!(time_ago(Some(&formatted_time)).is_ok_and(|datetime| datetime == "حدود 2 هفته بعد")); - } - - #[test] - fn test_time_ago_next_3_months() { - let current_time = Local::now(); - let ten_minutes_ago = current_time + Duration::days(31 * 3); - let formatted_time = ten_minutes_ago - .format("%Y-%m-%dT%H:%M:%S%.3f%:z") - .to_string(); - - assert!(time_ago(Some(&formatted_time)).is_ok_and(|datetime| datetime == "حدود 3 ماه بعد")); - } - - #[test] - fn test_check_valid_date_time() { - assert!(get_date_time("2019/03/18 12:22:14").is_ok()); - assert!(get_date_time("20192/03/18 12:22:14").is_err()); - } -} diff --git a/src/time_diff/mod.rs b/src/time_diff/mod.rs new file mode 100644 index 0000000..cfefa12 --- /dev/null +++ b/src/time_diff/mod.rs @@ -0,0 +1,366 @@ +use chrono::{DateTime, Local, NaiveDateTime, TimeZone}; +use thiserror::Error; + +pub(crate) const MINUTE: i64 = 60; +pub(crate) const HOUR: i64 = MINUTE * 60; +pub(crate) const DAY: i64 = HOUR * 24; +pub(crate) const MONTH: i64 = DAY * 30; +pub(crate) const YEAR: i64 = DAY * 365; + +#[derive(Error, Debug)] +pub enum TimeAgoError { + #[error("Wrong datetime format !")] + InvalidDateTimeFormat, + #[error("Unexpected error happened !")] + Unknown, +} + +/// The [TimeDiff] stuct has two methods , `short_form()` & `long_form()` \ +/// the `short_form()` returns a short desciption about time diffrence\ +/// - 5 دقیقه قبل +/// - حدود 2 هفته بعد +/// +/// the `long_form()` returns a long and exact desciption about time diffrence\ +/// - 6 سال و 6 ماه و 10 روز و 12 دقیقه و 37 ثانیه بعد +/// +#[derive(Debug, PartialEq)] +pub struct TimeDiff { + pub years: u32, + pub months: u8, + pub days: u8, + pub hours: u8, + pub minutes: u8, + pub seconds: u8, + pub is_remaining_time: bool, +} + +impl TimeDiff { + pub fn long_form(&self) -> String { + let mut periods: Vec = Vec::new(); + + let pre_or_next = pre_or_next(self.is_remaining_time); + + if self.years > 0 { + periods.push(format!("{} سال", self.years)) + } + if self.months > 0 { + periods.push(format!("{} ماه", self.months)) + } + if self.days > 0 { + periods.push(format!("{} روز", self.days)) + } + if self.hours > 0 { + periods.push(format!("{} ساعت", self.hours)) + } + if self.minutes > 0 { + periods.push(format!("{} دقیقه", self.minutes)) + } + if self.seconds > 0 { + periods.push(format!("{} ثانیه", self.seconds)) + } + + format!("{} {}", periods.join(" و "), pre_or_next) + } + + pub fn short_form(&self) -> String { + let pre_or_next = pre_or_next(self.is_remaining_time); + + if self.years != 0 { + format!("{} {} {} {}", "حدود", &self.years, "سال", pre_or_next) + } else if self.months != 0 { + format!("{} {} {} {}", "حدود", &self.months, "ماه", pre_or_next) + } else if self.days > 7 { + format!("{} {} {} {}", "حدود", &self.days / 7, "هفته", pre_or_next) + } else if self.days != 0 { + format!("{} {} {} {}", "حدود", &self.days, "روز", pre_or_next) + } else if self.hours != 0 { + format!("{} {} {}", &self.hours, "ساعت", pre_or_next) + } else if self.minutes != 0 { + format!("{} {} {}", &self.minutes, "دقیقه", pre_or_next) + } else if self.seconds != 0 { + format!("{} {} {}", &self.seconds, "ثانیه", pre_or_next) + } else { + "اکنون".to_string() + } + } +} + +fn pre_or_next(is_remaining_time: bool) -> String { + if is_remaining_time { + "بعد".to_owned() + } else { + "قبل".to_owned() + } +} + +/// Converts a valid datetime to timestamp +/// +/// # Warning +/// This function is desgined to only works for these date time formats : +/// +/// +/// - `%Y-%m-%d %H:%M:%S`: Sortable format +/// - `%Y/%m/%d %H:%M:%S`: Sortable format +/// - `%Y-%m-%dT%H:%M:%S%:z`: ISO 8601 with timezone offset +/// - `%Y-%m-%dT%H:%M:%S%.3f%:z`: ISO 8601 with milliseconds and timezone offset +/// - `%a, %d %b %Y %H:%M:%S %z`: RFC 2822 Format +/// +/// +/// timezone is set with the current timezone of the OS. +/// +/// # Examples +/// +/// ``` +/// use rust_persian_tools::time_diff::convert_to_timestamp; +/// +/// assert!(convert_to_timestamp("2023/12/30 12:21:13").is_ok()); +/// assert!(convert_to_timestamp("2023/12/30 25:21:13").is_err()); +/// ``` +pub fn convert_to_timestamp(datetime: impl AsRef) -> Result { + let datetime = datetime.as_ref(); + let date_obj = get_date_time(datetime)?; + + Ok(date_obj.timestamp()) +} + +/// Converts datetime to Chrono `DateTime` +/// +/// # Warning +/// This function is desgined to only works for these date time formats : +/// +/// - `%Y-%m-%d %H:%M:%S`: Sortable format +/// - `%Y/%m/%d %H:%M:%S`: Sortable format +/// - `%Y-%m-%dT%H:%M:%S%:z`: ISO 8601 with timezone offset +/// - `%Y-%m-%dT%H:%M:%S%.3f%:z`: ISO 8601 with milliseconds and timezone offset +/// - `%a, %d %b %Y %H:%M:%S %z`: RFC 2822 Format +/// +/// timezone is set with the current timezone of the OS. +/// +/// # Examples +/// +/// ``` +/// use rust_persian_tools::time_diff::get_date_time; +/// +/// assert!(get_date_time("2019/03/18 12:22:14").is_ok()); +/// assert!(get_date_time("20192/03/18 12:22:14").is_err()); +/// ``` +pub fn get_date_time(datetime: impl AsRef) -> Result, TimeAgoError> { + let datetime = datetime.as_ref(); + + let formats = [ + "%Y-%m-%d %H:%M:%S", // Sortable format + "%Y/%m/%d %H:%M:%S", // Sortable format + "%Y-%m-%dT%H:%M:%S%:z", // ISO 8601 with timezone offset + "%Y-%m-%dT%H:%M:%S%.3f%:z", // ISO 8601 with milliseconds and timezone offset + "%a, %d %b %Y %H:%M:%S %z", // RFC 2822 Format + ]; + + for format in formats { + if let Ok(parsed) = NaiveDateTime::parse_from_str(datetime, format) { + // Successfully parsed, convert to timestamp + let datetime_with_timezone = Local.from_local_datetime(&parsed).earliest(); + return match datetime_with_timezone { + Some(local_date_time) => Ok(local_date_time), + None => Err(TimeAgoError::Unknown), + }; + } + } + + Err(TimeAgoError::InvalidDateTimeFormat) +} + +/// Returns current timestamp +/// +/// # Warning +/// +/// timezone is set with the current timezone of the OS. +/// +pub fn get_current_timestamp() -> i64 { + let now = Local::now(); + now.timestamp() +} + +/// Returns a [TimeDiff] stuct based on how much time is remaining or passed based on the givin datetime\ +/// The [TimeDiff] stuct has two methods , `short_form()` & `long_form()` \ +/// the `short_form()` returns a short desciption about time diffrence\ +/// - 5 دقیقه قبل +/// - حدود 2 هفته بعد +/// +/// the `long_form()` returns a long and exact desciption about time diffrence\ +/// - 6 سال و 6 ماه و 10 روز و 12 دقیقه و 37 ثانیه بعد +/// +/// # Warning +/// This function is desgined to only works for these date time formats : +/// +/// - `%Y-%m-%d %H:%M:%S`: Sortable format +/// - `%Y/%m/%d %H:%M:%S`: Sortable format +/// - `%Y-%m-%dT%H:%M:%S%:z`: ISO 8601 with timezone offset +/// - `%Y-%m-%dT%H:%M:%S%.3f%:z`: ISO 8601 with milliseconds and timezone offset +/// - `%a, %d %b %Y %H:%M:%S %z`: RFC 2822 Format +/// +/// timezone is set with the current timezone of the OS. +/// +/// # Examples +/// +/// ``` +/// use rust_persian_tools::time_diff::{TimeDiff , time_diff}; +/// use chrono::{Duration,Local}; +/// +/// let current_time = Local::now(); +/// let due_date = current_time +/// + Duration::weeks(320) +/// + Duration::hours(7) +/// + Duration::minutes(13) +/// + Duration::seconds(37); +/// let formatted_time = due_date.format("%Y-%m-%d %H:%M:%S").to_string(); +/// assert_eq!( +/// time_diff(formatted_time).unwrap(), +/// TimeDiff { +/// years: 6, +/// months: 1, +/// days: 20, +/// hours: 7, +/// minutes: 13, +/// seconds: 37, +/// is_remaining_time: true, +/// } +/// ); +/// +/// // Example with short_form() +/// let current_time = Local::now(); +/// let ten_minutes_ago = current_time - Duration::minutes(10); +/// let formatted_time = ten_minutes_ago.format("%Y-%m-%d %H:%M:%S").to_string(); // create datetime string from 10 minutes ago +/// assert!(time_diff(formatted_time).is_ok_and(|datetime| datetime.short_form() == "10 دقیقه قبل")); +/// ``` +pub fn time_diff(datetime: impl AsRef) -> Result { + let datetime = datetime.as_ref(); + + let ts_now = get_current_timestamp(); + let ts = convert_to_timestamp(datetime)?; + + let timestamp_diff = ts - ts_now; + + let is_remaining_time = timestamp_diff > 0; + + let mut timestamp_diff = timestamp_diff.abs(); + + let years: u32 = (timestamp_diff / YEAR) as u32; + timestamp_diff %= YEAR; + + let months: u8 = ((timestamp_diff / MONTH) % MONTH) as u8; + timestamp_diff %= MONTH; + + let days: u8 = ((timestamp_diff / DAY) % DAY) as u8; + timestamp_diff %= DAY; + + let hours: u8 = ((timestamp_diff / HOUR) % HOUR) as u8; + timestamp_diff %= HOUR; + + let minutes: u8 = ((timestamp_diff / MINUTE) % MINUTE) as u8; + timestamp_diff %= MINUTE; + + let seconds: u8 = timestamp_diff as u8; + + Ok(TimeDiff { + years, + months, + days, + hours, + minutes, + seconds, + is_remaining_time, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Duration; + + #[test] + fn test_time_diff_now() { + let current_time = Local::now(); + // let ten_minutes_ago = current_time - Duration::minutes(10); + let formatted_time = current_time.format("%Y-%m-%dT%H:%M:%S%:z").to_string(); + + assert!(time_diff(&formatted_time).is_ok_and(|datetime| datetime.short_form() == "اکنون")); + } + + #[test] + fn test_time_diff_10_min_ago() { + let current_time = Local::now(); + let ten_minutes_ago = current_time - Duration::minutes(10); + let formatted_time = ten_minutes_ago.format("%Y-%m-%d %H:%M:%S").to_string(); + + // dbg!(time_diff(&formatted_time)) + assert!(time_diff(&formatted_time) + .is_ok_and(|datetime| datetime.short_form() == "10 دقیقه قبل")); + } + + #[test] + fn test_time_diff_next_2_weeks() { + let current_time = Local::now(); + let ten_minutes_ago = current_time + Duration::weeks(2); + let formatted_time = ten_minutes_ago + .format("%a, %d %b %Y %H:%M:%S %z") + .to_string(); + + assert!(time_diff(&formatted_time) + .is_ok_and(|datetime| datetime.short_form() == "حدود 2 هفته بعد")); + } + + #[test] + fn test_time_diff_next_3_months() { + let current_time = Local::now(); + let ten_minutes_ago = current_time + Duration::days(31 * 3); + let formatted_time = ten_minutes_ago + .format("%Y-%m-%dT%H:%M:%S%.3f%:z") + .to_string(); + + assert!(time_diff(&formatted_time) + .is_ok_and(|datetime| datetime.short_form() == "حدود 3 ماه بعد")); + } + + #[test] + fn test_time_diff_as_struct() { + let current_time = Local::now(); + let due_date = current_time + + Duration::weeks(320) + + Duration::hours(7) + + Duration::minutes(13) + + Duration::seconds(37); + let formatted_time = due_date.format("%Y-%m-%d %H:%M:%S").to_string(); + + assert_eq!( + time_diff(formatted_time).unwrap(), + TimeDiff { + years: 6, + months: 1, + days: 20, + hours: 7, + minutes: 13, + seconds: 37, + is_remaining_time: true, + } + ); + } + + #[test] + fn test_time_diff_as_long_form() { + let current_time = Local::now(); + let due_date = + current_time + Duration::weeks(340) + Duration::minutes(12) + Duration::seconds(37); + let formatted_time = due_date.format("%Y-%m-%d %H:%M:%S").to_string(); + + assert_eq!( + time_diff(formatted_time).unwrap().long_form(), + String::from("6 سال و 6 ماه و 10 روز و 12 دقیقه و 37 ثانیه بعد") + ); + } + + #[test] + fn test_check_valid_date_time() { + assert!(get_date_time("2019/03/18 12:22:14").is_ok()); + assert!(get_date_time("20192/03/18 12:22:14").is_err()); + } +}