From 7db8b5b746a3e728a28fd677222ac31b0edd34f8 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 12:01:27 +0000 Subject: [PATCH] functions: NUMBERVALUE strips trailing percent signs in one pass Each trailing % copied the rest of the text again, so a long run of them made one call quadratic. The signs are now counted and cut off in place. --- crates/functions/src/text.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/functions/src/text.rs b/crates/functions/src/text.rs index 7f1ba8731..bf4d51d22 100644 --- a/crates/functions/src/text.rs +++ b/crates/functions/src/text.rs @@ -339,11 +339,11 @@ fn numbervalue(a: &[Arg], c: &mut dyn Ctx) -> R { if body.is_empty() { return num_val(0.0); } - let mut pct = 0; - while let Some(b) = body.strip_suffix('%') { - body = b.to_string(); - pct += 1; - } + // Count the trailing percent signs and cut them off in place: copying the rest once per sign + // made a long run of them quadratic. + let kept = body.trim_end_matches('%').len(); + let pct = body.len() - kept; + body.truncate(kept); let mut seen_dec = false; let mut out = String::new(); for c in body.chars() { @@ -1084,6 +1084,15 @@ mod tests { use crate::util::testutil::*; + /// Trailing percent signs each divide by 100, and a long run of them is cut off in one pass. + #[test] + fn numbervalue_percent_signs() { + close(ev("NUMBERVALUE", vec![t("5%%")]), 0.0005); + close(ev("NUMBERVALUE", vec![t("1,5%"), t(","), t(".")]), 0.015); + let many = format!("1{}", "%".repeat(32_000)); + close(ev("NUMBERVALUE", vec![t(&many)]), 0.0); + } + #[test] fn bahttext() { let bt = |x: f64| ev("BAHTTEXT", vec![n(x)]);