src/timestamp.rs
Ref: Size: 4.8 KiB History
//! How a stored timestamp goes onto a screen.
//!
//! Events store `chrono::Utc::now().to_rfc3339()`, which is 35 characters of
//! which nine are nanoseconds:
//!
//! ```text
//! 2026-08-11T16:08:31.458124482+00:00
//! ```
//!
//! That is the right thing to *store* — it is exact, sortable and unambiguous
//! about its offset — and the wrong thing to put in a table cell. At 35
//! characters it was the widest column after the title, it wrapped every row
//! onto two lines, and the nine digits of precision are not a number any
//! reader has ever needed. See issue `b2a57996`.
//!
//! So a timestamp reaching a template is split in two: a short form for the
//! cell and the stored form for `title=`, which browsers surface on hover.
//! Nothing is discarded — the exact value is still on the page, one hover
//! away, and `--json` is untouched and still carries it in full.
//!
//! # Why the dashboard uses it too
//!
//! Issue `952390ad`: the TUI kept printing the stored string after the web had
//! stopped, which is the divergence `10bb2d84` says must not happen for a
//! displayed value — two surfaces rendering one field two ways teach two
//! different habits and make screenshots and pasted output disagree. A
//! terminal has no hover, so the dashboard shows [`Timestamp::short`] and
//! nothing else; the full value is still one `show` or `--json` away.
//!
//! # Why minutes, and why absolute
//!
//! Minute precision is the coarsest rendering that still orders two events on
//! the same day, which is the common case for a review and the comment
//! answering it. Seconds add three characters and settle nothing a reader
//! cares about.
//!
//! Absolute rather than relative ("3 days ago") because a relative rendering
//! is a function of when the page was built, so the same URL says different
//! things at different times: it cannot be cached, cannot be compared between
//! two rows rendered from different requests, and cannot be asserted by a test
//! without freezing a clock. The exact instant is what was recorded; the page
//! should say it.
//!
//! # Why UTC
//!
//! The server cannot know the reader's timezone, and a page rendered in the
//! *server's* zone would silently disagree with the same page rendered
//! elsewhere. Every timestamp this project writes is already `+00:00`, so
//! normalizing to UTC changes nothing in practice and fixes the display for a
//! value that arrived with some other offset from a repository synced in.
/// A stored timestamp, split into what a cell shows and what it stands for.
#[derive(Debug, Clone)]
pub struct Timestamp {
/// Exactly as stored — the `title` attribute, so nothing is lost.
pub full: String,
/// `YYYY-MM-DD HH:MM` in UTC.
pub short: String,
}
impl Timestamp {
/// Split a stored RFC3339 timestamp for display.
///
/// An unparseable value renders as itself. A timestamp this project did
/// not write is still a fact about the object, and a page that dropped it
/// or printed a placeholder would be hiding the one thing that could
/// explain the row. Ugly beats absent.
pub fn new(stored: impl Into<String>) -> Self {
let full = stored.into();
let short = chrono::DateTime::parse_from_rfc3339(&full)
.map(|dt| {
dt.with_timezone(&chrono::Utc)
.format("%Y-%m-%d %H:%M")
.to_string()
})
.unwrap_or_else(|_| full.clone());
Timestamp { full, short }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nanoseconds_and_the_offset_are_dropped_from_the_short_form() {
let t = Timestamp::new("2026-08-11T16:08:31.458124482+00:00");
assert_eq!(t.short, "2026-08-11 16:08");
assert_eq!(t.short.len(), 16);
}
#[test]
fn the_stored_value_survives_for_the_title_attribute() {
let stored = "2026-08-11T16:08:31.458124482+00:00";
assert_eq!(Timestamp::new(stored).full, stored);
}
/// A value that arrived with a non-UTC offset must not read as though its
/// wall-clock digits were UTC — two rows in one table have to be
/// comparable by eye.
#[test]
fn a_foreign_offset_is_normalized_to_utc() {
let t = Timestamp::new("2026-08-11T18:08:31+02:00");
assert_eq!(t.short, "2026-08-11 16:08");
}
#[test]
fn a_second_precision_timestamp_shortens_too() {
assert_eq!(
Timestamp::new("2026-08-11T16:08:31Z").short,
"2026-08-11 16:08"
);
}
#[test]
fn an_unparseable_timestamp_renders_as_itself_rather_than_vanishing() {
let t = Timestamp::new("not a date");
assert_eq!(t.short, "not a date");
assert_eq!(t.full, "not a date");
}
#[test]
fn an_empty_timestamp_stays_empty() {
assert_eq!(Timestamp::new("").short, "");
}
}