diff --git a/crates/forms/src/calc_order.rs b/crates/forms/src/calc_order.rs index ed69ac7c1b..3b4a684824 100644 --- a/crates/forms/src/calc_order.rs +++ b/crates/forms/src/calc_order.rs @@ -161,7 +161,7 @@ pub fn calculation_order(doc: &Document, fields: &[Field]) -> CalculationOrder { .collect(); let links = links(calculated.iter().map(|(f, _)| f.name.as_str()), &reads); let component = components(&links); - let mut all: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect(); + let mut all: Vec<(&str, usize)> = fields.iter().map(|f| (f.name.as_str(), f.widgets.len())).collect(); all.sort_unstable(); let name = |k: usize| calculated.get(k).map(|(f, _)| f.name.clone()).unwrap_or_default(); let mut out = Vec::with_capacity(calculated.len()); @@ -193,14 +193,22 @@ pub fn calculation_order(doc: &Document, fields: &[Field]) -> CalculationOrder { CalculationOrder { fields: out, extra, fixed } } -/// Whether a formula's read `name` finds a field in `names` (sorted): the field of that name, a -/// field under it (`row` for `row.1`), or a widget of one (`price.0`, as `getField` numbers them). -fn found(names: &[&str], name: &str) -> bool { - let has = |n: &str| names.binary_search(&n).is_ok(); +/// Whether a formula's read `name` finds a field in `fields` (names with their widget counts, +/// sorted): the field of that name, a field under it (`row` for `row.1`), or one of a field's +/// widgets (`price.0`, as Acrobat's `getField` numbers them, up to its last widget). +fn found(fields: &[(&str, usize)], name: &str) -> bool { + // The most widgets a field called `n` has (a malformed form can have two such fields). + let widgets = |n: &str| { + let at = fields.partition_point(|(f, _)| *f < n); + fields.get(at..).unwrap_or_default().iter().take_while(|(f, _)| *f == n).map(|&(_, w)| w).max() + }; let prefix = format!("{name}."); - let under = names.partition_point(|n| *n < prefix.as_str()); - let widget = name.rsplit_once('.').is_some_and(|(base, i)| !i.is_empty() && i.bytes().all(|b| b.is_ascii_digit()) && has(base)); - has(name) || names.get(under).is_some_and(|n| n.starts_with(&prefix)) || widget + let under = fields.partition_point(|(f, _)| *f < prefix.as_str()); + // A field with one widget, or none, is still `.0` (digits only: `parse` would take `+0`). + let widget = name.rsplit_once('.').is_some_and(|(base, i)| { + i.bytes().all(|b| b.is_ascii_digit()) && i.parse::().is_ok_and(|i| widgets(base).is_some_and(|w| i < w.max(1))) + }); + widgets(name).is_some() || fields.get(under).is_some_and(|(f, _)| f.starts_with(&prefix)) || widget } /// For each calculated field (by its place in `names`), the places of the calculated fields it @@ -410,14 +418,16 @@ mod tests { #[test] fn a_read_name_finds_a_field_a_group_or_a_widget() { - let mut names = vec!["price", "row.1", "row.2", "rows", "total"]; - names.sort_unstable(); - for name in ["price", "row", "row.1", "rows", "price.0", "price.12"] { - assert!(found(&names, name), "{name}"); + let mut fields = vec![("price", 1), ("qty", 3), ("row.1", 1), ("row.2", 1), ("rows", 1), ("total", 1), ("bare", 0)]; + fields.sort_unstable(); + for name in ["price", "row", "row.1", "rows", "price.0", "qty.0", "qty.2", "bare", "bare.0"] { + assert!(found(&fields, name), "{name}"); } - for name in ["pric", "ro", "row.3", "price.", "price.x", "price.-1", "total.0.1", ""] { - assert!(!found(&names, name), "{name}"); + // Past a field's last widget there is no widget to find. + for name in ["pric", "ro", "row.3", "price.", "price.x", "price.-1", "price.+0", "price.1", "price.12", "qty.3", "bare.1", "total.0.1", ""] { + assert!(!found(&fields, name), "{name}"); } + assert!(!found(&fields, "qty.99999999999999999999999"), "a number too big for usize is no widget"); } /// Two thousand formulas each reading a group of two thousand fields would be four million diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index c9c01c0e13..ae73da1a7d 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -1727,7 +1727,9 @@ fn missing_fields_and_built_names_are_flagged() { let calculate = |c: Calculate| FieldProps { calculate: Some(c), ..FieldProps::default() }; set_props(&mut doc, "sum", &calculate(Calculate::Simple { op: CalcOp::Sum, fields: vec!["qty".into(), "gone".into()] })).unwrap(); set_props(&mut doc, "notation", &calculate(Calculate::Notation("qty * nope".into()))).unwrap(); - set_field_script(&mut doc, "script", "calculate", Some("event.value = getField(\"qty\").value + getField(\"ghost\").value;")).unwrap(); + // qty has one widget: qty.0 is it, and there is no qty.1. + let script = "event.value = getField(\"qty\").value + getField(\"ghost\").value + getField(\"qty.0\").value + getField(\"qty.1\").value;"; + set_field_script(&mut doc, "script", "calculate", Some(script)).unwrap(); set_field_script(&mut doc, "built", "calculate", Some("event.value = getField(\"qty\").value + getField(\"row\" + 1).value;")).unwrap(); let issues = |doc: &Document| { let order = calculation_order(doc, &fields(doc)); @@ -1738,7 +1740,7 @@ fn missing_fields_and_built_names_are_flagged() { vec![ ("sum".to_owned(), sum), ("notation".to_owned(), vec![MissingField { field: "nope".into() }]), - ("script".to_owned(), vec![MissingField { field: "ghost".into() }]), + ("script".to_owned(), vec![MissingField { field: "ghost".into() }, MissingField { field: "qty.1".into() }]), ("built".to_owned(), vec![BuiltNames]), ] };