M6: a product of empty fields is 0, not 1 (form.af-simple-calculate) (#6567)

This commit is contained in:
mtlcl410
2026-11-09 08:57:28 +08:00
committed by GitHub
parent 3e48317ae3
commit b9ffb68b12
4 changed files with 88 additions and 2 deletions
+4
View File
@@ -531,6 +531,10 @@ pub fn calculate(c: &Calculate, values: &dyn Fn(&str) -> Vec<String>) -> Option<
let num = |s: &String| make_number(s).unwrap_or(0.0);
match c {
Calculate::None => None,
// A product of no values is 0, as an empty sum, average, minimum or maximum is, not the
// empty product 1: a blank quantity × price row reads 0, and a total over the rows
// doesn't count it as 1. (The form's `values` leaves out a field that has no value.)
Calculate::Simple { op: CalcOp::Product, fields } if fields.iter().all(|f| values(f).is_empty()) => Some("0".into()),
Calculate::Simple { op, fields } => {
let xs: Vec<f64> = fields.iter().flat_map(|f| values(f)).map(|v| num(&v)).collect();
let v = match op {
+29
View File
@@ -360,6 +360,35 @@ fn dates_times_and_masks() {
assert!(keystroke(&Format::Mask("AA-9999".into()), "Code", "a11234").is_err());
}
#[test]
fn a_calculation_over_fields_without_values_is_0_for_every_operation() {
// A blank row: neither field has a value, so the form's lookup returns none.
let vals = |name: &str| -> Vec<String> { if name == "a" { vec!["2".into()] } else { vec![] } };
let simple = |op, f: &[&str]| calculate(&Calculate::Simple { op, fields: f.iter().map(|s| s.to_string()).collect() }, &vals);
for op in CalcOp::ALL {
assert_eq!(simple(op, &["qty", "price"]), Some("0".into()), "{op:?} of empty fields");
assert_eq!(simple(op, &[]), Some("0".into()), "{op:?} of no fields");
}
assert_eq!(simple(CalcOp::Product, &["a", "qty"]), Some("2".into()), "a field with no value is left out");
}
/// A drop-down's blank option is often a single space, the value it starts with. That is a value
/// worth 0, so a row whose quantity is still blank reads 0 × price = 0. Read as "no value", it
/// would be left out and the row would read the price alone.
#[test]
fn a_blank_option_of_a_single_space_is_a_value_worth_0() {
let vals = |name: &str| -> Vec<String> {
match name {
"qty" => vec![" ".into()],
"price" => vec!["605".into()],
_ => vec![],
}
};
let simple = |op, f: &[&str]| calculate(&Calculate::Simple { op, fields: f.iter().map(|s| s.to_string()).collect() }, &vals);
assert_eq!(simple(CalcOp::Product, &["qty", "price"]), Some("0".into()));
assert_eq!(simple(CalcOp::Product, &["qty", "none"]), Some("0".into()), "a blank quantity on a row without a price");
}
#[test]
fn validation_and_calculation() {
let r = Validate::Range { min: Some(0.0), max: Some(100.0) };
+53
View File
@@ -1943,6 +1943,59 @@ fn missing_fields_and_built_names_are_flagged() {
assert_eq!(order.fields[3].reads, ["qty"], "the names it does know are still read");
}
/// An order form's rows, as a public one lays them out: an editable drop-down quantity whose blank
/// option, and starting value, is a single space, a prefilled price, the row's product and a total
/// over the rows. A blank quantity is 0, so its row is 0; a quantity cleared to nothing on a row
/// without a price leaves the product nothing to multiply, and it is 0 too, not 1.
#[test]
fn an_order_form_with_blank_drop_down_quantities_totals_only_the_chosen_rows() {
let tx = |name: &str, y: u32, rest: &str| {
format!("<< /Type /Annot /Subtype /Widget /FT /Tx /T ({name}) /Rect [250 {y} 350 {}] /P 3 0 R {rest} >>", y + 20)
};
let qty = |name: &str, y: u32| {
format!(
"<< /Type /Annot /Subtype /Widget /FT /Ch /Ff 393216 /T ({name}) /Opt [( ) (1) (2) (3)] /V ( ) /Rect [50 {y} 150 {}] /P 3 0 R >>",
y + 20
)
};
let prd = |row: u32| {
format!("/AA << /C << /S /JavaScript /JS (AFSimple_Calculate\\(\"PRD\", new Array \\(\"QTY {row}\", \"price {row}\"\\)\\);) >> >>")
};
let objs: Vec<String> = vec![
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(),
"<< /Type /Page /Parent 2 0 R /Annots [5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R] >>".into(),
"<< /Fields [5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R] /DA (/Helv 0 Tf 0 g) >>".into(),
qty("QTY 0", 700),
tx("price 0", 700, "/V (605)"),
tx("Total 0", 700, &format!("/V (0) {}", prd(0))),
qty("QTY 1", 660),
tx("price 1", 660, "/V (780)"),
tx("Total 1", 660, &format!("/V (0) {}", prd(1))),
qty("QTY 2", 620),
tx("price 2", 620, ""),
tx("Total 2", 620, &format!("/V (0) {}", prd(2))),
tx(
"Total",
580,
r#"/V (0) /AA << /C << /S /JavaScript /JS (AFSimple_Calculate\("SUM", new Array \("Total 0", "Total 1", "Total 2"\)\);) >> >>"#,
),
];
let mut doc = document(&objs);
let totals = |doc: &Document| {
let all = fields(doc);
["Total 0", "Total 1", "Total 2", "Total"].map(|n| field(&all, n).value.first().cloned().unwrap_or_default())
};
recalculate(&mut doc).unwrap();
assert_eq!(totals(&doc), ["0", "0", "0", "0"], "every quantity blank: a single space is worth 0");
set_value(&mut doc, "QTY 0", &FieldValue::Text("2".into())).unwrap();
assert_eq!(totals(&doc), ["1210", "0", "0", "1210"], "only the chosen row");
set_value(&mut doc, "QTY 2", &FieldValue::Text(String::new())).unwrap();
assert_eq!(totals(&doc), ["1210", "0", "0", "1210"], "a quantity cleared on a row without a price");
let doc = reopen(&doc);
assert_eq!(totals(&doc), ["1210", "0", "0", "1210"], "kept across a save");
}
/// `/CO` as other tools leave it: a calculated field missing, a field without a calculation, an
/// entry repeated, a reference to nothing and a number. The order reads around them, and setting
/// the order writes `/CO` afresh.
+2 -2
View File
@@ -4770,8 +4770,8 @@ milestone = "M6"
status = "shipped"
commands = ["form.field.properties"]
tools = ["form_set_props", "form_fill"]
evidence = ["crates/forms/src/af/tests.rs::validation_and_calculation", "crates/forms/src/tests.rs::formats_validation_and_calculations_run_like_acrobat"]
notes = "SUM, PRD, AVG, MIN, MAX over fields and groups"
evidence = ["crates/forms/src/af/tests.rs::validation_and_calculation", "crates/forms/src/tests.rs::formats_validation_and_calculations_run_like_acrobat", "crates/forms/src/af/tests.rs::a_calculation_over_fields_without_values_is_0_for_every_operation", "crates/forms/src/af/tests.rs::a_blank_option_of_a_single_space_is_a_value_worth_0", "crates/forms/src/tests.rs::an_order_form_with_blank_drop_down_quantities_totals_only_the_chosen_rows"]
notes = "SUM, PRD, AVG, MIN, MAX over fields and groups; fields without a value are left out, and when none of the listed fields has one, every operation gives 0 (a product too, not 1); a drop-down's blank option of a single space is a value worth 0"
[[feature]]
id = "form.simplified-notation"