//! What a time-derived readout says. //! //! The description carries an instant and which way the readout runs against //! now ([`Clock`]); the words and the cadence are this renderer's. //! //! This renderer is the split one, and it is the reason the ruling costs //! anything at all here. The markup is emitted once, by a server, and then a //! browser holds it for as long as the reader is on the page -- so the first //! words are made here and every set after them is made by `clock.js`, which is //! also where the cadences live. The two say the same thing in two languages //! and each carries a note pointing at the other; a change to either is a change //! to both. //! //! Emitting the first words rather than leaving the element empty is what keeps //! a reader with no script from seeing a hole where a time was: what they get is //! the truth at the moment the page was made, which is what every server-rendered //! timestamp in the tree already is. use std::time::{Duration, SystemTime}; use quasi_router::Clock; /// The words a readout of this kind shows, reckoned against `now`. #[must_use] pub(crate) fn text(clock: Clock, at: SystemTime, now: SystemTime) -> String { match clock { Clock::Since => face(now.duration_since(at).unwrap_or_default()), // A countdown that has run out reads as zero rather than as a negative // number. Nothing in the description says which, and inventing // "overdue by" here would be this renderer saying something the other // two do not. Clock::Until => face(at.duration_since(now).unwrap_or_default()), Clock::Age => ago(now.duration_since(at).unwrap_or_default()), } } /// The instant as the script reads it: milliseconds since the epoch. /// /// What rides in `data-at`. An instant before the epoch answers zero rather /// than wrapping, which is a readout of 1970 rather than a readout of the far /// future; nothing in these apps carries one and a wrap would be the worse /// failure if anything ever did. pub(crate) fn epoch_millis(at: SystemTime) -> u128 { at.duration_since(SystemTime::UNIX_EPOCH) .unwrap_or_default() .as_millis() } /// A span as a stopwatch: `h:mm:ss`, hours unbounded. /// /// Unbounded rather than rolling into days, because the readout this serves is /// a running timer and a timer that reads `2d 3:04:05` has stopped being one. fn face(span: Duration) -> String { let secs = span.as_secs(); format!("{}:{:02}:{:02}", secs / 3600, (secs % 3600) / 60, secs % 60) } /// A span as a stamp: the largest unit that is not zero, and how long ago. fn ago(span: Duration) -> String { let secs = span.as_secs(); if secs < 60 { "just now".to_owned() } else if secs < 3600 { format!("{}m ago", secs / 60) } else if secs < 86_400 { format!("{}h ago", secs / 3600) } else { format!("{}d ago", secs / 86_400) } } #[cfg(test)] mod tests { use super::*; fn at(seconds: u64) -> SystemTime { SystemTime::UNIX_EPOCH + Duration::from_secs(seconds) } #[test] fn a_stopwatch_counts_up_and_a_countdown_counts_down() { assert_eq!(text(Clock::Since, at(0), at(3845)), "1:04:05"); assert_eq!(text(Clock::Until, at(3845), at(0)), "1:04:05"); } #[test] fn a_readout_that_has_run_out_reads_as_zero_rather_than_backwards() { assert_eq!(text(Clock::Until, at(0), at(90)), "0:00:00"); assert_eq!(text(Clock::Since, at(90), at(0)), "0:00:00"); } #[test] fn a_stamp_is_coarse_and_a_stopwatch_is_not() { assert_eq!(text(Clock::Age, at(0), at(30)), "just now"); assert_eq!(text(Clock::Age, at(0), at(300)), "5m ago"); assert_eq!(text(Clock::Age, at(0), at(10_800)), "3h ago"); assert_eq!(text(Clock::Age, at(0), at(345_600)), "4d ago"); assert_eq!(text(Clock::Since, at(0), at(30)), "0:00:30"); } #[test] fn the_first_words_are_the_ones_the_script_would_have_written() { // The two implementations of one format, checked at the boundaries the // script's own branches turn on. A drift here is a page that changes // what it says a second after it loads. assert_eq!(text(Clock::Age, at(0), at(59)), "just now"); assert_eq!(text(Clock::Age, at(0), at(60)), "1m ago"); assert_eq!(text(Clock::Age, at(0), at(3600)), "1h ago"); assert_eq!(text(Clock::Age, at(0), at(86_400)), "1d ago"); assert_eq!(text(Clock::Since, at(0), at(0)), "0:00:00"); } }