From b60fa35bf69b249ae23e9ca0229d566aa9b503a3 Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Thu, 22 Oct 2026 13:25:00 -0400 Subject: [PATCH] fix(cards): name both months of a week that runs across them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A week's range printed Monday's day, Sunday's day, then Sunday's month, so 2026-W44 read "26–1 Nov 2026". The week card, page and JSON and the zine's issue line now share one formatter that names each end's month, and year, where they differ. Co-Authored-By: Claude Opus 5.5 Change-Id: I00fe7159532dc4e2e7a294df77b7d82c678ad187 --- crates/vibescrobble-query/src/paper.rs | 36 ++++++++++++++++++++++++++ crates/vibescrobble-query/src/swarm.rs | 22 +++++++--------- crates/vibescrobble-query/src/zine.rs | 33 ++++++++++++++--------- 3 files changed, 67 insertions(+), 24 deletions(-) diff --git a/crates/vibescrobble-query/src/paper.rs b/crates/vibescrobble-query/src/paper.rs index 6b6bb4c1..3e1d73d3 100644 --- a/crates/vibescrobble-query/src/paper.rs +++ b/crates/vibescrobble-query/src/paper.rs @@ -12,6 +12,8 @@ use std::fmt::Write as _; use base64::Engine as _; +use vibescrobble_index::tally::Week; + use crate::card::{escape, hsl, hue_of, width, Faces}; /// The plate's and the kit's inks, as `web/plate.css` and @@ -487,6 +489,25 @@ pub fn month(number: u8) -> &'static str { MONTHS[usize::from(number.clamp(1, 12)) - 1] } +/// A week's days, Monday to Sunday with `dash` between: `12–18 Oct 2026`. +/// A week that runs into the next month names both, `26 Oct–1 Nov 2026`, +/// and one into the next year both years, `28 Dec 2026–3 Jan 2027`. `None` +/// for a week its year lacks. +#[must_use] +pub fn week_days(week: Week, dash: char) -> Option { + let monday = week.monday()?; + let sunday = monday + time::Duration::days(6); + let (d1, m1, y1) = (monday.day(), month(monday.month() as u8), monday.year()); + let (d2, m2, y2) = (sunday.day(), month(sunday.month() as u8), sunday.year()); + Some(if y1 != y2 { + format!("{d1} {m1} {y1}{dash}{d2} {m2} {y2}") + } else if m1 != m2 { + format!("{d1} {m1}{dash}{d2} {m2} {y2}") + } else { + format!("{d1}{dash}{d2} {m2} {y2}") + }) +} + /// `1,479`, and `9.4k` from ten thousand. #[must_use] pub fn count(n: u32) -> String { @@ -552,6 +573,21 @@ mod tests { assert_eq!(duration(600), "10m"); } + #[test] + fn a_week_names_the_month_and_year_of_each_end_it_does_not_share() { + let days = |week| week_days(Week::parse(week).expect("a week"), '–'); + assert_eq!(days("2026-W42").as_deref(), Some("12–18 Oct 2026")); + assert_eq!(days("2026-W44").as_deref(), Some("26 Oct–1 Nov 2026")); + assert_eq!(days("2026-W53").as_deref(), Some("28 Dec 2026–3 Jan 2027")); + assert_eq!(days("2026-W01").as_deref(), Some("29 Dec 2025–4 Jan 2026")); + assert_eq!(days("2027-W05").as_deref(), Some("1–7 Feb 2027")); + let missing = Week { + year: 2025, + number: 53, + }; + assert_eq!(week_days(missing, '-'), None); + } + #[test] fn a_repo_is_named_short() { let handle = Some("permadeath.com"); diff --git a/crates/vibescrobble-query/src/swarm.rs b/crates/vibescrobble-query/src/swarm.rs index 7919a976..3469c3ad 100644 --- a/crates/vibescrobble-query/src/swarm.rs +++ b/crates/vibescrobble-query/src/swarm.rs @@ -672,19 +672,9 @@ fn week_sheet(origin: &str, gathered: &Gathered, week: Week, unwrapped: Option String { - let Some(monday) = week.monday() else { - return week.to_string(); - }; - let sunday = monday + time::Duration::days(6); - format!( - "{}–{} {} {}", - monday.day(), - sunday.day(), - crate::paper::month(sunday.month() as u8), - sunday.year() - ) + crate::paper::week_days(week, '–').unwrap_or_else(|| week.to_string()) } /// Everything one year's poster is drawn from, read under the view's lock. @@ -965,6 +955,14 @@ mod tests { use super::*; use std::path::PathBuf; + #[test] + fn a_week_across_a_month_or_year_names_both_ends() { + let span = |week| span(Week::parse(week).expect("a week")); + assert_eq!(span("2026-W43"), "19–25 Oct 2026"); + assert_eq!(span("2026-W44"), "26 Oct–1 Nov 2026"); + assert_eq!(span("2026-W53"), "28 Dec 2026–3 Jan 2027"); + } + /// A real week's page, written out to look at: `SWARM_SUMMARY` names a /// week's summary as JSON, as `zine::tests::a_real_week_draws` writes /// it, `SWARM_SUBJECT` and `SWARM_HANDLE` whose it is, and `CARD_OUT` a diff --git a/crates/vibescrobble-query/src/zine.rs b/crates/vibescrobble-query/src/zine.rs index 13c2c60e..d6551bc1 100644 --- a/crates/vibescrobble-query/src/zine.rs +++ b/crates/vibescrobble-query/src/zine.rs @@ -71,21 +71,16 @@ fn seed(issue: &Issue<'_>, part: &str) -> String { format!("{} {week} {part}", issue.did) } -/// `12–18 oct 2026`. +/// `12-18 oct 2026`, or `26 oct-1 nov 2026` across a month's end. fn dates(issue: &Issue<'_>) -> String { - let Some(monday) = issue.summary.week.and_then(|week| week.monday()) else { - return String::new(); - }; - let sunday = monday + time::Duration::days(6); // A hyphen, not a dash: the typewriter's face is cut to ASCII and a // little more, and a line with one glyph it lacks is set in another face. - format!( - "{}-{} {} {}", - monday.day(), - sunday.day(), - paper::month(sunday.month() as u8).to_lowercase(), - sunday.year() - ) + issue + .summary + .week + .and_then(|week| paper::week_days(week, '-')) + .map(|days| days.to_lowercase()) + .unwrap_or_default() } fn number(issue: &Issue<'_>) -> String { @@ -761,6 +756,20 @@ pub(crate) mod tests { } } + #[test] + fn an_issue_line_names_both_months_of_a_week_across_them() { + let summary = quiet(); + let issue = Issue { + summary: &summary, + did: "did:plc:quernstone", + handle: None, + name: "tidepool", + so_far: false, + link: String::new(), + }; + assert_eq!(dates(&issue), "26 oct-1 nov 2026"); + } + /// Each week draws as a page and a spread, a picture missing or not. With /// `CARD_OUT` set to a directory, they are written there to look at. #[test] -- knotmirror 0.0.1