//! Human-readable timestamp formatting for forum display. use chrono::{DateTime, Utc}; /// Relative timestamp for thread listings: "just now", "3m ago", "2h ago", etc. /// Falls back to "Jan 15" format for dates older than 7 days. pub fn relative_timestamp(dt: DateTime) -> String { let now = Utc::now(); let delta = now.signed_duration_since(dt); if delta.num_seconds() < 60 { "just now".into() } else if delta.num_minutes() < 60 { format!("{}m ago", delta.num_minutes()) } else if delta.num_hours() < 24 { format!("{}h ago", delta.num_hours()) } else if delta.num_days() < 7 { format!("{}d ago", delta.num_days()) } else { dt.format("%b %-d").to_string() } } /// Absolute timestamp for post display: "2026-03-13 10:30". pub fn post_timestamp(dt: DateTime) -> String { dt.format("%Y-%m-%d %H:%M").to_string() } #[cfg(test)] mod tests { use super::*; use chrono::Duration; #[test] fn just_now() { let dt = Utc::now() - Duration::seconds(30); assert_eq!(relative_timestamp(dt), "just now"); } #[test] fn minutes_ago() { let dt = Utc::now() - Duration::minutes(5); assert_eq!(relative_timestamp(dt), "5m ago"); } #[test] fn hours_ago() { let dt = Utc::now() - Duration::hours(3); assert_eq!(relative_timestamp(dt), "3h ago"); } #[test] fn days_ago() { let dt = Utc::now() - Duration::days(2); assert_eq!(relative_timestamp(dt), "2d ago"); } #[test] fn older_than_week() { let dt = Utc::now() - Duration::days(10); let result = relative_timestamp(dt); // Should be in "Jan 15" format assert!(!result.contains("ago"), "expected date format, got: {result}"); } #[test] fn post_timestamp_format() { let dt = chrono::NaiveDate::from_ymd_opt(2026, 3, 13) .unwrap() .and_hms_opt(10, 30, 0) .unwrap() .and_utc(); assert_eq!(post_timestamp(dt), "2026-03-13 10:30"); } #[test] fn boundary_59_seconds_is_just_now() { let dt = Utc::now() - Duration::seconds(59); assert_eq!(relative_timestamp(dt), "just now"); } #[test] fn boundary_60_seconds_is_1m() { let dt = Utc::now() - Duration::seconds(60); assert_eq!(relative_timestamp(dt), "1m ago"); } #[test] fn boundary_59_minutes_is_minutes() { let dt = Utc::now() - Duration::minutes(59); assert_eq!(relative_timestamp(dt), "59m ago"); } #[test] fn boundary_60_minutes_is_1h() { let dt = Utc::now() - Duration::minutes(60); assert_eq!(relative_timestamp(dt), "1h ago"); } #[test] fn boundary_23_hours_is_hours() { let dt = Utc::now() - Duration::hours(23); assert_eq!(relative_timestamp(dt), "23h ago"); } #[test] fn boundary_24_hours_is_1d() { let dt = Utc::now() - Duration::hours(24); assert_eq!(relative_timestamp(dt), "1d ago"); } #[test] fn boundary_6_days_is_days() { let dt = Utc::now() - Duration::days(6); assert_eq!(relative_timestamp(dt), "6d ago"); } #[test] fn boundary_7_days_is_date() { let dt = Utc::now() - Duration::days(7); let result = relative_timestamp(dt); assert!(!result.contains("ago"), "expected date format, got: {result}"); } #[test] fn post_timestamp_midnight() { let dt = chrono::NaiveDate::from_ymd_opt(2026, 1, 1) .unwrap() .and_hms_opt(0, 0, 0) .unwrap() .and_utc(); assert_eq!(post_timestamp(dt), "2026-01-01 00:00"); } #[test] fn zero_seconds_ago_is_just_now() { let dt = Utc::now(); assert_eq!(relative_timestamp(dt), "just now"); } }