2026-02-08 20:07:09 +00:00
|
|
|
//! Time utilities (enabled with "time" feature)
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "time")]
|
|
|
|
|
use chrono::{DateTime, Utc, TimeZone};
|
|
|
|
|
|
|
|
|
|
/// Time formatting utilities
|
|
|
|
|
#[cfg(feature = "time")]
|
|
|
|
|
pub mod format {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
/// Format timestamp for display in different languages
|
|
|
|
|
pub fn display_date(timestamp: &str, language: &str) -> Result<String, chrono::ParseError> {
|
|
|
|
|
let dt = DateTime::parse_from_rfc3339(timestamp)?
|
|
|
|
|
.with_timezone(&Utc);
|
|
|
|
|
|
|
|
|
|
match language {
|
|
|
|
|
"es" => Ok(dt.format("%d de %B de %Y").to_string()),
|
|
|
|
|
_ => Ok(dt.format("%B %d, %Y").to_string()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Format timestamp with time for display
|
|
|
|
|
pub fn display_datetime(timestamp: &str, language: &str) -> Result<String, chrono::ParseError> {
|
|
|
|
|
let dt = DateTime::parse_from_rfc3339(timestamp)?
|
|
|
|
|
.with_timezone(&Utc);
|
|
|
|
|
|
|
|
|
|
match language {
|
|
|
|
|
"es" => Ok(dt.format("%d de %B de %Y a las %H:%M UTC").to_string()),
|
|
|
|
|
_ => Ok(dt.format("%B %d, %Y at %H:%M UTC").to_string()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Format relative time (e.g., "2 hours ago")
|
|
|
|
|
pub fn relative_time(timestamp: &str, language: &str) -> Result<String, chrono::ParseError> {
|
|
|
|
|
let dt = DateTime::parse_from_rfc3339(timestamp)?
|
|
|
|
|
.with_timezone(&Utc);
|
|
|
|
|
let now = Utc::now();
|
|
|
|
|
let diff = now.signed_duration_since(dt);
|
|
|
|
|
|
|
|
|
|
let (value, unit) = if diff.num_seconds() < 60 {
|
|
|
|
|
(diff.num_seconds(), match language {
|
|
|
|
|
"es" => if diff.num_seconds() == 1 { "segundo" } else { "segundos" },
|
|
|
|
|
_ => if diff.num_seconds() == 1 { "second" } else { "seconds" },
|
|
|
|
|
})
|
|
|
|
|
} else if diff.num_minutes() < 60 {
|
|
|
|
|
(diff.num_minutes(), match language {
|
|
|
|
|
"es" => if diff.num_minutes() == 1 { "minuto" } else { "minutos" },
|
|
|
|
|
_ => if diff.num_minutes() == 1 { "minute" } else { "minutes" },
|
|
|
|
|
})
|
|
|
|
|
} else if diff.num_hours() < 24 {
|
|
|
|
|
(diff.num_hours(), match language {
|
|
|
|
|
"es" => if diff.num_hours() == 1 { "hora" } else { "horas" },
|
|
|
|
|
_ => if diff.num_hours() == 1 { "hour" } else { "hours" },
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
(diff.num_days(), match language {
|
|
|
|
|
"es" => if diff.num_days() == 1 { "día" } else { "días" },
|
|
|
|
|
_ => if diff.num_days() == 1 { "day" } else { "days" },
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let ago = match language {
|
|
|
|
|
"es" => "hace",
|
|
|
|
|
_ => "ago",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(format!("{} {} {}", value, unit, ago))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Time zone utilities
|
|
|
|
|
#[cfg(feature = "time")]
|
|
|
|
|
pub mod timezone {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
/// Convert UTC timestamp to local timezone display
|
|
|
|
|
pub fn to_local_display(timestamp: &str) -> Result<String, chrono::ParseError> {
|
|
|
|
|
let dt = DateTime::parse_from_rfc3339(timestamp)?;
|
|
|
|
|
// In WASM, we can't access system timezone easily, so return UTC
|
|
|
|
|
Ok(dt.format("%Y-%m-%d %H:%M UTC").to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get common timezone offsets
|
|
|
|
|
pub fn common_timezones() -> Vec<(&'static str, i32)> {
|
|
|
|
|
vec![
|
|
|
|
|
("UTC", 0),
|
|
|
|
|
("EST", -5),
|
|
|
|
|
("PST", -8),
|
|
|
|
|
("CET", 1),
|
|
|
|
|
("JST", 9),
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Duration utilities
|
|
|
|
|
#[cfg(feature = "time")]
|
|
|
|
|
pub mod duration {
|
|
|
|
|
use chrono::Duration;
|
|
|
|
|
|
|
|
|
|
/// Parse duration from string (e.g., "1h30m", "45s")
|
|
|
|
|
pub fn parse_duration(input: &str) -> Result<Duration, String> {
|
|
|
|
|
let mut total_seconds = 0i64;
|
|
|
|
|
let mut current_number = String::new();
|
|
|
|
|
|
|
|
|
|
for ch in input.chars() {
|
|
|
|
|
if ch.is_ascii_digit() {
|
|
|
|
|
current_number.push(ch);
|
|
|
|
|
} else if ch.is_alphabetic() {
|
|
|
|
|
if current_number.is_empty() {
|
|
|
|
|
return Err("No number before unit".to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let number: i64 = current_number.parse()
|
|
|
|
|
.map_err(|_| "Invalid number")?;
|
|
|
|
|
|
|
|
|
|
let seconds = match ch {
|
|
|
|
|
's' => number,
|
|
|
|
|
'm' => number * 60,
|
|
|
|
|
'h' => number * 3600,
|
|
|
|
|
'd' => number * 86400,
|
|
|
|
|
_ => return Err(format!("Unknown unit: {}", ch)),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
total_seconds += seconds;
|
|
|
|
|
current_number.clear();
|
|
|
|
|
} else if ch.is_whitespace() {
|
|
|
|
|
continue;
|
|
|
|
|
} else {
|
|
|
|
|
return Err(format!("Invalid character: {}", ch));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !current_number.is_empty() {
|
|
|
|
|
// Assume seconds if no unit specified
|
|
|
|
|
let number: i64 = current_number.parse()
|
|
|
|
|
.map_err(|_| "Invalid number")?;
|
|
|
|
|
total_seconds += number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(Duration::seconds(total_seconds))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Format duration for display
|
|
|
|
|
pub fn format_duration(duration: Duration, language: &str) -> String {
|
|
|
|
|
let total_seconds = duration.num_seconds();
|
|
|
|
|
|
|
|
|
|
if total_seconds < 60 {
|
|
|
|
|
match language {
|
|
|
|
|
"es" => format!("{} segundos", total_seconds),
|
|
|
|
|
_ => format!("{} seconds", total_seconds),
|
|
|
|
|
}
|
|
|
|
|
} else if total_seconds < 3600 {
|
|
|
|
|
let minutes = total_seconds / 60;
|
|
|
|
|
match language {
|
|
|
|
|
"es" => format!("{} minutos", minutes),
|
|
|
|
|
_ => format!("{} minutes", minutes),
|
|
|
|
|
}
|
|
|
|
|
} else if total_seconds < 86400 {
|
|
|
|
|
let hours = total_seconds / 3600;
|
|
|
|
|
let remaining_minutes = (total_seconds % 3600) / 60;
|
|
|
|
|
if remaining_minutes == 0 {
|
|
|
|
|
match language {
|
|
|
|
|
"es" => format!("{} horas", hours),
|
|
|
|
|
_ => format!("{} hours", hours),
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
match language {
|
|
|
|
|
"es" => format!("{} horas {} minutos", hours, remaining_minutes),
|
|
|
|
|
_ => format!("{} hours {} minutes", hours, remaining_minutes),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let days = total_seconds / 86400;
|
|
|
|
|
match language {
|
|
|
|
|
"es" => format!("{} días", days),
|
|
|
|
|
_ => format!("{} days", days),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No-op implementations for when time feature is not enabled
|
|
|
|
|
#[cfg(not(feature = "time"))]
|
|
|
|
|
pub mod format {
|
|
|
|
|
pub fn display_date(_timestamp: &str, _language: &str) -> Result<String, &'static str> {
|
|
|
|
|
Err("Time feature not enabled")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn display_datetime(_timestamp: &str, _language: &str) -> Result<String, &'static str> {
|
|
|
|
|
Err("Time feature not enabled")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn relative_time(_timestamp: &str, _language: &str) -> Result<String, &'static str> {
|
|
|
|
|
Err("Time feature not enabled")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "time")]
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_duration_parsing() {
|
|
|
|
|
use duration::parse_duration;
|
|
|
|
|
|
|
|
|
|
assert_eq!(parse_duration("30s").unwrap().num_seconds(), 30);
|
|
|
|
|
assert_eq!(parse_duration("5m").unwrap().num_seconds(), 300);
|
|
|
|
|
assert_eq!(parse_duration("2h").unwrap().num_seconds(), 7200);
|
|
|
|
|
assert_eq!(parse_duration("1d").unwrap().num_seconds(), 86400);
|
|
|
|
|
assert_eq!(parse_duration("1h30m").unwrap().num_seconds(), 5400);
|
|
|
|
|
assert_eq!(parse_duration("120").unwrap().num_seconds(), 120); // No unit = seconds
|
|
|
|
|
|
|
|
|
|
assert!(parse_duration("invalid").is_err());
|
|
|
|
|
assert!(parse_duration("5x").is_err()); // Invalid unit
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "time")]
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_duration_formatting() {
|
|
|
|
|
use duration::{parse_duration, format_duration};
|
|
|
|
|
|
|
|
|
|
let dur = parse_duration("30s").unwrap();
|
|
|
|
|
assert_eq!(format_duration(dur, "en"), "30 seconds");
|
|
|
|
|
assert_eq!(format_duration(dur, "es"), "30 segundos");
|
|
|
|
|
|
|
|
|
|
let dur = parse_duration("5m").unwrap();
|
|
|
|
|
assert_eq!(format_duration(dur, "en"), "5 minutes");
|
|
|
|
|
assert_eq!(format_duration(dur, "es"), "5 minutos");
|
|
|
|
|
|
|
|
|
|
let dur = parse_duration("2h30m").unwrap();
|
|
|
|
|
assert_eq!(format_duration(dur, "en"), "2 hours 30 minutes");
|
|
|
|
|
assert_eq!(format_duration(dur, "es"), "2 horas 30 minutos");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(feature = "time"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_time_feature_disabled() {
|
|
|
|
|
let result = format::display_date("2023-01-01T00:00:00Z", "en");
|
|
|
|
|
assert!(result.is_err());
|
|
|
|
|
}
|
2026-02-08 20:37:49 +00:00
|
|
|
}
|