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");
+}