From de10996c2eb99c63c2b06a6d1d1f209a355b27c5 Mon Sep 17 00:00:00 2001 From: 0xSoftBoi Date: Sun, 5 Apr 2026 19:11:16 -0400 Subject: [PATCH 1/2] fix: support HH:MM am/pm time formats in combined date-time parsing The combined date-time parser (e.g., "2024-06-15 12:00 PM") used time::iso which only accepts 24-hour notation. When an am/pm suffix followed, iso would greedily consume the time portion, leaving the meridiem orphaned and causing a parse error. Switch to time::parse which tries am_pm_time first (with fallback to iso), so inputs like "2024-06-15 12:00 PM", "2024-06-15 11:30am", and "2024-06-15 3:00 PM" are now accepted. Case insensitivity is already handled by parse_items lowercasing the input before parsing. Fixes #282 Co-Authored-By: Claude Opus 4.6 --- src/items/combined.rs | 2 +- src/items/mod.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/items/combined.rs b/src/items/combined.rs index cce0a1a..5843e7d 100644 --- a/src/items/combined.rs +++ b/src/items/combined.rs @@ -34,7 +34,7 @@ pub(crate) fn parse(input: &mut &str) -> ModalResult { date: trace("iso_date", alt((date::iso1, date::iso2))), // Note: the `T` is lowercased by the main parse function _: alt((s('t').void(), (' ', space).void())), - time: trace("iso_time", time::iso), + time: trace("iso_time", time::parse), }) .parse_next(input) } diff --git a/src/items/mod.rs b/src/items/mod.rs index a1529bb..d89e23f 100644 --- a/src/items/mod.rs +++ b/src/items/mod.rs @@ -377,6 +377,14 @@ mod tests { ); } + #[test] + fn date_and_time_ampm() { + // https://github.com/uutils/parse_datetime/issues/282 + assert_eq!("12:00", test_eq_fmt("%H:%M", "2024-06-15 12:00 PM")); + assert_eq!("11:30", test_eq_fmt("%H:%M", "2024-06-15 11:30am")); + assert_eq!("15:00", test_eq_fmt("%H:%M", "2024-06-15 3:00 PM")); + } + #[test] fn empty() { let result = parse(&mut ""); From fdef5ec396c8e405a85c21ba045adab62f670f94 Mon Sep 17 00:00:00 2001 From: 0xSoftBoi Date: Mon, 6 Apr 2026 05:31:10 -0400 Subject: [PATCH 2/2] perf: keep ISO fast path for combined datetime parsing --- src/items/combined.rs | 61 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/src/items/combined.rs b/src/items/combined.rs index 5843e7d..9729c15 100644 --- a/src/items/combined.rs +++ b/src/items/combined.rs @@ -16,7 +16,7 @@ //! > discarded. use winnow::{ combinator::{alt, trace}, - seq, ModalResult, Parser, + ModalResult, Parser, }; use crate::items::space; @@ -29,14 +29,32 @@ pub(crate) struct DateTime { pub(crate) time: time::Time, } +fn remaining_starts_with_meridiem(input: &str) -> bool { + let trimmed = input.trim_start(); + trimmed.starts_with("am") + || trimmed.starts_with("pm") + || trimmed.starts_with("a.m.") + || trimmed.starts_with("p.m.") +} + pub(crate) fn parse(input: &mut &str) -> ModalResult { - seq!(DateTime { - date: trace("iso_date", alt((date::iso1, date::iso2))), - // Note: the `T` is lowercased by the main parse function - _: alt((s('t').void(), (' ', space).void())), - time: trace("iso_time", time::parse), - }) - .parse_next(input) + let date = trace("iso_date", alt((date::iso1, date::iso2))).parse_next(input)?; + // Note: the `T` is lowercased by the main parse function + alt((s('t').void(), (' ', space).void())).parse_next(input)?; + + let mut iso_input = *input; + if let Ok(parsed_time) = trace("iso_time", time::iso).parse_next(&mut iso_input) { + if !remaining_starts_with_meridiem(iso_input) { + *input = iso_input; + return Ok(DateTime { + date, + time: parsed_time, + }); + } + } + + let time = trace("iso_time", time::parse).parse_next(input)?; + Ok(DateTime { date, time }) } #[cfg(test)] @@ -73,4 +91,31 @@ mod tests { assert_eq!(parse(&mut s).ok(), reference, "Failed string: {old_s}") } } + + #[test] + fn date_and_time_ampm() { + let reference = Some(DateTime { + date: Date { + day: 15, + month: 6, + year: Some(2024), + }, + time: Time { + hour: 15, + minute: 0, + second: 0, + nanosecond: 0, + offset: None, + }, + }); + + for mut s in [ + "2024-06-15 3:00 pm", + "2024-06-15 3:00pm", + "2024-06-15 3:00 p.m.", + ] { + let old_s = s.to_owned(); + assert_eq!(parse(&mut s).ok(), reference, "Failed string: {old_s}"); + } + } }