Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/kasper9n/cpc. Text calculator with support for units and conversion cpc.kasper.space
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530//! calculation + conversion//!//! cpc parses and evaluates strings of math, with support for units and conversion. 128-bit decimal floating points are used for high accuracy.//!//! cpc lets you mix units, so for example 1 km - 1m results in Number { value: 999, unit: Meter }.//!//! Check out the [list of supported units](units/enum.Unit.html)//!//! # Example usage//! ```rust//! use cpc::eval;//! use cpc::units::Unit;//!//! match eval("3m + 1cm", true, false) {//! Ok(answer) => {//! // answer: Number { value: 301, unit: Unit::Centimeter }//! println!("Evaluated value: {} {:?}", answer.value, answer.unit)//! },//! Err(e) => {//! println!("{e}")//! }//! }//! ```
use crate::units::{Unit, UnitType, primitive_unit, primitive_unit_strict, sort_units};use fastnum::{D128, dec128 as d};use serde::{Deserialize, Serialize};use std::{ fmt::{self, Debug, Display}, sync::{OnceLock, RwLock, RwLockReadGuard, RwLockWriteGuard},};use web_time::Instant;
/// Currency exchange ratespub mod currency;/// Turns an [`AstNode`](parser::AstNode) into a [`Number`]pub mod evaluator;/// Turns a string into [`Token`]spub mod lexer;mod lookup;/// Turns [`Token`]s into an [`AstNode`](parser::AstNode)pub mod parser;/// Units, and functions you can use with thempub mod units;
#[derive(Clone)]/// A number with a `Unit`.////// Example:/// ```rust/// use cpc::{eval,Number};/// use cpc::units::Unit;/// use fastnum::dec128;////// let x = Number::with_basic_unit(dec128!(100), Unit::Meter);/// ```pub struct Number { /// The value of the number pub value: D128, /// The unit and exponent 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 primitive_strict(&self) -> Vec<(Unit, isize)> { primitive_unit_strict(&self.unit) } pub fn contains_category(&self, category: UnitType) -> bool { self.unit.iter().any(|(u, _)| u.category() == category) } /// Always returns positive exponents, like `second^2` because we print /// `1 meter / second^2` rather than `1 meter * second^2` fn get_unit_string_abs_exponent(&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(" * "); } // only the last multiplication should be plural: `100 minute meters / volt hour` 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.abs().to_string()); } } for unit in units { if unit.1 >= 0 { continue; } match s.len() { 0 => s.push_str("/ "), _ => s.push_str(" / "), } s.push_str(unit.0.singular()); if unit.1.abs() >= 2 { s.push_str("^"); s.push_str(&unit.1.abs().to_string()); } } s } pub fn singular(&self) -> String { self.get_unit_string_abs_exponent(false) } pub fn plural(&self) -> String { self.get_unit_string_abs_exponent(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 formatted_value = add_thousands_separator(&value.to_string()); let output = match word.as_str() { "" => format!("{approx_str}{formatted_value}"), _ => format!("{approx_str}{formatted_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() }}
fn add_thousands_separator(s: &str) -> String { let (sign, s) = match s.strip_prefix('-') { Some(rest) => ("-", rest), None => ("", s), }; let (int_part, rest) = s.split_once('.').map_or((s, ""), |(i, f)| (i, f)); let len = int_part.len(); let mut result = sign.to_string(); for (i, c) in int_part.chars().enumerate() { if i > 0 && (len - i) % 3 == 0 { result.push(' '); } result.push(c); } if !rest.is_empty() { result.push('.'); result.push_str(rest); } result}
#[test]fn test_thousands_separator() { assert_eq!(add_thousands_separator("-100000000"), "-100 000 000"); assert_eq!(add_thousands_separator("1000"), "1 000"); assert_eq!(add_thousands_separator("100"), "100"); assert_eq!(add_thousands_separator("-1000"), "-1 000");}
#[derive(Clone, Debug, PartialEq)]/// Math operators like [`Multiply`](Operator::Multiply), parentheses, etc.pub enum Operator { Plus, Minus, Multiply, Divide, Modulo, Caret, LeftParen, // lexer only RightParen, // lexer only}
#[derive(Clone, Debug, PartialEq)]/// Unary operators like [`Percent`](UnaryOperator::Percent) and [`Factorial`](UnaryOperator::Factorial).pub enum UnaryOperator { Percent, Factorial,}
#[derive(Clone, Debug, PartialEq)]/// A Text operator like [`To`](TextOperator::To) or [`Of`](TextOperator::Of).pub enum TextOperator { To, Of, Per,}
#[derive(Clone, Debug, PartialEq)]/// A named number like [`Million`](NamedNumber::Million).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)]/// A constant like [`Pi`](Constant::Pi) or [`E`](Constant::E).pub enum Constant { Pi, E,}
#[derive(Clone, Debug, PartialEq)]/// Functions identifiers like [`Sqrt`](FunctionIdentifier::Sqrt), [`Sin`](FunctionIdentifier::Sin), [`Round`](FunctionIdentifier::Round), etc.pub enum FunctionIdentifier { Sqrt, Cbrt,
Log, Ln, Exp,
Round, Ceil, Floor, Abs,
Sin, Cos, Tan,}
#[derive(Clone, Debug, PartialEq)]/// A temporary enum used by the [`lexer`] to later determine what [`Token`] it is.////// For example, when a symbol like `%` is found, the lexer turns it into a/// the [`PercentChar`](LexerKeyword::PercentChar) variant/// and then later it checks the surrounding [`Token`]s and,/// dependingon them, turns it into a [`Percent`](UnaryOperator::Percent) or/// [`Modulo`](Operator::Modulo) [`Token`].pub enum LexerKeyword { PercentChar, In,}
#[derive(Clone, PartialEq)]/// A token like a [`Number`](Token::Number), [`Operator`](Token::Operator), [`Unit`](Token::Unit) etc.////// Strings can be divided up into these tokens by the [`lexer`], and then put into the [`parser`].pub enum Token { Operator(Operator), UnaryOperator(UnaryOperator), Number(D128), FunctionIdentifier(FunctionIdentifier), Constant(Constant), /// Used by the parser only Paren, /// Used by the lexer only LexerKeyword(LexerKeyword), TextOperator(TextOperator), NamedNumber(NamedNumber), /// The `-` symbol, specifically when used as `-5` and not `5-5`. Used by the parser only 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)) };}
static SETTINGS: OnceLock<RwLock<Settings>> = OnceLock::new();
#[derive(Serialize, Deserialize)]pub struct Settings { pub locale: String,}
impl Settings { pub fn region(&self) -> String { let region = self.locale.split('-').last().unwrap().to_string(); region } pub fn get() -> &'static RwLock<Settings> { SETTINGS.get_or_init(|| RwLock::new(Settings::default())) } pub fn read() -> RwLockReadGuard<'static, Settings> { Self::get().read().unwrap() } pub fn write() -> RwLockWriteGuard<'static, Settings> { Self::get().write().unwrap() } #[cfg(test)] pub fn reset_to_default() { *Self::write() = Settings::default(); }}impl Default for Settings { fn default() -> Self { #[cfg(test)] { Settings { locale: "nb-NO".to_string(), } } #[cfg(not(test))] { #[cfg(target_arch = "wasm32")] { Settings { locale: "".to_string(), } } #[cfg(not(target_arch = "wasm32"))] Settings { locale: sys_locale::get_locale().unwrap_or("".to_string()), } } }}
#[cfg(target_arch = "wasm32")]#[wasm_bindgen]pub fn set_settings(value: JsValue) -> Result<(), JsValue> { let settings: Settings = serde_wasm_bindgen::from_value(value).map_err(|e| JsValue::from_str(&e.to_string()))?; *Settings::write() = settings; Ok(())}
pub fn get_region() -> String { Settings::read().region()}
/// Evaluates a string into a resulting [`Number`].////// Example:/// ```rust/// use cpc::eval;/// use cpc::units::Unit;////// match eval("3m + 1cm", true, false) {/// Ok(answer) => {/// // answer: Number { value: 301, unit: Unit::Centimeter }/// println!("Evaluated value: {} {:?}", answer.value, answer.unit)/// },/// Err(e) => {/// println!("{e}")/// }/// }/// ```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(mut 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(&mut 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))); }}