Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add time_ago module #28

Merged
merged 1 commit into from
Dec 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ thiserror = { version = "1.0.48", optional = true }
num = { version = "0.4", optional = true }
num-derive = { version = "0.4", optional = true }
num-traits = { version = "0.2", optional = true }

chrono = { version = "0.4.31", optional = true }

# Edit `Makefile` and `src/lib.src` after making changes in this section:
[features]
Expand All @@ -43,7 +43,7 @@ full = [
"bill",
"number-to-words",
"get-bank-name-by-card-number",

"time-ago",
]
add-ordinal-suffix = []
commas = []
Expand All @@ -59,6 +59,7 @@ serde = ["dep:serde"]
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"]
time-ago = ["dep:thiserror", "dep:chrono"]

[package.metadata.docs.rs]
all-features = true
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ verity-card-number:
@ echo ""
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

phone-number:
@ echo ""
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@ pub mod number_to_words;

#[cfg(feature = "get-bank-name-by-card-number")]
pub mod get_bank_name_by_card_number;

#[cfg(feature = "time-ago")]
pub mod time_ago;
224 changes: 224 additions & 0 deletions src/time_ago/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
use chrono::{DateTime, Local, NaiveDateTime, TimeZone};
use thiserror::Error;

const MINUTE: i64 = 60;
const HOUR: i64 = MINUTE * 60;
const DAY: i64 = HOUR * 24;
const WEEK: i64 = DAY * 7;
const MONTH: i64 = DAY * 30;
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<str>) -> Result<i64, TimeAgoError> {
let datetime = datetime.as_ref();
let date_obj = get_date_time(datetime)?;

Ok(date_obj.timestamp())
}

/// Converts datetime to Chrono `DateTime<Local>`
///
/// # 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<str>) -> Result<DateTime<Local>, 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<impl AsRef<str>>) -> Result<String, TimeAgoError> {
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 elapsed <= 1 && elapsed >= -1 {
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 test_phone_number {
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());
}
}