diff --git a/crates/catalog/src/dates.rs b/crates/catalog/src/dates.rs index d180d013d..4d16c2f4d 100644 --- a/crates/catalog/src/dates.rs +++ b/crates/catalog/src/dates.rs @@ -49,7 +49,9 @@ pub fn shift_iso(iso: &str, secs: i64) -> Option { if !(first..=last).contains(&t) { return None; } - let tail = iso.trim().get(19..).unwrap_or(""); + let iso = iso.trim(); + // the seconds are optional (`10:00+02:00`): without them the tail starts right after the minutes + let tail = iso.get(if iso.get(16..17) == Some(":") { 19 } else { 16 }..).unwrap_or(""); Some(format!("{}{tail}", civil(t))) } @@ -222,6 +224,15 @@ mod tests { assert_eq!(normalize_iso("2026-02-30"), None); } + #[test] + fn shift_keeps_the_zone_of_a_time_without_seconds() { + assert_eq!(shift_iso("2026-01-01T10:00+02:00", 3600).as_deref(), Some("2026-01-01T11:00:00+02:00")); + assert_eq!(shift_iso("2026-01-01T10:00Z", 60).as_deref(), Some("2026-01-01T10:01:00Z")); + assert_eq!(shift_iso("2026-01-01T10:00", 3600).as_deref(), Some("2026-01-01T11:00:00")); + assert_eq!(shift_iso("2026-01-01T10:00:00.5-05:00", 3600).as_deref(), Some("2026-01-01T11:00:00.5-05:00")); + assert_eq!(iso_seconds("2026-01-01T10:00+02:00"), iso_seconds("2026-01-01T10:00")); + } + #[test] fn runs_by_day_month_year_keep_stacks_together() { let mut c = Catalog::new(); diff --git a/crates/catalog/src/stacks.rs b/crates/catalog/src/stacks.rs index a713b7736..eb7e89ef4 100644 --- a/crates/catalog/src/stacks.rs +++ b/crates/catalog/src/stacks.rs @@ -294,7 +294,8 @@ pub fn iso_seconds(s: &str) -> Option { if !(1..=12).contains(&m) || !(1..=31).contains(&d) { return None; } - let (hh, mm, ss) = if s.len() >= 16 { (num(11, 13)?, num(14, 16)?, num(17, 19).unwrap_or(0)) } else { (0, 0, 0) }; + let (hh, mm, ss) = + if s.len() >= 16 { (num(11, 13)?, num(14, 16)?, if s.get(16..17) == Some(":") { num(17, 19).unwrap_or(0) } else { 0 }) } else { (0, 0, 0) }; // days from civil (proleptic Gregorian) let y2 = if m <= 2 { y - 1 } else { y }; let era = y2.div_euclid(400);