diff --git a/libraries/math-parser/src/ast.rs b/libraries/math-parser/src/ast.rs index 3b3c928989..97076312f8 100644 --- a/libraries/math-parser/src/ast.rs +++ b/libraries/math-parser/src/ast.rs @@ -38,6 +38,8 @@ pub enum UnaryOp { Neg, Fac, Not, + /// The magnitude bars `|x|`: absolute value on the reals, extending to the Euclidean magnitude. + Magnitude, } #[derive(Debug, PartialEq)] diff --git a/libraries/math-parser/src/constants.rs b/libraries/math-parser/src/constants.rs index 891fcb95f2..9ddb5ec59c 100644 --- a/libraries/math-parser/src/constants.rs +++ b/libraries/math-parser/src/constants.rs @@ -1,6 +1,6 @@ use crate::value::{Complex, Number, Value}; use num_complex::ComplexFloat; -use std::f64::consts::{LN_2, PI}; +use std::f64::consts::LN_2; pub type BuiltinFunction = fn(&[Value]) -> Option; @@ -223,9 +223,10 @@ pub fn builtin_function(name: &str) -> Option { }, // Mapping Functions + // Each part's absolute value, where `|x|` is instead the one magnitude of the whole value "abs" => |values| match values { [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(real.abs()))), - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.abs()))), + [Value::Number(Number::Complex(complex))] => Some(Value::from(Complex::new(complex.re.abs(), complex.im.abs()))), _ => None, }, @@ -442,34 +443,13 @@ pub fn builtin_function(name: &str) -> Option { _ => None, }, - // Complex Number Functions - "real" => |values| match values { - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.re))), - [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(*real))), - _ => None, - }, - - "imag" => |values| match values { - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.im))), - [Value::Number(Number::Real(_))] => Some(Value::Number(Number::Real(0.))), - _ => None, - }, - + // The conjugate negates the imaginary part "conj" => |values| match values { [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Complex(complex.conj()))), [Value::Number(Number::Real(real))] => Some(Value::Number(Number::Real(*real))), _ => None, }, - "arg" => |values| match values { - [Value::Number(Number::Complex(complex))] => Some(Value::Number(Number::Real(complex.arg()))), - [Value::Number(Number::Real(real))] => { - let angle = if *real >= 0. { 0. } else { PI }; - Some(Value::Number(Number::Real(angle))) - } - _ => None, - }, - _ => return None, }) } diff --git a/libraries/math-parser/src/lexer.rs b/libraries/math-parser/src/lexer.rs index ea53c1c0b6..4d8d1c60ae 100644 --- a/libraries/math-parser/src/lexer.rs +++ b/libraries/math-parser/src/lexer.rs @@ -16,6 +16,8 @@ pub enum Token<'src> { OrOr, Bang, Not, + BarOpen, + BarClose, LParen, RParen, @@ -50,6 +52,7 @@ impl<'src> fmt::Display for Token<'src> { Token::OrOr => f.write_str("||"), Token::Bang => f.write_str("!"), Token::Not => f.write_str("¬"), + Token::BarOpen | Token::BarClose => f.write_str("|"), Token::LParen => f.write_str("("), Token::RParen => f.write_str(")"), @@ -141,14 +144,82 @@ impl fmt::Display for Constant { } } +/// How a `|` reads at its position: opening or closing a magnitude, or, as the first of a `||` pair, the Or operator. +#[derive(Clone, Copy, Debug, PartialEq)] +enum Bar { + Open, + Close, + Or, +} + +/// Whether a character ends an operand: a name, a number, a closing parenthesis, or the `∞` literal. +fn ends_operand(c: char) -> bool { + c.is_alphanumeric() || unicode_ident::is_xid_continue(c) || matches!(c, '.' | ')' | '∞') +} + +/// Reads every `|` in the source up front, since each depends on what precedes it: a bar opens a magnitude where an operand +/// is expected and closes one after an operand. A `||` likewise opens or closes two, but is Or after an operand unless two are +/// open, or with nothing after it. So `|a||b|` is the magnitude of `a` or `b`, `||a|-b|` and `|a*|b||` nest, and `|a|b||` is rejected. +fn classify_bars(input: &str) -> Vec<(usize, Bar)> { + let mut bars = Vec::new(); + let mut depth = 0_usize; + let mut after_operand = false; + let mut chars = input.char_indices().peekable(); + + while let Some((position, c)) = chars.next() { + match c { + '|' if chars.next_if(|(_, next)| *next == '|').is_some() => { + let at_end = chars.clone().all(|(_, c)| c.is_whitespace()); + if !after_operand && !at_end { + bars.push((position, Bar::Open)); + bars.push((position + 1, Bar::Open)); + depth += 2; + } else if depth >= 2 { + bars.push((position, Bar::Close)); + bars.push((position + 1, Bar::Close)); + depth -= 2; + } else { + bars.push((position, Bar::Or)); + after_operand = false; + } + } + '|' if after_operand && depth > 0 => { + bars.push((position, Bar::Close)); + depth -= 1; + after_operand = true; + } + '|' => { + bars.push((position, Bar::Open)); + depth += 1; + after_operand = false; + } + // Whitespace changes nothing, and neither does `!`, a postfix factorial after an operand or a prefix not before one + c if c.is_whitespace() || c == '!' => {} + c => after_operand = ends_operand(c), + } + } + + bars +} + pub struct Lexer<'a> { input: &'a str, pos: usize, + bars: Vec<(usize, Bar)>, } impl<'a> Lexer<'a> { pub fn new(input: &'a str) -> Self { - Self { input, pos: 0 } + Self { + input, + pos: 0, + bars: classify_bars(input), + } + } + + /// The reading of the `|` at the given byte position. + fn bar_at(&self, position: usize) -> Option { + self.bars.binary_search_by_key(&position, |(bar_position, _)| *bar_position).ok().map(|index| self.bars[index].1) } fn peek(&self) -> Option { @@ -209,7 +280,7 @@ impl<'a> Lexer<'a> { preceding .chars() .next_back() - .is_some_and(|c| c.is_alphanumeric() || unicode_ident::is_xid_continue(c) || c == '.' || c == ')' || c == '∞') + .is_some_and(|c| ends_operand(c) || (c == '|' && self.bar_at(preceding.len() - 1) == Some(Bar::Close))) } fn lex_number(&mut self) -> Option { @@ -288,14 +359,15 @@ impl<'a> Lexer<'a> { Error } } - '|' => { - if self.peek() == Some('|') { + '|' => match self.bar_at(start) { + Some(Bar::Or) => { self.bump(); OrOr - } else { - Error } - } + Some(Bar::Open) => BarOpen, + Some(Bar::Close) => BarClose, + None => Error, + }, '(' => LParen, ')' => RParen, diff --git a/libraries/math-parser/src/lib.rs b/libraries/math-parser/src/lib.rs index 612bb51e53..d44dd25dc9 100644 --- a/libraries/math-parser/src/lib.rs +++ b/libraries/math-parser/src/lib.rs @@ -51,7 +51,59 @@ mod tests { #[test] fn juxtaposed_numbers_fail_to_parse() { // Adjacent number literals like digit-grouped `10 000` must not silently multiply, and a `.`-led literal after any operand needs its leading zero - for input in ["2 3", "10 000", "1 .5", "2 3 + 1", "1e5 3", "2. 3", "sqrt(4).5", "sqrt(4) .5", "pi.5", "5!.5"] { + for input in ["2 3", "10 000", "1 .5", "2 3 + 1", "1e5 3", "2. 3", "sqrt(4).5", "sqrt(4) .5", "pi.5", "5!.5", "|2| .5"] { + assert!(evaluate(input).is_err(), "expected `{input}` to be a parse error"); + } + } + + #[test] + fn lone_double_bar_is_or() { + // Two opening bars with nothing after them could never close, so a lone `||` is the Or operator itself + for source in ["||", " || ", "\t||\n"] { + assert_eq!(lexer::Lexer::new(source).collect::>(), [lexer::Token::OrOr], "`{source}`"); + } + } + + #[test] + fn unbalanced_magnitude_bars_fail_to_parse() { + // Each leaves a magnitude unclosed or empty, or an Or missing an operand, like `|1|2||`, which closes after the 1 and leaves `2 ||` dangling + for input in [ + "|1|2||", + "|2", + "|1||2||", + "2|", + "|", + "||", + "|||", + "||||", + "| |", + "|1|2|", + "1 |", + "|1| |", + "||1|", + "|1||", + "|1 || |", + "|| 1", + "1 ||", + "|1|||0|", + "|0|||1|", + "|2*||-3||", + "(|1)|", + "|(1|)", + "| 1 | | 2", + "|2|-3|", + "0 || || 1", + "|| || 1", + "|1| ||", + "||1||2||", + "|1 ∨|", + "∨ 1", + "|∨|", + "|1|||", + "1 ||| 0", + "||| |", + "|0 || |1|", + ] { assert!(evaluate(input).is_err(), "expected `{input}` to be a parse error"); } } @@ -342,6 +394,7 @@ mod tests { implicit_multiplication_trailing_number_after_name_run: "2pi 3" => 6. * std::f64::consts::PI, implicit_multiplication_trailing_number_after_factorial: "3! 2" => 12., implicit_multiplication_trailing_number_after_infinity: "∞ 2" => f64::INFINITY, + implicit_multiplication_trailing_number_after_magnitude: "|2| 3" => 6., // Factorial (postfix !) factorial_simple: "5!" => 120., @@ -569,9 +622,125 @@ mod tests { climb_sqrt: "sqrt(-4)" => Complex::new(0., 2.), climb_ln: "ln(-1)" => Complex::new(0., std::f64::consts::PI), climb_log_base: "log(-1, 10)" => Complex::new(0., std::f64::consts::PI / std::f64::consts::LN_10), - climb_asin: "abs(sin(asin(2)))" => 2., - climb_acosh: "abs(cosh(acosh(0.5)))" => 0.5, - climb_power: "abs((-8)^(1/3))" => 2., + climb_asin: "|sin(asin(2))|" => 2., + climb_acosh: "|cosh(acosh(0.5))|" => 0.5, + climb_power: "|(-8)^(1/3)|" => 2., + + // Magnitude bars: absolute value on the reals and the Euclidean magnitude beyond, with `||` reading as Or unless two magnitudes are open + magnitude_real: "|-3|" => 3., + mapping_abs: "abs(-3)" => 3., + mapping_abs_per_part: "abs(-3 - 4i)" => Complex::new(3., 4.), + magnitude_complex: "|3 + 4i|" => 5., + magnitude_implicit_multiplication: "2|-3|" => 6., + magnitude_juxtaposed: "|-2| |-3|" => 6., + magnitude_then_subtraction: "|-2|-3" => -1., + magnitude_nested: "|2*|-3||" => 6., + magnitude_nested_open: "||-3| - 5|" => 2., + magnitude_nested_both_ends: "|1 + ||-2| - 3||" => 2., + magnitude_double_bars: "||-3||" => 3., + magnitude_or_inside: "|1||0|" => 1., + magnitude_factorial: "|-3|!" => 6., + + // Magnitude bars around single values + magnitude_zero: "|0|" => 0., + magnitude_negative_zero: "|-0|" => 0., + magnitude_positive: "|5|" => 5., + magnitude_inner_spaces: "| - 2 |" => 2., + magnitude_inner_tab_and_newline: "|\t-2\n|" => 2., + magnitude_fraction: "|-2.5|" => 2.5, + magnitude_leading_dot_fraction: "|-.5|" => 0.5, + magnitude_scientific: "|-1e-3|" => 0.001, + magnitude_infinity_word: "|-inf|" => f64::INFINITY, + magnitude_infinity_symbol: "|-∞|" => f64::INFINITY, + magnitude_imaginary_unit: "|-i|" => 1., + magnitude_complex_negative_parts: "|-3 - 4i|" => 5., + magnitude_of_parenthesized: "|(-3)|" => 3., + magnitude_in_parentheses: "(|-3|)" => 3., + magnitude_of_difference: "|2 - 5|" => 3., + + // Magnitude bars among operators, where each bar after an operand closes and each bar after an operator opens + magnitude_negated: "-|-3|" => -3., + magnitude_squared: "|-3|^2" => 9., + magnitude_as_exponent: "2^|-3|" => 8., + magnitude_power_of_magnitudes: "|-2|^|-3|" => 8., + magnitude_product_spaced: "|-2| * |-3|" => 6., + magnitude_product_unspaced: "|-2|*|-3|" => 6., + magnitude_sum_spaced: "|-2| + |-3|" => 5., + magnitude_sum_unspaced: "|-2|+|-3|" => 5., + magnitude_difference_spaced: "|-2| - |-3|" => -1., + magnitude_difference_unspaced: "|-2|-|-3|" => -1., + magnitude_quotient: "|-2| / |-4|" => 0.5, + magnitude_modulo: "|-7| % |-4|" => 3., + magnitude_factorial_inside: "|3!|" => 6., + magnitude_factorial_then_sum: "|-3|! + 1" => 7., + magnitude_less_than_spaced: "|-3| < |-5|" => 1., + magnitude_less_than_unspaced: "|-3|<|-5|" => 1., + magnitude_equality: "|-3| == 3" => 1., + + // Magnitude bars juxtaposed with other operands multiply + magnitude_then_number: "|-2|3" => 6., + magnitude_after_number: "3|-2|" => 6., + magnitude_then_parenthesized: "|-2|(3)" => 6., + magnitude_after_parenthesized: "(3)|-2|" => 6., + magnitude_then_constant: "|-1|pi" => std::f64::consts::PI, + magnitude_after_constant: "pi|-1|" => std::f64::consts::PI, + magnitude_three_juxtaposed: "|2| |3| |4|" => 24., + magnitude_juxtaposed_then_sum: "|-2| |-3| + 1" => 7., + magnitude_number_then_two_magnitudes: "2 |-3| |-4|" => 24., + magnitude_difference_then_implicit_product: "|-2|-3|-4|" => -10., + + // Magnitude bars around function calls and function arguments + magnitude_as_argument: "sqrt(|-16|)" => 4., + magnitude_of_complex_result: "|sqrt(-16)|" => 4., + magnitude_as_both_arguments: "max(|-3|, |2|)" => 3., + magnitude_as_two_argument_log: "log(|-100|, |-10|)" => 2., + magnitude_of_call: "|max(-3, -5)|" => 3., + magnitude_of_call_with_magnitude_argument: "|log(|-100|)|" => 2., + magnitude_of_conditional: "|if(1, -2, 3)|" => 2., + magnitude_in_conditional: "if(|-1| == 1, |-7|, 0)" => 7., + + // Nested magnitude bars, spaced and unspaced, including runs of three bars + magnitude_nested_closing_spaced: "|1 - |2 - 5| |" => 2., + magnitude_nested_closing_unspaced: "|1 - |2 - 5||" => 2., + magnitude_nested_both_spaced: "| |-3| |" => 3., + magnitude_nested_negated: "|-|-3||" => 3., + magnitude_nested_negated_sum: "|-|-3| + 1|" => 2., + magnitude_nested_open_product: "||-3|*2|" => 6., + magnitude_nested_open_difference: "||-3| - |-5||" => 2., + magnitude_triple_nested: "|||-3|||" => 3., + magnitude_triple_nested_differences: "|1 - |1 - |1 - 5|||" => 2., + magnitude_triple_open_run: "|||1|-2|-3|" => 2., + magnitude_product_of_nested: "|2*||-3|||" => 6., + + // Or among magnitude bars, where `||` after an operand is Or unless two magnitudes are open + or_spaced: "1 || 0" => 1., + or_unspaced: "0||1" => 1., + or_both_false: "0 || 0" => 0., + or_typeset: "1 ∨ 0" => 1., + or_of_parenthesized: "(1)||(0)" => 1., + or_of_parenthesized_magnitudes: "(|1|)||(|0|)" => 1., + or_of_magnitudes_spaced: "|1| || |0|" => 1., + or_of_magnitudes_typeset: "|1|∨|0|" => 1., + or_then_magnitude_spaced: "0 || |-1|" => 1., + or_then_magnitude_unspaced: "0|||-1|" => 1., + or_then_magnitude_bar_run_spaced: "0 ||| -1|" => 1., + or_magnitude_then_bar_run: "|0| ||| -1 |" => 1., + or_of_magnitude_first: "|-1| || 0" => 1., + or_of_double_bars: "||1|| || ||0||" => 1., + or_of_double_bars_unspaced: "||1||||||0||" => 1., + or_inside_both_false: "|0||0|" => 0., + or_inside_then_product: "|0 || 1| * 5" => 5., + or_inside_after_product: "2 * |0 || 1|" => 2., + or_inside_parenthesized: "|(1 || 0)|" => 1., + or_parenthesized_plus_magnitude: "(1 || 0) + |-2|" => 3., + or_inside_after_and: "1 && |0 || 1|" => 1., + or_after_and_inside: "|1 && 0| || 1" => 1., + or_after_comparison_of_magnitude: "|3 - 5| > 1 || 0" => 1., + or_as_condition: "if(0 || 1, |-2|, |-3|)" => 2., + not_of_magnitude: "!|0|" => 1., + not_of_magnitude_then_or: "!|0| || 0" => 1., + not_inside_magnitude: "|!0|" => 1., + not_inside_magnitude_with_or: "| !0 || 0 |" => 1., // Correctly rounded literals via std parsing seventeen_digit_literal: "999999999999999999" => 1e18, diff --git a/libraries/math-parser/src/parser.rs b/libraries/math-parser/src/parser.rs index f964246d6f..8488e3bdf3 100644 --- a/libraries/math-parser/src/parser.rs +++ b/libraries/math-parser/src/parser.rs @@ -67,8 +67,12 @@ where }); let parens = expr.clone().delimited_by(just(Token::LParen), just(Token::RParen)); + let magnitude = expr.clone().delimited_by(just(Token::BarOpen), just(Token::BarClose)).map(|expr| Node::UnaryOp { + op: UnaryOp::Magnitude, + expr: Box::new(expr), + }); - let atom = choice((constant, if_expr, call_or_var, parens)).labelled("atom"); + let atom = choice((constant, if_expr, call_or_var, parens, magnitude)).labelled("atom"); let add_op = choice((just(Token::Plus).to(BinaryOp::Add), just(Token::Minus).to(BinaryOp::Sub))); let mul_op = choice((just(Token::Star).to(BinaryOp::Mul), just(Token::Slash).to(BinaryOp::Div), just(Token::Modulo).to(BinaryOp::Modulo))); diff --git a/libraries/math-parser/src/value.rs b/libraries/math-parser/src/value.rs index ddec2f5880..f5ef02d328 100644 --- a/libraries/math-parser/src/value.rs +++ b/libraries/math-parser/src/value.rs @@ -219,6 +219,10 @@ impl Number { Number::Complex(complex) => Number::Complex(-complex), }), UnaryOp::Not => Some(Number::Real(!self.as_bool() as u8 as f64)), + UnaryOp::Magnitude => Some(Number::Real(match self { + Number::Real(real) => real.abs(), + Number::Complex(complex) => complex.norm(), + })), UnaryOp::Fac => { // A factorial is defined for whole numbers at or above zero let Number::Real(real) = self else { return None };