From 9dab857ca3babe685e7187f63ceac0f01da2feb8 Mon Sep 17 00:00:00 2001 From: Eric Kaden <608341+EricKaden@users.noreply.github.com> Date: Thu, 22 Oct 2026 08:48:21 -0400 Subject: [PATCH] M12: only date clauses are other ways to type a date, and a date is rewritten only into a first pattern that keeps it --- crates/xfa/src/fixtures.rs | 5 ++++- crates/xfa/src/picture.rs | 23 +++++++++++++++++++---- crates/xfa/src/tests.rs | 13 +++++++++++++ 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/crates/xfa/src/fixtures.rs b/crates/xfa/src/fixtures.rs index 9e76fbbae..9b4988aef 100644 --- a/crates/xfa/src/fixtures.rs +++ b/crates/xfa/src/fixtures.rs @@ -506,7 +506,8 @@ pub fn fields_template(data: &str) -> String { } /// Date fields whose edit pictures list alternatives, as forms let a date be typed several ways -/// (`date{YYYYMMDD}|date{YYMMDD}|…`); one with a single pattern; a text field. +/// (`date{YYYYMMDD}|date{YYMMDD}|…`); one with a single pattern; a text field; `code`, whose +/// only alternative is a text clause; and `short`, whose first pattern has a two-digit year. pub fn date_alternatives_template() -> String { r##" @@ -516,6 +517,8 @@ pub fn date_alternatives_template() -> String { date{YYYYMMDD}|date{DDMMMYY}|date{YYMMDD}|date{YYYYMMD}|date{YY MM DD} date{YYYY-MM-DD} + date{YYYYMMDD}|text{YYMMDD} + date{YYMMDD}|date{YYYYMMDD} diff --git a/crates/xfa/src/picture.rs b/crates/xfa/src/picture.rs index 58bd48cd6..2da57a2b0 100644 --- a/crates/xfa/src/picture.rs +++ b/crates/xfa/src/picture.rs @@ -29,8 +29,9 @@ pub(crate) fn alternatives(picture: &str) -> Vec<&str> { /// `text` typed into the date field at `som` by one of its edit picture's alternatives after the /// first, written in the first, which the field shows and checks typed dates against (`261018` /// becomes `20261018` for `date{YYYYMMDD}|date{YYMMDD}`). A pattern reads the text only when the -/// date it gives, written in that pattern, is the text again. `None` when the field isn't a date -/// field with alternatives, the first pattern reads the text, or no other does. +/// date it gives, written in that pattern, is the text again, and only date clauses count. `None` +/// when the field isn't a date field with alternatives, the first pattern reads the text, no +/// other does, or the first pattern can't hold the date (a two-digit year for 1901). pub fn typed_date(tpl: &Template, som: &str, text: &str) -> Option { let f = template_field(tpl, som)?; if f.ui != Ui::DateTimeEdit { @@ -42,6 +43,20 @@ pub fn typed_date(tpl: &Template, som: &str, text: &str) -> Option { if reads(&first).is_some() { return None; } - let iso = alternatives(picture).into_iter().skip(1).filter_map(date_letters).find_map(|p| reads(&p))?; - Some(iso_to_pattern(&iso, &first)) + let iso = alternatives(picture).into_iter().skip(1).filter(|a| is_date_clause(a)).filter_map(date_letters).find_map(|p| reads(&p))?; + // Written in the first pattern only when it reads back as the same date: a year a two-digit + // year can't hold would otherwise become another century's. + let shown = iso_to_pattern(&iso, &first); + (pattern_to_iso(&shown, &first).as_deref() == Some(iso.as_str())).then_some(shown) +} + +/// Whether picture clause `clause` is a date pattern: a `date{…}` clause (with or without a +/// locale, `date(en_US){…}`), or a bare pattern, which a date field reads as a date. A clause of +/// another category (`text{…}`, `num{…}`, `time{…}`) is not, whatever its letters. +fn is_date_clause(clause: &str) -> bool { + let clause = clause.trim(); + match clause.find(['{', '(']) { + Some(i) => clause.get(..i).is_some_and(|category| category.trim().eq_ignore_ascii_case("date")), + None => true, + } } diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 87875211f..da9c8832e 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -2675,3 +2675,16 @@ fn a_date_typed_by_another_of_its_edit_pictures_patterns_is_written_in_the_first assert_eq!(typed("form[0].born[0]", "181026"), None, "one pattern: nothing else to try"); assert_eq!(typed("form[0].name[0]", "261018"), None, "not a date field"); } + +/// Only date clauses are other ways to type a date, and a date is rewritten only when the first +/// pattern keeps it: a later `text{…}` clause is no date pattern, and a year a two-digit first +/// pattern can't hold leaves the text as typed instead of becoming another date. +#[test] +fn a_typed_date_is_rewritten_only_by_date_clauses_into_a_first_pattern_that_keeps_it() { + let (tpl, _) = crate::parse(&crate::fixtures::date_alternatives_template()).unwrap(); + let typed = |som: &str, text: &str| crate::typed_date(&tpl, som, text); + assert_eq!(typed("form[0].code[0]", "261018"), None, "text{{YYMMDD}} is not a date alternative"); + let short = "form[0].short[0]"; + assert_eq!(typed(short, "20261018").as_deref(), Some("261018"), "a year the first pattern keeps"); + assert_eq!(typed(short, "19011018"), None, "1901 as 01 would read back as 2001"); +}