use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
pub const DEFAULT_COOLDOWN_SECS: u64 = 3 * 24 * 60 * 60;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct PublishTime(i64);
impl PublishTime {
#[must_use]
pub fn now() -> Self {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs().cast_signed());
Self(secs)
}
#[must_use]
pub const fn from_unix_secs(secs: i64) -> Self {
Self(secs)
}
#[must_use]
pub fn parse_rfc3339(s: &str) -> Option<Self> {
OffsetDateTime::parse(s, &Rfc3339)
.ok()
.map(|dt| Self(dt.unix_timestamp()))
}
#[must_use]
pub const fn as_unix_secs(self) -> i64 {
self.0
}
#[must_use]
pub const fn age_secs_from(self, now: Self) -> u64 {
let diff = now.0.saturating_sub(self.0);
#[expect(
clippy::cast_sign_loss,
reason = "diff >= 0 guaranteed by the else branch check on the same line"
)]
if diff < 0 { 0 } else { diff as u64 }
}
}
#[must_use]
pub const fn is_within_cooldown(age_secs: u64, cooldown_secs: u64) -> bool {
age_secs < cooldown_secs
}
const MINUTE: u64 = 60;
const HOUR: u64 = 60 * MINUTE;
const DAY: u64 = 24 * HOUR;
const WEEK: u64 = 7 * DAY;
const MONTH: u64 = 30 * DAY;
const YEAR: u64 = 365 * DAY;
fn format_unit_ago(count: u64, unit: &str) -> String {
if count == 1 {
format!("1 {unit} ago")
} else {
format!("{count} {unit}s ago")
}
}
#[must_use]
pub fn format_relative_age(age_secs: u64) -> String {
if age_secs < MINUTE {
"just now".to_string()
} else if age_secs < HOUR {
format_unit_ago(age_secs / MINUTE, "minute")
} else if age_secs < DAY {
format_unit_ago(age_secs / HOUR, "hour")
} else if age_secs < WEEK {
format_unit_ago(age_secs / DAY, "day")
} else if age_secs < MONTH {
format_unit_ago(age_secs / WEEK, "week")
} else if age_secs < YEAR {
format_unit_ago(age_secs / MONTH, "month")
} else {
format_unit_ago(age_secs / YEAR, "year")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FreshnessSettings {
pub enabled: bool,
pub cooldown_secs: u64,
}
impl Default for FreshnessSettings {
fn default() -> Self {
Self {
enabled: true,
cooldown_secs: DEFAULT_COOLDOWN_SECS,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_rfc3339_cargo_bare_z() {
let t = PublishTime::parse_rfc3339("2026-07-18T23:05:13Z").unwrap();
assert_eq!(t.as_unix_secs(), 1_784_415_913);
}
#[test]
fn test_parse_rfc3339_pypi_six_digit_fraction() {
assert!(PublishTime::parse_rfc3339("2026-05-14T19:25:27.735762Z").is_some());
}
#[test]
fn test_parse_rfc3339_composer_numeric_offset() {
let a = PublishTime::parse_rfc3339("2026-01-02T08:56:05+00:00").unwrap();
let b = PublishTime::parse_rfc3339("2026-01-02T08:56:05Z").unwrap();
assert_eq!(a, b);
}
#[test]
fn test_parse_rfc3339_bundler_millisecond_fraction() {
assert!(PublishTime::parse_rfc3339("2024-01-15T10:30:00.000Z").is_some());
}
#[test]
fn test_parse_rfc3339_dart_fractional_form() {
assert!(PublishTime::parse_rfc3339("2025-03-10T14:22:05.123Z").is_some());
}
#[test]
fn test_parse_rfc3339_go_bare_z() {
assert!(PublishTime::parse_rfc3339("2026-02-01T00:00:00Z").is_some());
}
#[test]
fn test_parse_rfc3339_garbage_is_none() {
assert!(PublishTime::parse_rfc3339("not-a-timestamp").is_none());
}
#[test]
fn test_parse_rfc3339_empty_is_none() {
assert!(PublishTime::parse_rfc3339("").is_none());
}
#[test]
fn test_age_secs_from_future_timestamp_clamps_to_zero() {
let now = PublishTime::from_unix_secs(1_000);
let published_in_future = PublishTime::from_unix_secs(1_500);
assert_eq!(published_in_future.age_secs_from(now), 0);
}
#[test]
fn test_age_secs_from_normal_case() {
let published = PublishTime::from_unix_secs(1_000);
let now = PublishTime::from_unix_secs(1_360);
assert_eq!(published.age_secs_from(now), 360);
}
#[test]
fn test_age_secs_from_extreme_values_do_not_panic() {
let published = PublishTime::from_unix_secs(i64::MIN);
let now = PublishTime::from_unix_secs(i64::MAX);
let _ = published.age_secs_from(now);
let published = PublishTime::from_unix_secs(i64::MAX);
let now = PublishTime::from_unix_secs(i64::MIN);
assert_eq!(published.age_secs_from(now), 0);
}
#[test]
fn test_is_within_cooldown_future_timestamp_counts_as_within() {
let now = PublishTime::from_unix_secs(1_000);
let published_in_future = PublishTime::from_unix_secs(1_500);
let age = published_in_future.age_secs_from(now);
assert!(is_within_cooldown(age, DEFAULT_COOLDOWN_SECS));
}
#[test]
fn test_is_within_cooldown_at_boundary_is_false() {
assert!(!is_within_cooldown(200, 200));
}
#[test]
fn test_is_within_cooldown_one_below_boundary_is_true() {
assert!(is_within_cooldown(199, 200));
}
#[test]
fn test_format_relative_age_just_now() {
assert_eq!(format_relative_age(0), "just now");
assert_eq!(format_relative_age(59), "just now");
}
#[test]
fn test_format_relative_age_minutes() {
assert_eq!(format_relative_age(60), "1 minute ago");
assert_eq!(format_relative_age(119), "1 minute ago");
assert_eq!(format_relative_age(300), "5 minutes ago");
assert_eq!(format_relative_age(HOUR - 1), "59 minutes ago");
}
#[test]
fn test_format_relative_age_hours() {
assert_eq!(format_relative_age(HOUR), "1 hour ago");
assert_eq!(format_relative_age(2 * HOUR), "2 hours ago");
assert_eq!(format_relative_age(DAY - 1), "23 hours ago");
}
#[test]
fn test_format_relative_age_days() {
assert_eq!(format_relative_age(DAY), "1 day ago");
assert_eq!(format_relative_age(3 * DAY), "3 days ago");
assert_eq!(format_relative_age(WEEK - 1), "6 days ago");
}
#[test]
fn test_format_relative_age_weeks() {
assert_eq!(format_relative_age(WEEK), "1 week ago");
assert_eq!(format_relative_age(2 * WEEK), "2 weeks ago");
assert_eq!(format_relative_age(MONTH - 1), "4 weeks ago");
}
#[test]
fn test_format_relative_age_months() {
assert_eq!(format_relative_age(MONTH), "1 month ago");
assert_eq!(format_relative_age(5 * MONTH), "5 months ago");
assert_eq!(format_relative_age(YEAR - 1), "12 months ago");
}
#[test]
fn test_format_relative_age_years() {
assert_eq!(format_relative_age(YEAR), "1 year ago");
assert_eq!(format_relative_age(2 * YEAR), "2 years ago");
}
#[test]
fn test_freshness_settings_default() {
let settings = FreshnessSettings::default();
assert!(settings.enabled);
assert_eq!(settings.cooldown_secs, DEFAULT_COOLDOWN_SECS);
}
}