mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 09:33:45 +00:00
M6: a product of empty fields is 0, not 1 (form.af-simple-calculate) (#6567)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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) };
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user