Skip to main content

max / quasi

4.4 KB · 113 lines History Blame Raw
1 //! What a time-derived readout says.
2 //!
3 //! The description carries an instant and which way the readout runs against
4 //! now ([`Clock`]); the words and the cadence are this renderer's.
5 //!
6 //! This renderer is the split one, and it is the reason the ruling costs
7 //! anything at all here. The markup is emitted once, by a server, and then a
8 //! browser holds it for as long as the reader is on the page -- so the first
9 //! words are made here and every set after them is made by `clock.js`, which is
10 //! also where the cadences live. The two say the same thing in two languages
11 //! and each carries a note pointing at the other; a change to either is a change
12 //! to both.
13 //!
14 //! Emitting the first words rather than leaving the element empty is what keeps
15 //! a reader with no script from seeing a hole where a time was: what they get is
16 //! the truth at the moment the page was made, which is what every server-rendered
17 //! timestamp in the tree already is.
18
19 use std::time::{Duration, SystemTime};
20
21 use quasi_router::Clock;
22
23 /// The words a readout of this kind shows, reckoned against `now`.
24 #[must_use]
25 pub(crate) fn text(clock: Clock, at: SystemTime, now: SystemTime) -> String {
26 match clock {
27 Clock::Since => face(now.duration_since(at).unwrap_or_default()),
28 // A countdown that has run out reads as zero rather than as a negative
29 // number. Nothing in the description says which, and inventing
30 // "overdue by" here would be this renderer saying something the other
31 // two do not.
32 Clock::Until => face(at.duration_since(now).unwrap_or_default()),
33 Clock::Age => ago(now.duration_since(at).unwrap_or_default()),
34 }
35 }
36
37 /// The instant as the script reads it: milliseconds since the epoch.
38 ///
39 /// What rides in `data-at`. An instant before the epoch answers zero rather
40 /// than wrapping, which is a readout of 1970 rather than a readout of the far
41 /// future; nothing in these apps carries one and a wrap would be the worse
42 /// failure if anything ever did.
43 pub(crate) fn epoch_millis(at: SystemTime) -> u128 {
44 at.duration_since(SystemTime::UNIX_EPOCH)
45 .unwrap_or_default()
46 .as_millis()
47 }
48
49 /// A span as a stopwatch: `h:mm:ss`, hours unbounded.
50 ///
51 /// Unbounded rather than rolling into days, because the readout this serves is
52 /// a running timer and a timer that reads `2d 3:04:05` has stopped being one.
53 fn face(span: Duration) -> String {
54 let secs = span.as_secs();
55 format!("{}:{:02}:{:02}", secs / 3600, (secs % 3600) / 60, secs % 60)
56 }
57
58 /// A span as a stamp: the largest unit that is not zero, and how long ago.
59 fn ago(span: Duration) -> String {
60 let secs = span.as_secs();
61 if secs < 60 {
62 "just now".to_owned()
63 } else if secs < 3600 {
64 format!("{}m ago", secs / 60)
65 } else if secs < 86_400 {
66 format!("{}h ago", secs / 3600)
67 } else {
68 format!("{}d ago", secs / 86_400)
69 }
70 }
71
72 #[cfg(test)]
73 mod tests {
74 use super::*;
75
76 fn at(seconds: u64) -> SystemTime {
77 SystemTime::UNIX_EPOCH + Duration::from_secs(seconds)
78 }
79
80 #[test]
81 fn a_stopwatch_counts_up_and_a_countdown_counts_down() {
82 assert_eq!(text(Clock::Since, at(0), at(3845)), "1:04:05");
83 assert_eq!(text(Clock::Until, at(3845), at(0)), "1:04:05");
84 }
85
86 #[test]
87 fn a_readout_that_has_run_out_reads_as_zero_rather_than_backwards() {
88 assert_eq!(text(Clock::Until, at(0), at(90)), "0:00:00");
89 assert_eq!(text(Clock::Since, at(90), at(0)), "0:00:00");
90 }
91
92 #[test]
93 fn a_stamp_is_coarse_and_a_stopwatch_is_not() {
94 assert_eq!(text(Clock::Age, at(0), at(30)), "just now");
95 assert_eq!(text(Clock::Age, at(0), at(300)), "5m ago");
96 assert_eq!(text(Clock::Age, at(0), at(10_800)), "3h ago");
97 assert_eq!(text(Clock::Age, at(0), at(345_600)), "4d ago");
98 assert_eq!(text(Clock::Since, at(0), at(30)), "0:00:30");
99 }
100
101 #[test]
102 fn the_first_words_are_the_ones_the_script_would_have_written() {
103 // The two implementations of one format, checked at the boundaries the
104 // script's own branches turn on. A drift here is a page that changes
105 // what it says a second after it loads.
106 assert_eq!(text(Clock::Age, at(0), at(59)), "just now");
107 assert_eq!(text(Clock::Age, at(0), at(60)), "1m ago");
108 assert_eq!(text(Clock::Age, at(0), at(3600)), "1h ago");
109 assert_eq!(text(Clock::Age, at(0), at(86_400)), "1d ago");
110 assert_eq!(text(Clock::Since, at(0), at(0)), "0:00:00");
111 }
112 }
113