From 863d4004a3e9497443797ec0de69a0a2fc85ddfc Mon Sep 17 00:00:00 2001 From: Alyssa Evans Date: Sat, 7 Nov 2026 19:53:14 -0800 Subject: [PATCH] fix(cli): sort timeline and archive date groups by calendar date display_by_date sorted its group keys with a plain string compare. The keys are display strings ("Wed Oct 21 2026", the %a %b %d %Y format items are created with), so tb --timeline and tb --archive ordered days by weekday name, then month name, despite the comment saying the order was chronological. Wed Oct 21 came before Sat Jan 02 of the next year. Parse each key with the same format and sort newest first. A key that does not parse (hand-edited storage) sorts after every dated key, in the descending string order it had before. Ordering by the newest item timestamp in each group was the other option; parsing keeps the order a property of the keys alone, so it does not depend on what the group holds. The order of items inside a group is a separate HashMap-order issue, handled by #66; this change touches only the order of the groups. Co-Authored-By: Claude Opus 5.5 --- crates/taskbook-client/src/render.rs | 62 +++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/crates/taskbook-client/src/render.rs b/crates/taskbook-client/src/render.rs index 0dbfa86..7a008a2 100644 --- a/crates/taskbook-client/src/render.rs +++ b/crates/taskbook-client/src/render.rs @@ -327,11 +327,7 @@ impl Render { } pub fn display_by_date(&self, data: &HashMap>) { - // Sort dates chronologically (most recent first based on actual date parsing) - let mut dates: Vec<_> = data.keys().collect(); - dates.sort_by(|a, b| b.cmp(a)); - - for date in dates { + for date in dates_newest_first(data.keys()) { let items = &data[date]; if self.is_board_complete(items) && !self.config.display_complete_tasks { @@ -620,3 +616,59 @@ impl Render { } } } + +/// Date-group keys in display order, newest first. +/// +/// Keys are display strings in the format items are created with +/// (`%a %b %d %Y`, e.g. "Wed Oct 21 2026"). Compared as strings they sort by +/// weekday name, then month name, so parse them. Keys that do not parse sort +/// after every parsed key, in descending string order as before. +fn dates_newest_first<'a>(keys: impl IntoIterator) -> Vec<&'a String> { + let mut dates: Vec<_> = keys.into_iter().collect(); + dates.sort_by_cached_key(|key| { + let date = chrono::NaiveDate::parse_from_str(key, "%a %b %d %Y").ok(); + std::cmp::Reverse((date, key.to_string())) + }); + dates +} + +#[cfg(test)] +mod tests { + use super::*; + + fn order(keys: &[&str]) -> Vec { + let keys: Vec = keys.iter().map(|k| k.to_string()).collect(); + dates_newest_first(&keys).into_iter().cloned().collect() + } + + #[test] + fn dates_newest_first_orders_by_calendar_date_not_weekday_name() { + // As strings, descending: Wed, Thu, Sat, Fri. + let keys = [ + "Wed Oct 21 2026", + "Fri Oct 23 2026", + "Sat Jan 02 2027", + "Thu Oct 22 2026", + ]; + + assert_eq!( + order(&keys), + [ + "Sat Jan 02 2027", + "Fri Oct 23 2026", + "Thu Oct 22 2026", + "Wed Oct 21 2026", + ] + ); + } + + #[test] + fn dates_newest_first_puts_unparseable_keys_last() { + let keys = ["not a date", "Mon Jan 05 2026", "Tue Jan 06 2026"]; + + assert_eq!( + order(&keys), + ["Tue Jan 06 2026", "Mon Jan 05 2026", "not a date"] + ); + } +}