M12: accept literal digits in date formats (dd.mm.26) (#1352)

A date field formatted "dd.mm.26" (a leave form fixing the year) rejected
"15.05.26": parse_date read three numbers but the format only asked for a
day and a month. Digit runs in the format are now literal tokens that may
be typed or left out; a different number in their place is still rejected.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Paul Spooren 2026-10-12 11:43:55 +02:00 committed by GitHub
parent 47d612817a
commit 9a8517599b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 32 additions and 7 deletions

View File

@ -543,13 +543,24 @@ fn weekday(y: i32, m: u32, d: u32) -> usize {
(y + y / 4 - y / 100 + y / 400 + t + i64::from(d)).rem_euclid(7) as usize
}
/// The order of the year/month/day fields in a format, as letters.
fn order(fmt: &str) -> Vec<char> {
let mut out = Vec::new();
/// The order of the year/month/day fields in a format, as letters. A run of literal digits
/// (the fixed year in "dd.mm.26") is `'#'` with its value; the others carry 0.
fn order(fmt: &str) -> Vec<(char, u32)> {
let mut out: Vec<(char, u32)> = Vec::new();
// Weekday names (ddd, dddd) are shown, not typed: leave them out.
let mut cleaned = fmt.replace("dddd", " ");
cleaned = cleaned.replace("ddd", " ");
let mut prev = ' ';
for c in cleaned.chars() {
if let Some(digit) = c.to_digit(10) {
match out.last_mut() {
Some(('#', n)) if prev.is_ascii_digit() => *n = n.saturating_mul(10).saturating_add(digit),
_ => out.push(('#', digit)),
}
prev = c;
continue;
}
prev = c;
let k = match c {
'y' => 'y',
'm' => 'm',
@ -559,8 +570,8 @@ fn order(fmt: &str) -> Vec<char> {
's' => 's',
_ => continue,
};
if out.last() != Some(&k) {
out.push(k);
if out.last().map(|&(l, _)| l) != Some(k) {
out.push((k, 0));
}
}
out
@ -607,10 +618,17 @@ pub fn parse_date(text: &str, fmt: &str) -> Option<DateTime> {
}
flush(&mut cur, &mut nums, &mut month_name);
let mut dt = DateTime { y: 0, m: 1, d: 1, hh: 0, mm: 0, ss: 0 };
let mut it = nums.into_iter();
let mut it = nums.into_iter().peekable();
let mut have_y = false;
for k in order(fmt) {
for (k, literal) in order(fmt) {
match k {
// Literal digits in the format may be typed or left out ("15.05.26" or "15.05"
// for "dd.mm.26"); a different number there is left over and rejected below.
'#' => {
if it.peek() == Some(&literal) {
it.next();
}
}
'm' => {
dt.m = match month_name {
Some(m) => m,
@ -1038,6 +1056,13 @@ mod tests {
assert_eq!(format_value(&Format::Date("dd-mmm-yy".into()), "5-Mar-21"), "05-Mar-21", "month names are read");
assert!(keystroke(&d, "Due", "2/30/2024").is_err(), "no 30 February");
assert!(keystroke(&d, "Due", "13/1/2024").is_err());
// Literal digits in the format (a leave form fixing the year as "dd.mm.26").
let fixed = Format::Date("dd.mm.26".into());
assert_eq!(keystroke(&fixed, "Von", "15.05.26"), Ok("15.05.26".into()));
assert_eq!(format_value(&fixed, "15.05.26"), "15.05.26");
assert_eq!(format_value(&fixed, "3.1"), "03.01.26", "the literal may be left out");
assert!(keystroke(&fixed, "Von", "15.05.27").is_err(), "a different number there is not the literal");
assert!(keystroke(&fixed, "Von", "32.05.26").is_err());
assert_eq!(format_value(&Format::Time("h:MM tt".into()), "14:05"), "2:05 pm");
assert_eq!(format_value(&Format::Time("HH:MM".into()), "2:05 pm"), "14:05");
assert_eq!(format_value(&Format::Special(2), "5551234567"), "(555) 123-4567");