use chrono::{DateTime, Utc};
use std::cmp::Ordering;
use std::fmt::Display;
pub(crate) const EM_DASH: &str = "\u{2014}";
#[allow(dead_code)]
pub(crate) fn or_em_dash(opt: Option<impl Display>) -> String {
match opt {
Some(v) => v.to_string(),
None => EM_DASH.to_string(),
}
}
pub(crate) fn truncate(s: &str, max_chars: usize) -> String {
if max_chars <= 3 {
return s.chars().take(max_chars).collect();
}
let char_count = s.chars().count();
if char_count <= max_chars {
s.to_string()
} else {
let truncated: String = s.chars().take(max_chars - 3).collect();
format!("{}...", truncated)
}
}
pub(crate) fn format_tokens(n: u64) -> String {
if n >= 1_000_000 {
let m = n as f64 / 1_000_000.0;
if m.fract() == 0.0 {
format!("{}M", m as u64)
} else {
format!("{:.1}M", m)
}
} else if n >= 1_000 {
let k = n as f64 / 1_000.0;
if k.fract() == 0.0 {
format!("{}k", k as u64)
} else {
format!("{:.1}k", k)
}
} else {
n.to_string()
}
}
pub(crate) fn format_stars(stars: u64) -> String {
if stars >= 1_000_000 {
format!("{:.1}m", stars as f64 / 1_000_000.0)
} else if stars >= 1_000 {
format!("{:.1}k", stars as f64 / 1_000.0)
} else {
stars.to_string()
}
}
pub(crate) fn parse_date(date_str: &str) -> Option<DateTime<Utc>> {
if let Ok(dt) = date_str.parse::<DateTime<Utc>>() {
return Some(dt);
}
chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
.ok()
.and_then(|d| d.and_hms_opt(0, 0, 0))
.map(|ndt| DateTime::from_naive_utc_and_offset(ndt, Utc))
}
pub(crate) fn format_relative_time(dt: &DateTime<Utc>) -> String {
let delta = Utc::now().signed_duration_since(*dt);
let minutes = delta.num_minutes();
let hours = delta.num_hours();
let days = delta.num_days();
let weeks = days / 7;
let months = days / 30;
if months > 0 {
format!("{}mo ago", months)
} else if weeks > 0 {
format!("{}w ago", weeks)
} else if days > 0 {
format!("{}d ago", days)
} else if hours > 0 {
format!("{}h ago", hours)
} else {
format!("{}m ago", minutes.max(1))
}
}
pub(crate) fn format_relative_time_from_str(ts: &str) -> String {
parse_date(ts)
.map(|dt| format_relative_time(&dt))
.unwrap_or_else(|| ts.to_string())
}
pub(crate) fn parse_date_to_numeric(date_str: &str) -> Option<f64> {
let parts: Vec<&str> = date_str.split('-').collect();
if parts.len() == 3 {
let year: f64 = parts[0].parse().ok()?;
let month: f64 = parts[1].parse().ok()?;
let day: f64 = parts[2].parse().ok()?;
Some(year * 10000.0 + month * 100.0 + day)
} else {
None
}
}
pub(crate) fn cmp_opt_f64(a: Option<f64>, b: Option<f64>) -> Ordering {
match (a, b) {
(Some(a), Some(b)) => a.partial_cmp(&b).unwrap_or(Ordering::Equal),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
#[test]
fn test_or_em_dash_some() {
assert_eq!(or_em_dash(Some(42)), "42");
assert_eq!(or_em_dash(Some("hello")), "hello");
}
#[test]
fn test_or_em_dash_none() {
assert_eq!(or_em_dash(None::<String>), "\u{2014}");
}
#[test]
fn test_truncate_short() {
assert_eq!(truncate("hi", 10), "hi");
}
#[test]
fn test_truncate_exact() {
assert_eq!(truncate("hello", 5), "hello");
}
#[test]
fn test_truncate_long() {
assert_eq!(truncate("hello world", 8), "hello...");
}
#[test]
fn test_truncate_tiny_max() {
assert_eq!(truncate("hello", 3), "hel");
}
#[test]
fn test_format_tokens() {
assert_eq!(format_tokens(500), "500");
assert_eq!(format_tokens(1000), "1k");
assert_eq!(format_tokens(128000), "128k");
assert_eq!(format_tokens(1500000), "1.5M");
assert_eq!(format_tokens(2000000), "2M");
}
#[test]
fn test_format_stars() {
assert_eq!(format_stars(0), "0");
assert_eq!(format_stars(999), "999");
assert_eq!(format_stars(1000), "1.0k");
assert_eq!(format_stars(1234567), "1.2m");
}
#[test]
fn format_relative_time_minutes() {
let dt = Utc::now() - Duration::minutes(5);
assert_eq!(format_relative_time(&dt), "5m ago");
}
#[test]
fn format_relative_time_minimum_one_minute() {
let dt = Utc::now() - Duration::seconds(10);
assert_eq!(format_relative_time(&dt), "1m ago");
}
#[test]
fn format_relative_time_hours() {
let dt = Utc::now() - Duration::hours(3);
assert_eq!(format_relative_time(&dt), "3h ago");
}
#[test]
fn format_relative_time_days() {
let dt = Utc::now() - Duration::days(2);
assert_eq!(format_relative_time(&dt), "2d ago");
}
#[test]
fn format_relative_time_weeks() {
let dt = Utc::now() - Duration::weeks(3);
assert_eq!(format_relative_time(&dt), "3w ago");
}
#[test]
fn format_relative_time_months() {
let dt = Utc::now() - Duration::days(90);
assert_eq!(format_relative_time(&dt), "3mo ago");
}
#[test]
fn parse_date_iso() {
let result = parse_date("2024-06-15");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(dt.format("%Y-%m-%d").to_string(), "2024-06-15");
}
#[test]
fn parse_date_rfc3339() {
let result = parse_date("2024-06-15T12:30:00Z");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(
dt.format("%Y-%m-%dT%H:%M:%S").to_string(),
"2024-06-15T12:30:00"
);
}
#[test]
fn parse_date_rfc3339_positive_offset() {
let result = parse_date("2024-06-15T12:30:00+05:30");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(
dt.format("%Y-%m-%dT%H:%M:%S").to_string(),
"2024-06-15T07:00:00"
);
}
#[test]
fn parse_date_rfc3339_negative_offset() {
let result = parse_date("2024-06-15T12:30:00-07:00");
assert!(result.is_some());
let dt = result.unwrap();
assert_eq!(
dt.format("%Y-%m-%dT%H:%M:%S").to_string(),
"2024-06-15T19:30:00"
);
}
#[test]
fn parse_date_invalid() {
assert!(parse_date("not-a-date").is_none());
assert!(parse_date("").is_none());
}
#[test]
fn test_parse_date_to_numeric() {
assert_eq!(parse_date_to_numeric("2024-01-15"), Some(20240115.0));
assert_eq!(parse_date_to_numeric("not-a-date"), None);
}
#[test]
fn test_cmp_opt_f64() {
assert_eq!(cmp_opt_f64(Some(1.0), Some(2.0)), Ordering::Less);
assert_eq!(cmp_opt_f64(Some(2.0), Some(1.0)), Ordering::Greater);
assert_eq!(cmp_opt_f64(Some(1.0), None), Ordering::Less);
assert_eq!(cmp_opt_f64(None, Some(1.0)), Ordering::Greater);
assert_eq!(cmp_opt_f64(None, None), Ordering::Equal);
}
}