From 1524ceae096f3c8538d89f150aab775f7b52637b Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:40:40 +0000 Subject: [PATCH] Evaluate * and / left to right in field arithmetic Found while testing crates/doc/src/lib.rs:269-297 (parse_measure, used by Unit::parse and parse_number) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 Co-Authored-By: Claude Opus 5.5 --- crates/doc/src/lib.rs | 11 ++++++++--- crates/doc/src/tests_units.rs | 9 +++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 8742330bc..d6dbdcbae 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -270,17 +270,22 @@ impl Unit { fn parse_measure(s: &str, default: Unit) -> Option { let s = s.trim(); // Simple arithmetic on the right (Illustrator fields accept "10+5", "100/2", "3in*2"). - for op in ['+', '-', '*', '/'] { + // `*` and `/` share one precedence level, so they split at the last of either (left to right). + for ops in [&['+'][..], &['-'][..], &['*', '/'][..]] { // Past the first character, so a leading sign isn't an operator. let skip = s.chars().next().map_or(0, char::len_utf8); // A `+` or `-` right after another operator is the sign of its operand (`10*-2`, `5--3`). - let is_sign = |i: usize| matches!(op, '+' | '-') && s.get(..i).and_then(|l| l.trim_end().chars().last()).is_some_and(|c| "+-*/".contains(c)); - let found = s.char_indices().rev().find(|&(i, c)| i >= skip && c == op && !is_sign(i)); + let is_sign = |i: usize| { + s.get(i..).and_then(|r| r.chars().next()).is_some_and(|c| matches!(c, '+' | '-')) + && s.get(..i).and_then(|l| l.trim_end().chars().last()).is_some_and(|c| "+-*/".contains(c)) + }; + let found = s.char_indices().rev().find(|&(i, c)| i >= skip && ops.contains(&c) && !is_sign(i)); if let Some(i) = found.map(|(i, _)| i) { let (l, r) = (&s[..i], &s[i + 1..]); if l.trim().is_empty() { continue; } + let Some(op) = s[i..].chars().next() else { continue }; return match op { '+' => Some(parse_measure(l, default)? + parse_measure(r, default)?), '-' => Some(parse_measure(l, default)? - parse_measure(r, default)?), diff --git a/crates/doc/src/tests_units.rs b/crates/doc/src/tests_units.rs index 80a0dee21..fc1980622 100644 --- a/crates/doc/src/tests_units.rs +++ b/crates/doc/src/tests_units.rs @@ -77,3 +77,12 @@ fn a_negative_operand_after_an_operator_is_a_sign() { assert_eq!(Unit::Points.parse("10-2"), Some(8.0)); assert_eq!(super::parse_number("90*-1"), Some(-90.0)); } + +#[test] +fn mixed_mul_div() { + // `*` and `/` share one precedence level and run left to right. + assert_eq!(Unit::Points.parse("10/2*5"), Some(25.0)); + assert_eq!(crate::parse_number("100*3/4"), Some(75.0)); + assert_eq!(Unit::Points.parse("10*2/5"), Some(4.0)); + assert_eq!(Unit::Points.parse("10*2/5 px"), Some(Unit::Pixels.to_pt(4.0))); +}