pub use chrono::{DateTime, Local, TimeDelta, Utc};
use regex::Regex;
use relativetime::NegativeRelativeTime;
use std::time::Duration;
pub trait Clock {
fn now(&self) -> DateTime<Utc>;
}
#[derive(Debug, Default)]
pub struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> DateTime<Utc> {
Utc::now()
}
}
pub trait HasAge {
fn created_utc(&self) -> DateTime<Utc>;
fn created_local(&self) -> DateTime<Local> {
self.created_utc().with_timezone(&Local)
}
fn age<C: Clock>(&self, clock: &C) -> TimeDelta {
let birthday = self.created_utc();
clock.now() - birthday
}
fn relative_age<C: Clock>(&self, clock: &C) -> String {
let age = self.age(clock).as_seconds_f64();
let d = Duration::from_secs(age.trunc() as u64);
let s = d.to_relative_in_past();
let re = Regex::new("^1 (?<unit>[a-z]+)s ago$").unwrap();
re.replace(&s, "1 $unit ago").to_string()
}
}
#[cfg(test)]
mod tests {
mod clock {
use super::super::*;
use std::ops::Sub;
#[test]
fn it_returns_the_system_time() {
let clock = SystemClock::default();
let delta = Utc::now().sub(clock.now());
let secs = delta.num_seconds();
assert_eq!(secs, 0);
}
}
mod has_age {
use super::super::*;
use crate::clock::HasAge;
use crate::test_utils::FrozenClock;
#[derive(Debug)]
struct ThingWithAge {
created_utc: DateTime<Utc>,
}
impl ThingWithAge {
pub fn new(timestamp: i64) -> Self {
let created_utc = DateTime::from_timestamp(timestamp, 0).unwrap();
Self { created_utc }
}
}
impl HasAge for ThingWithAge {
fn created_utc(&self) -> DateTime<Utc> {
self.created_utc
}
}
#[test]
fn it_returns_its_age() {
let clock = FrozenClock::default();
let thing = ThingWithAge::new(1349074800);
assert_eq!(thing.age(&clock).num_seconds(), 398945580);
}
#[test]
fn it_returns_its_age_as_a_relative_string() {
let clock = FrozenClock::default();
let thing = ThingWithAge::new(1349074800);
assert_eq!(thing.relative_age(&clock), "13 years ago");
}
#[test]
fn it_correctly_formats_singular_time_units() {
let datetime = DateTime::parse_from_rfc3339("2025-05-28T10:51:00-07:00")
.expect("could not parse timestamp")
.with_timezone(&Utc);
let clock = FrozenClock::new(datetime);
let thing = ThingWithAge::new(1744177355);
assert_eq!(thing.relative_age(&clock), "1 month ago");
}
#[test]
fn it_correctly_formats_singular_time_units_with_indefinite_articles() {
let datetime = DateTime::parse_from_rfc3339("2025-05-28T10:51:00-07:00")
.expect("could not parse timestamp")
.with_timezone(&Utc);
let clock = FrozenClock::new(datetime);
let thing = ThingWithAge::new(1744481059);
assert_eq!(thing.relative_age(&clock), "a month ago");
}
}
}