feat(math): exact evaluation of handwritten expressions (Issue #46, stage 1)
New module math.rs: solve() reads a recognised expression such as 2^4, 2,5 · 3 or (x + 3)² and returns its result as text (16, 7,5, x² + 6x + 9). Own parser with exact fractions and polynomial expansion instead of evalexpr, as implicit multiplication and expanding need it anyway. Not used by the UI yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8af10fe542
commit
cd490dd9a7
4 files changed
+659
No files matched your search
@@ -191,6 +191,7 @@ src/
|
||||
├── storage.rs # Laden/Speichern (atomar), Notizbücher, Papierkorb, Speicherort
|
||||
├── disk_watch.rs # Änderungen anderer Geräte übernehmen (Sync-Clients)
|
||||
├── library.rs # Ordner der Notizbücher (library.json)
|
||||
├── math.rs # Exaktes Ausrechnen handgeschriebener Formeln (Issue #46)
|
||||
├── autosave.rs # Speichern im Hintergrund
|
||||
├── export.rs # PDF-Export
|
||||
├── page_export.rs # Seite als PNG/SVG, Auswahl als Bild
|
||||
|
||||
@@ -193,6 +193,7 @@ src/
|
||||
├── storage.rs # Loading/saving (atomic), notebooks, trash, storage location
|
||||
├── disk_watch.rs # Taking over changes from other devices (sync clients)
|
||||
├── library.rs # Notebook folders (library.json)
|
||||
├── math.rs # Exact evaluation of handwritten math (Issue #46)
|
||||
├── autosave.rs # Saving in the background
|
||||
├── export.rs # PDF export
|
||||
├── page_export.rs # Page as PNG/SVG, selection as image
|
||||
|
||||
@@ -18,6 +18,7 @@ pub mod i18n;
|
||||
pub mod image_layer;
|
||||
pub mod import;
|
||||
pub mod library;
|
||||
pub mod math;
|
||||
pub mod osk;
|
||||
pub mod page_export;
|
||||
pub mod paths;
|
||||
|
||||
+656
@@ -0,0 +1,656 @@
|
||||
//! Exact evaluation of handwritten math (Issue #46, stage 1).
|
||||
//!
|
||||
//! Takes the text of a recognised expression such as `2^4`, `2,5 · 3` or `(x + 3)²`
|
||||
//! (optionally ending with `=`) and returns its result as text: `16`, `7,5` and
|
||||
//! `x² + 6x + 9`. Numbers are exact fractions, so `1 : 3 · 3` gives `1`, and expressions
|
||||
//! with variables are expanded into polynomials.
|
||||
//!
|
||||
//! The input is lenient towards how math is written by hand: decimal comma or point,
|
||||
//! `·`, `×`, `*` and `:`, `÷`, `/`, superscript exponents, Unicode minus signs and implicit
|
||||
//! multiplication (`2x`, `3(x + 1)`, `(a + b)(a − b)`).
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::Write as _;
|
||||
|
||||
/// Largest exponent for a polynomial that is not a plain number
|
||||
const MAX_POLYNOMIAL_EXPONENT: u32 = 32;
|
||||
/// Largest exponent for a plain number
|
||||
const MAX_NUMBER_EXPONENT: u32 = 1000;
|
||||
/// Results with more terms are not shown (they would not fit next to the `=` anyway)
|
||||
const MAX_TERMS: usize = 64;
|
||||
/// Terminating decimals with more digits after the separator are shown as fractions
|
||||
const MAX_DECIMALS: u32 = 12;
|
||||
|
||||
/// Computes the result of `expression`, written for display after its `=` sign.
|
||||
///
|
||||
/// `decimal_comma` selects the decimal separator of the result. Returns `None` if the
|
||||
/// expression cannot be read or computed (e.g. division by zero, a variable in a
|
||||
/// denominator or overflow), and also if there is nothing to compute, i.e. the result would
|
||||
/// just repeat the input (`5`, `x`, `2x`).
|
||||
pub fn solve(expression: &str, decimal_comma: bool) -> Option<String> {
|
||||
let tokens = tokenize(expression)?;
|
||||
let ast = Parser::new(&tokens).parse()?;
|
||||
if matches!(ast, Expr::Number(_) | Expr::Variable(_)) {
|
||||
return None;
|
||||
}
|
||||
let result = ast.eval()?;
|
||||
if result.terms.len() > MAX_TERMS {
|
||||
return None;
|
||||
}
|
||||
let text = result.format(decimal_comma);
|
||||
let compact = |s: &str| -> String {
|
||||
s.chars()
|
||||
.filter(|c| !c.is_whitespace())
|
||||
.map(|c| if c == '−' { '-' } else { c })
|
||||
.collect()
|
||||
};
|
||||
if compact(&text) == compact(expression.trim().trim_end_matches('=')) {
|
||||
return None;
|
||||
}
|
||||
Some(text)
|
||||
}
|
||||
|
||||
// --- Rational numbers ---------------------------------------------------------------------
|
||||
|
||||
/// Exact fraction `num / den` with `den > 0`, always reduced
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct Rational {
|
||||
num: i128,
|
||||
den: i128,
|
||||
}
|
||||
|
||||
fn gcd(mut a: i128, mut b: i128) -> i128 {
|
||||
a = a.abs();
|
||||
b = b.abs();
|
||||
while b != 0 {
|
||||
(a, b) = (b, a % b);
|
||||
}
|
||||
a
|
||||
}
|
||||
|
||||
impl Rational {
|
||||
const ZERO: Rational = Rational { num: 0, den: 1 };
|
||||
const ONE: Rational = Rational { num: 1, den: 1 };
|
||||
|
||||
fn new(num: i128, den: i128) -> Option<Rational> {
|
||||
// i128::MIN has no positive counterpart, which `gcd` and `neg` need
|
||||
if den == 0 || num == i128::MIN || den == i128::MIN {
|
||||
return None;
|
||||
}
|
||||
let g = gcd(num, den).max(1);
|
||||
let (mut num, mut den) = (num / g, den / g);
|
||||
if den < 0 {
|
||||
num = num.checked_neg()?;
|
||||
den = den.checked_neg()?;
|
||||
}
|
||||
Some(Rational { num, den })
|
||||
}
|
||||
|
||||
fn integer(n: i128) -> Rational {
|
||||
Rational { num: n, den: 1 }
|
||||
}
|
||||
|
||||
fn is_zero(self) -> bool {
|
||||
self.num == 0
|
||||
}
|
||||
|
||||
fn add(self, other: Rational) -> Option<Rational> {
|
||||
let g = gcd(self.den, other.den);
|
||||
let den = (self.den / g).checked_mul(other.den)?;
|
||||
let num = self
|
||||
.num
|
||||
.checked_mul(other.den / g)?
|
||||
.checked_add(other.num.checked_mul(self.den / g)?)?;
|
||||
Rational::new(num, den)
|
||||
}
|
||||
|
||||
fn neg(self) -> Option<Rational> {
|
||||
Some(Rational {
|
||||
num: self.num.checked_neg()?,
|
||||
den: self.den,
|
||||
})
|
||||
}
|
||||
|
||||
fn mul(self, other: Rational) -> Option<Rational> {
|
||||
// Cross-reducing first keeps the intermediate products small
|
||||
let g1 = gcd(self.num, other.den).max(1);
|
||||
let g2 = gcd(other.num, self.den).max(1);
|
||||
let num = (self.num / g1).checked_mul(other.num / g2)?;
|
||||
let den = (self.den / g2).checked_mul(other.den / g1)?;
|
||||
Rational::new(num, den)
|
||||
}
|
||||
|
||||
fn recip(self) -> Option<Rational> {
|
||||
Rational::new(self.den, self.num)
|
||||
}
|
||||
|
||||
fn pow(self, exponent: i128) -> Option<Rational> {
|
||||
if exponent.unsigned_abs() > MAX_NUMBER_EXPONENT as u128 {
|
||||
return None;
|
||||
}
|
||||
let base = if exponent < 0 { self.recip()? } else { self };
|
||||
let mut result = Rational::ONE;
|
||||
for _ in 0..exponent.unsigned_abs() {
|
||||
result = result.mul(base)?;
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
|
||||
fn as_integer(self) -> Option<i128> {
|
||||
(self.den == 1).then_some(self.num)
|
||||
}
|
||||
|
||||
/// The number without its sign: as a terminating decimal if it has one with at most
|
||||
/// [`MAX_DECIMALS`] digits, otherwise as a fraction `p/q`.
|
||||
fn format_abs(self, decimal_comma: bool) -> String {
|
||||
let num = self.num.unsigned_abs();
|
||||
let den = self.den.unsigned_abs();
|
||||
let whole = num / den;
|
||||
let mut rest = num % den;
|
||||
if rest == 0 {
|
||||
return whole.to_string();
|
||||
}
|
||||
let mut reduced = den;
|
||||
let mut decimals = 0;
|
||||
for factor in [2, 5] {
|
||||
while reduced.is_multiple_of(factor) {
|
||||
reduced /= factor;
|
||||
}
|
||||
}
|
||||
if reduced == 1 {
|
||||
let mut digits = String::new();
|
||||
while rest != 0 && decimals <= MAX_DECIMALS {
|
||||
let Some(shifted) = rest.checked_mul(10) else {
|
||||
break;
|
||||
};
|
||||
rest = shifted;
|
||||
digits.push(char::from(b'0' + (rest / den) as u8));
|
||||
rest %= den;
|
||||
decimals += 1;
|
||||
}
|
||||
if rest == 0 {
|
||||
let separator = if decimal_comma { ',' } else { '.' };
|
||||
return format!("{whole}{separator}{digits}");
|
||||
}
|
||||
}
|
||||
format!("{num}/{den}")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Polynomials --------------------------------------------------------------------------
|
||||
|
||||
/// Product of variables with their exponents, e.g. `a²b` = `{a: 2, b: 1}`
|
||||
type Monomial = BTreeMap<char, u32>;
|
||||
|
||||
/// Sum of monomials with exact coefficients; a plain number is the empty monomial
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct Polynomial {
|
||||
terms: BTreeMap<Monomial, Rational>,
|
||||
}
|
||||
|
||||
impl Polynomial {
|
||||
fn constant(value: Rational) -> Polynomial {
|
||||
let mut terms = BTreeMap::new();
|
||||
if !value.is_zero() {
|
||||
terms.insert(Monomial::new(), value);
|
||||
}
|
||||
Polynomial { terms }
|
||||
}
|
||||
|
||||
fn variable(name: char) -> Polynomial {
|
||||
let mut terms = BTreeMap::new();
|
||||
terms.insert(Monomial::from([(name, 1)]), Rational::ONE);
|
||||
Polynomial { terms }
|
||||
}
|
||||
|
||||
/// The value if this is a plain number
|
||||
fn as_constant(&self) -> Option<Rational> {
|
||||
match self.terms.len() {
|
||||
0 => Some(Rational::ZERO),
|
||||
1 => self.terms.get(&Monomial::new()).copied(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_term(&mut self, monomial: Monomial, coefficient: Rational) -> Option<()> {
|
||||
let sum = match self.terms.get(&monomial) {
|
||||
Some(existing) => existing.add(coefficient)?,
|
||||
None => coefficient,
|
||||
};
|
||||
if sum.is_zero() {
|
||||
self.terms.remove(&monomial);
|
||||
} else {
|
||||
self.terms.insert(monomial, sum);
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn add(mut self, other: &Polynomial) -> Option<Polynomial> {
|
||||
for (monomial, coefficient) in &other.terms {
|
||||
self.add_term(monomial.clone(), *coefficient)?;
|
||||
}
|
||||
Some(self)
|
||||
}
|
||||
|
||||
fn neg(&self) -> Option<Polynomial> {
|
||||
let mut terms = BTreeMap::new();
|
||||
for (monomial, coefficient) in &self.terms {
|
||||
terms.insert(monomial.clone(), coefficient.neg()?);
|
||||
}
|
||||
Some(Polynomial { terms })
|
||||
}
|
||||
|
||||
fn mul(&self, other: &Polynomial) -> Option<Polynomial> {
|
||||
let mut product = Polynomial::constant(Rational::ZERO);
|
||||
for (m1, c1) in &self.terms {
|
||||
for (m2, c2) in &other.terms {
|
||||
let mut monomial = m1.clone();
|
||||
for (&name, &exponent) in m2 {
|
||||
let e = monomial.entry(name).or_insert(0);
|
||||
*e = e.checked_add(exponent)?;
|
||||
}
|
||||
product.add_term(monomial, c1.mul(*c2)?)?;
|
||||
}
|
||||
if product.terms.len() > MAX_TERMS * 4 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(product)
|
||||
}
|
||||
|
||||
fn div(&self, divisor: &Polynomial) -> Option<Polynomial> {
|
||||
let factor = divisor.as_constant()?.recip()?;
|
||||
self.mul(&Polynomial::constant(factor))
|
||||
}
|
||||
|
||||
fn pow(&self, exponent: &Polynomial) -> Option<Polynomial> {
|
||||
let exponent = exponent.as_constant()?.as_integer()?;
|
||||
if let Some(base) = self.as_constant() {
|
||||
return Some(Polynomial::constant(base.pow(exponent)?));
|
||||
}
|
||||
if !(0..=MAX_POLYNOMIAL_EXPONENT as i128).contains(&exponent) {
|
||||
return None;
|
||||
}
|
||||
let mut result = Polynomial::constant(Rational::ONE);
|
||||
for _ in 0..exponent {
|
||||
result = result.mul(self)?;
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
|
||||
/// E.g. `x² + 6x + 9`, `a² − b²`, `7,5`: highest degree first, then in alphabetical
|
||||
/// order of the variables (`a² + 2ab + b²`).
|
||||
fn format(&self, decimal_comma: bool) -> String {
|
||||
if self.terms.is_empty() {
|
||||
return "0".to_owned();
|
||||
}
|
||||
let degree = |m: &Monomial| m.values().sum::<u32>();
|
||||
let mut terms: Vec<_> = self.terms.iter().collect();
|
||||
terms.sort_by(|(a, _), (b, _)| degree(b).cmp(°ree(a)).then_with(|| graded_order(a, b)));
|
||||
let mut out = String::new();
|
||||
for (index, (monomial, coefficient)) in terms.into_iter().enumerate() {
|
||||
let negative = coefficient.num < 0;
|
||||
match (index, negative) {
|
||||
(0, true) => out.push('−'),
|
||||
(0, false) => {}
|
||||
(_, true) => out.push_str(" − "),
|
||||
(_, false) => out.push_str(" + "),
|
||||
}
|
||||
let magnitude = coefficient.format_abs(decimal_comma);
|
||||
if monomial.is_empty() {
|
||||
out.push_str(&magnitude);
|
||||
continue;
|
||||
}
|
||||
if magnitude.contains('/') {
|
||||
let _ = write!(out, "({magnitude})");
|
||||
} else if magnitude != "1" {
|
||||
out.push_str(&magnitude);
|
||||
}
|
||||
for (&name, &exponent) in monomial {
|
||||
out.push(name);
|
||||
if exponent > 1 {
|
||||
out.push_str(&superscript(exponent));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Order within the same degree: the monomial with the higher power of the alphabetically
|
||||
/// first variable comes first (`a²`, `ab`, `b²`).
|
||||
fn graded_order(a: &Monomial, b: &Monomial) -> Ordering {
|
||||
let names: std::collections::BTreeSet<char> = a.keys().chain(b.keys()).copied().collect();
|
||||
for name in names {
|
||||
let ea = a.get(&name).copied().unwrap_or(0);
|
||||
let eb = b.get(&name).copied().unwrap_or(0);
|
||||
match eb.cmp(&ea) {
|
||||
Ordering::Equal => continue,
|
||||
other => return other,
|
||||
}
|
||||
}
|
||||
Ordering::Equal
|
||||
}
|
||||
|
||||
const SUPERSCRIPT_DIGITS: [char; 10] = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'];
|
||||
|
||||
fn superscript(n: u32) -> String {
|
||||
n.to_string()
|
||||
.chars()
|
||||
.map(|d| SUPERSCRIPT_DIGITS[d.to_digit(10).unwrap_or(0) as usize])
|
||||
.collect()
|
||||
}
|
||||
|
||||
// --- Reading the expression ---------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum Token {
|
||||
Number(Rational),
|
||||
Variable(char),
|
||||
Plus,
|
||||
Minus,
|
||||
Times,
|
||||
Divide,
|
||||
Power,
|
||||
Open,
|
||||
Close,
|
||||
}
|
||||
|
||||
fn tokenize(input: &str) -> Option<Vec<Token>> {
|
||||
let input = input.trim().trim_end_matches('=').trim_end();
|
||||
let chars: Vec<char> = input.chars().collect();
|
||||
let mut tokens = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < chars.len() {
|
||||
let c = chars[i];
|
||||
if c.is_ascii_digit() {
|
||||
let (value, next) = read_number(&chars, i)?;
|
||||
tokens.push(Token::Number(value));
|
||||
i = next;
|
||||
continue;
|
||||
}
|
||||
if superscript_value(c).is_some() || c == '⁻' {
|
||||
// A run of superscript characters is the exponent: x²³ = x^23, 2⁻¹ = 2^(-1)
|
||||
let negative = c == '⁻';
|
||||
let start = if negative { i + 1 } else { i };
|
||||
let mut end = start;
|
||||
let mut value: i128 = 0;
|
||||
while let Some(digit) = chars.get(end).and_then(|&d| superscript_value(d)) {
|
||||
value = value.checked_mul(10)?.checked_add(digit)?;
|
||||
end += 1;
|
||||
}
|
||||
if end == start {
|
||||
return None;
|
||||
}
|
||||
tokens.push(Token::Power);
|
||||
if negative {
|
||||
tokens.push(Token::Minus);
|
||||
}
|
||||
tokens.push(Token::Number(Rational::integer(value)));
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
let token = match c {
|
||||
_ if c.is_whitespace() => None,
|
||||
'+' => Some(Token::Plus),
|
||||
'-' | '−' | '–' => Some(Token::Minus),
|
||||
'*' | '·' | '⋅' | '∙' | '×' => Some(Token::Times),
|
||||
'/' | ':' | '÷' => Some(Token::Divide),
|
||||
'^' => Some(Token::Power),
|
||||
'(' | '[' | '{' => Some(Token::Open),
|
||||
')' | ']' | '}' => Some(Token::Close),
|
||||
_ if c.is_ascii_alphabetic() => Some(Token::Variable(c)),
|
||||
_ => return None,
|
||||
};
|
||||
tokens.extend(token);
|
||||
i += 1;
|
||||
}
|
||||
Some(tokens)
|
||||
}
|
||||
|
||||
fn superscript_value(c: char) -> Option<i128> {
|
||||
SUPERSCRIPT_DIGITS
|
||||
.iter()
|
||||
.position(|&d| d == c)
|
||||
.map(|p| p as i128)
|
||||
}
|
||||
|
||||
/// Reads `12`, `2,5` or `0.125` from `start`; returns the value and the index after it
|
||||
fn read_number(chars: &[char], start: usize) -> Option<(Rational, usize)> {
|
||||
let mut i = start;
|
||||
let mut num: i128 = 0;
|
||||
let mut den: i128 = 1;
|
||||
let mut in_fraction = false;
|
||||
while i < chars.len() {
|
||||
let c = chars[i];
|
||||
if let Some(digit) = c.to_digit(10) {
|
||||
num = num.checked_mul(10)?.checked_add(digit as i128)?;
|
||||
if in_fraction {
|
||||
den = den.checked_mul(10)?;
|
||||
}
|
||||
} else if (c == ',' || c == '.')
|
||||
&& !in_fraction
|
||||
&& chars.get(i + 1).is_some_and(|d| d.is_ascii_digit())
|
||||
{
|
||||
in_fraction = true;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
Some((Rational::new(num, den)?, i))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum Expr {
|
||||
Number(Rational),
|
||||
Variable(char),
|
||||
Neg(Box<Expr>),
|
||||
Add(Box<Expr>, Box<Expr>),
|
||||
Sub(Box<Expr>, Box<Expr>),
|
||||
Mul(Box<Expr>, Box<Expr>),
|
||||
Div(Box<Expr>, Box<Expr>),
|
||||
Pow(Box<Expr>, Box<Expr>),
|
||||
}
|
||||
|
||||
impl Expr {
|
||||
fn eval(&self) -> Option<Polynomial> {
|
||||
Some(match self {
|
||||
Expr::Number(value) => Polynomial::constant(*value),
|
||||
Expr::Variable(name) => Polynomial::variable(*name),
|
||||
Expr::Neg(e) => e.eval()?.neg()?,
|
||||
Expr::Add(a, b) => a.eval()?.add(&b.eval()?)?,
|
||||
Expr::Sub(a, b) => a.eval()?.add(&b.eval()?.neg()?)?,
|
||||
Expr::Mul(a, b) => a.eval()?.mul(&b.eval()?)?,
|
||||
Expr::Div(a, b) => a.eval()?.div(&b.eval()?)?,
|
||||
Expr::Pow(a, b) => a.eval()?.pow(&b.eval()?)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursive descent with the usual precedence: `+ −` < `· :` and implicit
|
||||
/// multiplication < unary minus < `^` (right-associative, so `−2² = −4` and `2^3^2 = 2^9`).
|
||||
struct Parser<'a> {
|
||||
tokens: &'a [Token],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> Parser<'a> {
|
||||
fn new(tokens: &'a [Token]) -> Self {
|
||||
Parser { tokens, pos: 0 }
|
||||
}
|
||||
|
||||
fn parse(mut self) -> Option<Expr> {
|
||||
let expr = self.sum()?;
|
||||
(self.pos == self.tokens.len()).then_some(expr)
|
||||
}
|
||||
|
||||
fn peek(&self) -> Option<&Token> {
|
||||
self.tokens.get(self.pos)
|
||||
}
|
||||
|
||||
fn eat(&mut self, token: &Token) -> bool {
|
||||
let found = self.peek() == Some(token);
|
||||
if found {
|
||||
self.pos += 1;
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
fn sum(&mut self) -> Option<Expr> {
|
||||
let mut left = self.product()?;
|
||||
loop {
|
||||
if self.eat(&Token::Plus) {
|
||||
left = Expr::Add(Box::new(left), Box::new(self.product()?));
|
||||
} else if self.eat(&Token::Minus) {
|
||||
left = Expr::Sub(Box::new(left), Box::new(self.product()?));
|
||||
} else {
|
||||
return Some(left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn product(&mut self) -> Option<Expr> {
|
||||
let mut left = self.unary()?;
|
||||
loop {
|
||||
if self.eat(&Token::Times) {
|
||||
left = Expr::Mul(Box::new(left), Box::new(self.unary()?));
|
||||
} else if self.eat(&Token::Divide) {
|
||||
left = Expr::Div(Box::new(left), Box::new(self.unary()?));
|
||||
} else if matches!(self.peek(), Some(Token::Variable(_) | Token::Open)) {
|
||||
// Implicit multiplication: 2x, 3(x + 1), (a + b)(a − b). Two numbers in a
|
||||
// row are not multiplied, they are more likely a misread.
|
||||
left = Expr::Mul(Box::new(left), Box::new(self.power()?));
|
||||
} else {
|
||||
return Some(left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unary(&mut self) -> Option<Expr> {
|
||||
if self.eat(&Token::Minus) {
|
||||
return Some(Expr::Neg(Box::new(self.unary()?)));
|
||||
}
|
||||
if self.eat(&Token::Plus) {
|
||||
return self.unary();
|
||||
}
|
||||
self.power()
|
||||
}
|
||||
|
||||
fn power(&mut self) -> Option<Expr> {
|
||||
let base = self.atom()?;
|
||||
if self.eat(&Token::Power) {
|
||||
return Some(Expr::Pow(Box::new(base), Box::new(self.unary()?)));
|
||||
}
|
||||
Some(base)
|
||||
}
|
||||
|
||||
fn atom(&mut self) -> Option<Expr> {
|
||||
let token = self.peek()?.clone();
|
||||
self.pos += 1;
|
||||
match token {
|
||||
Token::Number(value) => Some(Expr::Number(value)),
|
||||
Token::Variable(name) => Some(Expr::Variable(name)),
|
||||
Token::Open => {
|
||||
let inner = self.sum()?;
|
||||
self.eat(&Token::Close).then_some(inner)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn de(expression: &str) -> Option<String> {
|
||||
solve(expression, true)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithmetic() {
|
||||
assert_eq!(de("2^4 ="), Some("16".into()));
|
||||
assert_eq!(de("2⁴="), Some("16".into()));
|
||||
assert_eq!(de("1 + 2 · 3"), Some("7".into()));
|
||||
assert_eq!(de("(1 + 2) × 3"), Some("9".into()));
|
||||
assert_eq!(de("7 − 10"), Some("−3".into()));
|
||||
assert_eq!(de("−2^2"), Some("−4".into()));
|
||||
assert_eq!(de("(−2)²"), Some("4".into()));
|
||||
assert_eq!(de("2^3^2"), Some("512".into()));
|
||||
assert_eq!(de("10 : 4"), Some("2,5".into()));
|
||||
assert_eq!(de("10 ÷ 4"), Some("2,5".into()));
|
||||
assert_eq!(de("3 * [2 + 1]"), Some("9".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decimals_and_fractions() {
|
||||
assert_eq!(de("2,5 · 3"), Some("7,5".into()));
|
||||
assert_eq!(solve("2.5 * 3", false), Some("7.5".into()));
|
||||
assert_eq!(de("0,1 + 0,2"), Some("0,3".into()));
|
||||
assert_eq!(de("1 : 3"), Some("1/3".into()));
|
||||
assert_eq!(de("1 : 3 · 3"), Some("1".into()));
|
||||
assert_eq!(de("2^-2"), Some("0,25".into()));
|
||||
assert_eq!(de("2⁻¹"), Some("0,5".into()));
|
||||
assert_eq!(de("1 : 8"), Some("0,125".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polynomials() {
|
||||
assert_eq!(de("(x + 3)^2 ="), Some("x² + 6x + 9".into()));
|
||||
assert_eq!(de("(x+3)²"), Some("x² + 6x + 9".into()));
|
||||
assert_eq!(de("(a + b)²"), Some("a² + 2ab + b²".into()));
|
||||
assert_eq!(de("(a + b)(a − b)"), Some("a² − b²".into()));
|
||||
assert_eq!(de("(a − b)²"), Some("a² − 2ab + b²".into()));
|
||||
assert_eq!(de("2x + 3x"), Some("5x".into()));
|
||||
assert_eq!(de("x · x"), Some("x²".into()));
|
||||
assert_eq!(de("3(x + 1)"), Some("3x + 3".into()));
|
||||
assert_eq!(de("(x + 1)^3"), Some("x³ + 3x² + 3x + 1".into()));
|
||||
assert_eq!(de("(2x − 1)²"), Some("4x² − 4x + 1".into()));
|
||||
assert_eq!(de("x − x"), Some("0".into()));
|
||||
assert_eq!(de("(x + 1) : 2"), Some("0,5x + 0,5".into()));
|
||||
assert_eq!(de("x : 3"), Some("(1/3)x".into()));
|
||||
assert_eq!(de("−(x + 1)"), Some("−x − 1".into()));
|
||||
assert_eq!(
|
||||
de("(x+1)^10").map(|s| s.starts_with("x¹⁰ + 10x⁹")),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_to_compute() {
|
||||
assert_eq!(de("5"), None);
|
||||
assert_eq!(de("x ="), None);
|
||||
assert_eq!(de("2x"), None);
|
||||
assert_eq!(de("x² + 1"), None);
|
||||
assert_eq!(de("2,5"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unreadable_or_not_computable() {
|
||||
assert_eq!(de(""), None);
|
||||
assert_eq!(de("="), None);
|
||||
assert_eq!(de("1 +"), None);
|
||||
assert_eq!(de("(1 + 2"), None);
|
||||
assert_eq!(de("1 + 2)"), None);
|
||||
assert_eq!(de("2 3"), None);
|
||||
assert_eq!(de("1 : 0"), None);
|
||||
assert_eq!(de("0^-1"), None);
|
||||
assert_eq!(de("1 : x"), None);
|
||||
assert_eq!(de("x^y"), None);
|
||||
assert_eq!(de("x^0,5"), None);
|
||||
assert_eq!(de("2 ? 3"), None);
|
||||
assert_eq!(de("1,2,3"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_and_limits() {
|
||||
assert_eq!(de("10^100"), None);
|
||||
assert_eq!(de("2^100000"), None);
|
||||
assert_eq!(de("(x + 1)^33"), None);
|
||||
assert_eq!(de("(a+b+c+d+e)^8"), None);
|
||||
assert_eq!(de("99999999999999999999999999999999999999999999 + 1"), None);
|
||||
assert_eq!(de("2^64"), Some("18446744073709551616".into()));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user