use crate::units::{Unit, UnitType, primitive_unit, sort_units};
use fastnum::{D128, dec128 as d};
use std::fmt::{self, Debug, Display};
use web_time::Instant;
pub mod currency;
pub mod evaluator;
pub mod lexer;
mod lookup;
pub mod parser;
pub mod units;
#[derive(Clone)]
pub struct Number {
pub value: D128,
pub unit: Vec<(Unit, isize)>,
}
impl Number {
pub fn new_unitless(value: D128) -> Number {
Number {
value,
unit: vec![],
}
}
pub fn with_basic_unit(value: D128, unit: Unit) -> Number {
Number {
value,
unit: vec![(unit, 1)],
}
}
pub fn with_unit(value: D128, unit: Vec<(Unit, isize)>) -> Number {
Number { value, unit }
}
pub fn has_unit(&self) -> bool {
!self.unit.is_empty()
}
pub fn is_unitless(&self) -> bool {
self.unit.is_empty()
}
pub fn get_simplified_value(&self) -> D128 {
self.value.reduce()
}
pub fn primitive_unit(&self) -> Vec<(Unit, isize)> {
primitive_unit(&self.unit)
}
pub fn contains_category(&self, category: UnitType) -> bool {
self.unit.iter().any(|(u, _)| u.category() == category)
}
fn get_unit_string(&self, plural: bool) -> String {
let mut s = String::new();
let mut units = self.unit.clone();
sort_units(&mut units);
let mut positives = units.iter().filter(|u| u.1 > 0).peekable();
while let Some(unit) = positives.next() {
if unit.1 <= 0 {
continue;
}
if s.len() != 0 {
s.push_str(" * ");
}
let is_last = positives.peek().is_none();
match is_last && plural {
false => s.push_str(unit.0.singular()),
true => s.push_str(unit.0.plural()),
};
if unit.1.abs() >= 2 {
s.push_str("^");
s.push_str(&unit.1.to_string());
}
}
for unit in units {
if unit.1 >= 0 {
continue;
}
s.push_str(" / ");
s.push_str(unit.0.singular());
if unit.1.abs() >= 2 {
s.push_str("^");
s.push_str(&unit.1.to_string());
}
}
s
}
pub fn singular(&self) -> String {
self.get_unit_string(false)
}
pub fn plural(&self) -> String {
self.get_unit_string(true)
}
}
impl Display for Number {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let value = self.get_simplified_value();
let word = match self.value == d!(1) {
true => self.singular(),
false => self.plural(),
};
let approx_str = match value.is_op_inexact() {
true => "≈ ",
false => "",
};
let output = match word.as_str() {
"" => format!("{approx_str}{value}"),
_ => format!("{approx_str}{value} {word}"),
};
write!(f, "{output}")
}
}
impl Debug for Number {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let unit_strings: Vec<_> = self
.unit
.iter()
.map(|u| format!("{:?}^{}", u.0, u.1))
.collect();
write!(
f,
"Number({} {})",
self.get_simplified_value(),
unit_strings.join(" ")
)
}
}
impl PartialEq for Number {
fn eq(&self, other: &Self) -> bool {
self.value == other.value && self.primitive_unit() == other.primitive_unit()
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Operator {
Plus,
Minus,
Multiply,
Divide,
Modulo,
Caret,
LeftParen, RightParen, }
#[derive(Clone, Debug, PartialEq)]
pub enum UnaryOperator {
Percent,
Factorial,
}
#[derive(Clone, Debug, PartialEq)]
pub enum TextOperator {
To,
Of,
Per,
}
#[derive(Clone, Debug, PartialEq)]
pub enum NamedNumber {
Hundred,
Thousand,
Million,
Billion,
Trillion,
Quadrillion,
Quintillion,
Sextillion,
Septillion,
Octillion,
Nonillion,
Decillion,
Undecillion,
Duodecillion,
Tredecillion,
Quattuordecillion,
Quindecillion,
Sexdecillion,
Septendecillion,
Octodecillion,
Novemdecillion,
Vigintillion,
Centillion,
Googol,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Constant {
Pi,
E,
}
#[derive(Clone, Debug, PartialEq)]
pub enum FunctionIdentifier {
Sqrt,
Cbrt,
Log,
Ln,
Exp,
Round,
Ceil,
Floor,
Abs,
Sin,
Cos,
Tan,
}
#[derive(Clone, Debug, PartialEq)]
pub enum LexerKeyword {
PercentChar,
In,
DoubleQuotes,
Mercury,
Hg,
PoundForce,
Force,
Revolution,
}
#[derive(Clone, PartialEq)]
pub enum Token {
Operator(Operator),
UnaryOperator(UnaryOperator),
Number(D128),
FunctionIdentifier(FunctionIdentifier),
Constant(Constant),
Paren,
LexerKeyword(LexerKeyword),
TextOperator(TextOperator),
NamedNumber(NamedNumber),
Negative,
Unit(Vec<(Unit, isize)>),
}
impl Token {
fn unit(u: Unit) -> Token {
Token::Unit(vec![(u, 1)])
}
}
impl fmt::Debug for Token {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Token::Operator(op) => write!(f, "Operator({:?})", op),
Token::UnaryOperator(op) => write!(f, "UnaryOperator({:?})", op),
Token::Number(num) => write!(f, "Number({num})"),
Token::FunctionIdentifier(id) => write!(f, "FunctionIdentifier({:?})", id),
Token::Constant(c) => write!(f, "Constant({:?})", c),
Token::Paren => write!(f, "Paren"),
Token::LexerKeyword(op) => write!(f, "LexerKeyword({:?})", op),
Token::TextOperator(op) => write!(f, "TextOperator({:?})", op),
Token::NamedNumber(num) => write!(f, "NamedNumber({:?})", num),
Token::Negative => write!(f, "Negative"),
Token::Unit(u) => write!(f, "Unit({:?})", u),
}
}
}
#[macro_export]
macro_rules! numtok {
( $num:literal ) => {
Token::Number(fastnum::dec128!($num))
};
}
pub fn eval(input: &str, allow_trailing_operators: bool, verbose: bool) -> Result<Number, String> {
let lex_start = Instant::now();
match lexer::lex(input, allow_trailing_operators) {
Ok(tokens) => {
let lex_time = Instant::now().duration_since(lex_start).as_nanos() as f32;
if verbose {
println!("Lexed TokenVector: {:?}", tokens);
}
let parse_start = Instant::now();
match parser::parse(&tokens) {
Ok(ast) => {
let parse_time = Instant::now().duration_since(parse_start).as_nanos() as f32;
if verbose {
println!("Parsed AstNode: {:#?}", ast);
}
let eval_start = Instant::now();
match evaluator::evaluate(&ast) {
Ok(answer) => {
let eval_time =
Instant::now().duration_since(eval_start).as_nanos() as f32;
if verbose {
println!("Evaluated value: {} {:?}", answer.value, answer.unit);
println!("\u{23f1} {:.3}ms lexing", lex_time / 1000.0 / 1000.0);
println!("\u{23f1} {:.3}ms parsing", parse_time / 1000.0 / 1000.0);
println!(
"\u{23f1} {:.3}ms evaluation",
eval_time / 1000.0 / 1000.0
);
}
Ok(answer)
}
Err(e) => Err(format!("Eval error: {}", e)),
}
}
Err(e) => Err(format!("Parsing error: {}", e)),
}
}
Err(e) => Err(format!("Lexing error: {}", e)),
}
}
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn wasm_eval(expression: &str) -> String {
console_error_panic_hook::set_once();
let result = eval(expression, true, false);
match result {
Ok(result) => result.to_string(),
Err(e) => format!("Error: {e}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_eval(input: &str) -> Number {
eval(input, true, false).unwrap()
}
#[test]
fn test_simplify() {
assert_eq!(&default_eval("sin(pi)").to_string(), "0");
assert_eq!(&default_eval("0.2/0.01").to_string(), "20");
}
#[test]
fn test_evaluations() {
assert_eq!(default_eval("-2(-3)"), Number::new_unitless(d!(6)));
assert_eq!(default_eval("-2(3)"), Number::new_unitless(d!(-6)));
assert_eq!(default_eval("(3)-2"), Number::new_unitless(d!(1)));
assert_eq!(
default_eval("-1km to m"),
Number::with_basic_unit(d!(-1000), Unit::Meter)
);
assert_eq!(default_eval("2*-3*0.5"), Number::new_unitless(d!(-3)));
assert_eq!(default_eval("-3^2"), Number::new_unitless(d!(-9)));
assert_eq!(default_eval("-1+2"), Number::new_unitless(d!(1)));
}
}