Thanks @SychPL.
This commit is contained in:
1 parent
64966bab88
commit
f350c35414
2 files changed
+72
-1
No files matched your search
@@ -359,6 +359,15 @@ 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.replace(',', ".").parse().map_err(|_| DocError::Expr(format!("bad number `{t}`")))?;
|
||||
// A mixed number, as imperial sizes are written (`2 1/8"` is 2.125 in, #1802): a whole
|
||||
// number, then a proper fraction of whole numbers, is one value, not `2 × 1/8`.
|
||||
if t.chars().all(|c| c.is_ascii_digit())
|
||||
&& let Some((fraction, end)) = mixed_fraction(&cs, i, depth)
|
||||
{
|
||||
out.push((Tok::Num(v + fraction), at(st), at(end)));
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
out.push((Tok::Num(v), at(st), at(i)));
|
||||
} else if c.is_alphabetic() || c == '_' || c == 'µ' {
|
||||
let st = i;
|
||||
@@ -404,6 +413,43 @@ fn lex_spans(s: &str) -> Result<Vec<(Tok, usize, usize)>, DocError> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The proper fraction of a mixed number whose whole part ends at `i`: spaces, digits, `/`,
|
||||
/// digits, with nothing after them that would continue the number (`2 1/8.5`, `2 1/8e3`, a
|
||||
/// decimal comma outside parentheses: `2 1/8,5`). Its value and where it ends. `depth`: the
|
||||
/// parentheses open there (inside them a comma separates arguments: `max(2 1/8,1)`).
|
||||
fn mixed_fraction(cs: &[(usize, char)], mut i: usize, depth: usize) -> Option<(f64, usize)> {
|
||||
let digits = |from: usize| cs.get(from..).map_or(0, |rest| rest.iter().take_while(|(_, c)| c.is_ascii_digit()).count());
|
||||
let space = i;
|
||||
while cs.get(i).is_some_and(|(_, c)| *c == ' ' || *c == '\t') {
|
||||
i += 1;
|
||||
}
|
||||
let numerator = digits(i);
|
||||
if i == space || numerator == 0 || !cs.get(i + numerator).is_some_and(|(_, c)| *c == '/') {
|
||||
return None;
|
||||
}
|
||||
let denominator = digits(i + numerator + 1);
|
||||
let end = i + numerator + 1 + denominator;
|
||||
let next = |k: usize| cs.get(end + k).map(|(_, c)| *c);
|
||||
let continues = matches!(next(0), Some('.'))
|
||||
|| (depth == 0 && matches!(next(0), Some(',')) && next(1).is_some_and(|c| c.is_ascii_digit()))
|
||||
|| (matches!(next(0), Some('e' | 'E')) && next(1).is_some_and(|c| c.is_ascii_digit() || c == '-' || c == '+'));
|
||||
if denominator == 0 || continues {
|
||||
return None;
|
||||
}
|
||||
// Compared as digits, not as floats: long ones round to the same float (#1802 review).
|
||||
let digits_of = |from: usize, len: usize| -> Option<String> {
|
||||
let s: String = cs.get(from..from + len)?.iter().map(|(_, c)| *c).collect();
|
||||
Some(s.trim_start_matches('0').to_string())
|
||||
};
|
||||
let (n, d) = (digits_of(i, numerator)?, digits_of(i + numerator + 1, denominator)?);
|
||||
let proper = !d.is_empty() && (n.len(), n.as_str()) < (d.len(), d.as_str());
|
||||
let value = |t: &str| if t.is_empty() { Some(0.0) } else { t.parse::<f64>().ok() };
|
||||
if !proper {
|
||||
return None;
|
||||
}
|
||||
Some((value(&n)? / value(&d)?, end))
|
||||
}
|
||||
|
||||
fn lex(s: &str) -> Result<Vec<Tok>, DocError> {
|
||||
Ok(lex_spans(s)?.into_iter().map(|(t, _, _)| t).collect())
|
||||
}
|
||||
@@ -965,6 +1011,29 @@ mod tests {
|
||||
assert_eq!(ev("\"", Kind::Length).ok(), ev("in", Kind::Length).ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_numbers() {
|
||||
// #1802: `2 1/8"` is 2.125 in, as imperial sizes are written, not 2 × 1/8 in.
|
||||
assert_eq!(ev("2 1/8\"", Kind::Length).unwrap(), 2.125 * 25.4);
|
||||
assert_eq!(ev("2 1/8 in", Kind::Length).unwrap(), 2.125 * 25.4);
|
||||
assert_eq!(ev("3 7/16\"", Kind::Length).unwrap(), 3.4375 * 25.4);
|
||||
assert_eq!(ev("10 1/2mm", Kind::Length).unwrap(), 10.5);
|
||||
assert_eq!(ev("width + 1 1/2", Kind::Length).unwrap(), 41.5);
|
||||
assert_eq!(ev("max(2 1/8, 3)", Kind::Unitless).unwrap(), 3.0);
|
||||
// Inside parentheses a comma after the fraction separates arguments; outside, a comma
|
||||
// between digits is a decimal comma, which continues the number (as before).
|
||||
assert_eq!(ev("max(2 1/8,1)", Kind::Unitless).unwrap(), 2.125);
|
||||
assert_eq!(ev("2 1/8,5", Kind::Unitless).unwrap(), 2.0 / 8.5);
|
||||
// Long numerators and denominators are compared as digits, not as rounded floats.
|
||||
assert!((ev("2 9007199254740992/9007199254740993", Kind::Unitless).unwrap() - 3.0).abs() < 1e-12);
|
||||
assert_eq!(ev("2 007/8", Kind::Unitless).unwrap(), 2.875);
|
||||
assert_eq!(ev("-2 1/4", Kind::Unitless).unwrap(), -2.25);
|
||||
// Not a proper fraction of whole numbers after a whole number: as before.
|
||||
for (s, v) in [("2 3/2", 3.0), ("2.5 1/2", 1.25), ("2 1/8.5", 2.0 / 8.5), ("2 1 / 8", 0.25), ("2 1/0", f64::INFINITY), ("1/8", 0.125)] {
|
||||
assert_eq!(ev(s, Kind::Unitless).ok(), Some(v).filter(|v| v.is_finite()), "{s}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithmetic_and_units() {
|
||||
assert_eq!(ev("10", Kind::Length).unwrap(), 10.0);
|
||||
|
||||
@@ -19,7 +19,9 @@ Expressions are unit-aware: `2 * width + 5 mm`, `angle / 2`, `sqrt(area)`, `1 in
|
||||
`+ - * / ^`, parentheses, `sin cos tan asin acos atan atan2 sqrt abs min max floor ceil round mod
|
||||
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
|
||||
`max(2.5, 3)` there). A mixed number is one value, as imperial sizes are written: `2 1/8"` and
|
||||
`3 7/16 in` are 2.125 in and 3.4375 in (a whole number, a space, then a proper fraction of whole
|
||||
numbers). 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