"2,5 mm" is 2.5 mm; inside a function's parentheses the comma still separates the arguments. Noted under design format 22: an expression typed with a decimal comma is saved as typed, so 0.12.0 asks for an update instead of failing to evaluate it.
This commit is contained in:
1 parent
e8c340c59d
commit
2bfec256b7
3 files changed
+45
-2
No files matched your search
+38
-1
@@ -237,6 +237,8 @@ fn lex_spans(s: &str) -> Result<Vec<(Tok, usize, usize)>, DocError> {
|
||||
let at = |i: usize| cs.get(i).map(|x| x.0).unwrap_or(s.len());
|
||||
let mut i = 0;
|
||||
let mut out = Vec::new();
|
||||
// Parentheses open at this point: a comma outside them can only be a decimal comma.
|
||||
let mut depth = 0usize;
|
||||
while let Some(&(_, c)) = cs.get(i) {
|
||||
if c.is_whitespace() {
|
||||
i += 1;
|
||||
@@ -245,6 +247,17 @@ fn lex_spans(s: &str) -> Result<Vec<(Tok, usize, usize)>, DocError> {
|
||||
while cs.get(i).is_some_and(|(_, c)| c.is_ascii_digit() || *c == '.') {
|
||||
i += 1;
|
||||
}
|
||||
// A decimal comma (`2,5 mm`, as typed with a numeric keypad in many locales, #1347):
|
||||
// a comma between digits, in a number with no point, outside any parentheses (inside
|
||||
// them a comma separates a function's arguments: `max(1,2)`).
|
||||
let digit = |k: usize| cs.get(k).is_some_and(|(_, c)| c.is_ascii_digit());
|
||||
let has_point = cs.get(st..i).is_some_and(|ds| ds.iter().any(|(_, c)| *c == '.'));
|
||||
if depth == 0 && !has_point && digit(i.wrapping_sub(1)) && cs.get(i).is_some_and(|(_, c)| *c == ',') && digit(i + 1) {
|
||||
i += 1;
|
||||
while digit(i) {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
// Exponent: 1e-3 (but not `2em`).
|
||||
if cs.get(i).is_some_and(|(_, c)| *c == 'e' || *c == 'E') {
|
||||
let sign = cs.get(i + 1).is_some_and(|(_, c)| *c == '-' || *c == '+');
|
||||
@@ -257,7 +270,7 @@ fn lex_spans(s: &str) -> Result<Vec<(Tok, usize, usize)>, DocError> {
|
||||
}
|
||||
}
|
||||
let t = s.get(at(st)..at(i)).unwrap_or_default();
|
||||
let v: f64 = t.parse().map_err(|_| DocError::Expr(format!("bad number `{t}`")))?;
|
||||
let v: f64 = t.replace(',', ".").parse().map_err(|_| DocError::Expr(format!("bad number `{t}`")))?;
|
||||
out.push((Tok::Num(v), at(st), at(i)));
|
||||
} else if c.is_alphabetic() || c == '_' || c == 'µ' {
|
||||
let st = i;
|
||||
@@ -282,6 +295,11 @@ fn lex_spans(s: &str) -> Result<Vec<(Tok, usize, usize)>, DocError> {
|
||||
out.push((Tok::Op(','), at(i), at(i + 1)));
|
||||
i += 1;
|
||||
} else if "+-*/^(),%".contains(c) {
|
||||
match c {
|
||||
'(' => depth += 1,
|
||||
')' => depth = depth.saturating_sub(1),
|
||||
_ => {}
|
||||
}
|
||||
out.push((Tok::Op(c), at(i), at(i + 1)));
|
||||
i += 1;
|
||||
} else {
|
||||
@@ -819,6 +837,25 @@ mod tests {
|
||||
ParamDef { name: n.into(), expr: e.into(), unit: u.into() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_decimal_comma_outside_parentheses() {
|
||||
// #1347: a comma between digits is a decimal comma where it cannot separate arguments.
|
||||
assert_eq!(ev("2,5", Kind::Unitless).unwrap(), 2.5);
|
||||
assert_eq!(ev("2,5 mm", Kind::Length).unwrap(), 2.5);
|
||||
assert_eq!(ev("12,75mm + 0,25", Kind::Length).unwrap(), 13.0);
|
||||
assert_eq!(ev("width / 2,5", Kind::Length).unwrap(), 16.0);
|
||||
assert!((ev("1,5e3 mm", Kind::Length).unwrap() - 1500.0).abs() < 1e-9);
|
||||
assert!((ev("22,5 deg", Kind::Angle).unwrap() - 22.5f64.to_radians()).abs() < 1e-12);
|
||||
// Inside parentheses a comma still separates a function's arguments.
|
||||
assert_eq!(ev("max(1,2)", Kind::Unitless).unwrap(), 2.0);
|
||||
assert_eq!(ev("max(1 mm, 3, 2)", Kind::Length).unwrap(), 3.0);
|
||||
assert_eq!(ev("(2,5)", Kind::Unitless).ok(), None, "a comma inside parentheses is not a decimal comma");
|
||||
// Not between two digits, or a second comma: still an error.
|
||||
for bad in ["2,", ",5", "2 ,5", "2, 5", "1,5,3", "1.5,3"] {
|
||||
assert!(ev(bad, Kind::Unitless).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithmetic_and_units() {
|
||||
assert_eq!(ev("10", Kind::Length).unwrap(), 10.0);
|
||||
|
||||
@@ -208,6 +208,10 @@
|
||||
//! whose imports sit loose in a component are left as they are. The version stamp makes
|
||||
//! 0.12.0 (which reads up to 21) ask for an update instead of building those instances again
|
||||
//! on top of their occurrences.
|
||||
//! Also under 22, a number in an expression can be written with a decimal comma outside
|
||||
//! parentheses (`2,5 mm` is `2.5 mm`, #1347; inside a function's parentheses the comma still
|
||||
//! separates its arguments). Nothing new is saved, but an expression typed that way is saved as
|
||||
//! typed; 0.12.0 asks for an update instead of failing to evaluate it.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ does not also submit the parameter.
|
||||
|
||||
Expressions are unit-aware: `2 * width + 5 mm`, `angle / 2`, `sqrt(area)`, `1 in + 3 mm`, using
|
||||
`+ - * / ^`, parentheses, `sin cos tan asin acos atan atan2 sqrt abs min max floor ceil round mod
|
||||
trunc` and unit conversion. A length where an angle is expected (or the other way round) is an
|
||||
trunc` and unit conversion. A decimal comma works too, outside parentheses: `2,5 mm` is
|
||||
`2.5 mm` (inside a function's parentheses the comma separates its arguments, so write
|
||||
`max(2.5, 3)` there). A length where an angle is expected (or the other way round) is an
|
||||
error, not a silent conversion. Cycles are reported with their path (`a → b → a`), renaming a
|
||||
parameter updates every expression, dimension and feature input that uses it, and a parameter
|
||||
in use can't be deleted (the error lists its users).
|
||||
|
||||
Reference in new issue
Block a user