diff --git a/Cargo.toml b/Cargo.toml index a6c52cf..5793b00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,7 @@ thiserror = "2.0.18" version = "1.30" default-features = false features = ["std", "float"] + +[profile.release] +lto = true +codegen-units = 1 diff --git a/flake.nix b/flake.nix index 62c5763..f7862ee 100644 --- a/flake.nix +++ b/flake.nix @@ -12,14 +12,14 @@ let pkgs = import inputs.nixpkgs { inherit system; }; - cargoToml = fromTOML (builtins.readFile ./Cargo.toml); + src = ./.; + cargoToml = fromTOML (builtins.readFile "${src}/Cargo.toml"); name = cargoToml.package.name; crate = pkgs.rustPlatform.buildRustPackage { - inherit name; - src = ./.; + inherit name src; - cargoLock.lockFile = ./Cargo.lock; + cargoLock.lockFile = "${src}/Cargo.lock"; nativeBuildInputs = with pkgs; [ m4 diff --git a/src/formatting.rs b/src/formatting.rs new file mode 100644 index 0000000..e20e378 --- /dev/null +++ b/src/formatting.rs @@ -0,0 +1,100 @@ +use std::str::FromStr; + +use rug::Float; + +use crate::nodes::{Error, Nodes}; + +pub fn pretty_eval(mut input: String) -> Result { + // no need to remove the characters here, + // as we ignore the them in all other logic already + let should_add_back_alignment = input.contains('&'); + let should_add_back_backslash = input.contains('\\'); + let leading_spaces = input.chars().take_while(|c| *c == ' ').count(); + + let should_add_back_equals = input + .find('=') + .inspect(|i| { + input.remove(*i); + }) + .is_some(); + + let result = Nodes::from_str(&input)?.evaluate()?; + let significant_digits = get_significant_digits(&result); + + Ok(format!( + "{:leading_spaces$}{}{result:.significant_digits$}{}", + ' ', + if should_add_back_equals && should_add_back_alignment { + "& = " + } else if should_add_back_equals { + "= " + } else if should_add_back_alignment { + "& " + } else { + "" + }, + if should_add_back_backslash { " \\" } else { "" } + )) +} +#[test] +fn test_character_readdition() { + assert_eq!(pretty_eval(" = 11".into()).unwrap(), " = 11"); + assert_eq!(pretty_eval(" &= 11".into()).unwrap(), " & = 11"); + assert_eq!(pretty_eval(" & 11".into()).unwrap(), " & 11"); + assert_eq!(pretty_eval(" & 11 \\".into()).unwrap(), " & 11 \\"); +} + +pub fn get_significant_digits(n: &Float) -> usize { + let raw_string: String = format!("{n}").chars().filter(|c| *c != '.').collect(); + + let mut consecutive_zeros = 0; + let mut consecutive_nines = 0; + for (i, c) in raw_string.char_indices() { + if c == '0' { + consecutive_zeros += 1; + } else { + consecutive_zeros = 0; + } + + if c == '9' { + consecutive_nines += 1; + } else { + consecutive_nines = 0; + } + + if consecutive_zeros >= 10 { + return i + 1 - consecutive_zeros; + } else if consecutive_nines >= 10 { + return i + 1 - consecutive_nines; + } + + if !raw_string[i..raw_string.len()].contains(|c| matches!(c, '1'..='9')) { + return i; + } + } + + // all digits are significant + raw_string.len() +} + +#[cfg(test)] +fn eval(s: &str) -> Float { + crate::nodes::eval(s).unwrap() +} + +#[test] +fn test_get_significant_digits() { + let many_zeros = eval("14.00643 + 1.00784 * 4 + 35.446 + 15.9994 * 4"); + assert_eq!(many_zeros.to_f64(), 117.48139); + let many_nines = eval("12.0096 + 15.9994 * 2"); + assert_eq!(many_nines.to_f64(), 44.0084); + let zero_nine_zero = eval("2+12321(5+123)"); + assert_eq!(zero_nine_zero.to_f64(), 1577090.0); + let test_4 = eval("12.0096 + 1.00784 * 3 + 12.0096 + 1.00784 * 2 + 15.9994 + 1.00784"); + assert_eq!(test_4.to_f64(), 46.06564); + + assert_eq!(get_significant_digits(&many_zeros), 8); + assert_eq!(get_significant_digits(&many_nines), 6); + assert_eq!(get_significant_digits(&zero_nine_zero), 6); + assert_eq!(get_significant_digits(&test_4), 7); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..71e5c48 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod formatting; +pub mod nodes; + +pub use nodes::eval; diff --git a/src/main.rs b/src/main.rs index 3e9206b..32f04a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,13 @@ use std::{ env, io::{self, Read}, - str::FromStr, + process::ExitCode, }; -use rug::Float; - +mod formatting; mod nodes; -fn main() { +fn main() -> ExitCode { let input = match env::args().skip(1).rev().reduce(|x, a| a + &x) { Some(s) => s, None => { @@ -18,100 +17,14 @@ fn main() { } }; - println!("{}", parse_and_calc_humanreadable(input)); -} + match formatting::pretty_eval(input) { + Ok(s) => println!("{s}"), -pub fn parse_and_calc_humanreadable(mut input: String) -> String { - let should_add_back_equals = if let Some((i, _)) = input - .chars() - .enumerate() - .filter(|(_, c)| !matches!(c, ' ' | '&' | '\n')) - .find(|(_, c)| *c == '=') - { - input.remove(i); - true - } else { - false - }; - - // no need to remove the character here, - // as we ignore the ampersand in all other logic already - let should_add_back_alignment = input.contains('&'); - - let result = parse_and_calc(&input); - - format!( - "{0}{result:.1$}", - if should_add_back_equals && should_add_back_alignment { - "& = " - } else if should_add_back_equals { - "= " - } else if should_add_back_alignment { - "& " - } else { - "" - }, - get_significant_digits(&result), - ) -} -#[test] -fn test_equals_readdition() { - assert_eq!(parse_and_calc_humanreadable(" = 11".into()), "= 11"); - assert_eq!(parse_and_calc_humanreadable(" &= 11".into()), "& = 11"); - assert_eq!(parse_and_calc_humanreadable(" & 11".into()), "& 11"); -} - -pub fn parse_and_calc(s: &str) -> Float { - nodes::Nodes::from_str(s).unwrap().evaluate() -} - -pub fn get_significant_digits(n: &Float) -> usize { - let raw_string: String = format!("{n}").chars().filter(|c| *c != '.').collect(); - - let mut consecutive_zeros = 0; - let mut consecutive_nines = 0; - for (i, c) in raw_string.char_indices() { - if c == '0' { - consecutive_zeros += 1; - } else { - consecutive_zeros = 0; - } - - if c == '9' { - consecutive_nines += 1; - } else { - consecutive_nines = 0; - } - - if consecutive_zeros >= 10 { - return i + 1 - consecutive_zeros; - } else if consecutive_nines >= 10 { - return i + 1 - consecutive_nines; - } - - if !raw_string[i..raw_string.len()].contains(|c| matches!(c, '1'..='9')) { - return i; + Err(e) => { + eprintln!("{e}"); + return ExitCode::FAILURE; } } - // all digits are significant - raw_string.len() -} - -#[test] -fn test_get_significant_digits() { - let many_zeros = parse_and_calc("14.00643 + 1.00784 * 4 + 35.446 + 15.9994 * 4"); - assert_eq!(many_zeros.to_f64(), 117.48139); - let many_nines = parse_and_calc("12.0096 + 15.9994 * 2"); - assert_eq!(many_nines.to_f64(), 44.0084); - let zero_nine_zero = parse_and_calc("2+12321(5+123)"); - assert_eq!(zero_nine_zero.to_f64(), 1577090.0); - let test_4 = - parse_and_calc("12.0096 + 1.00784 * 3 + 12.0096 + 1.00784 * 2 + 15.9994 + 1.00784"); - assert_eq!(test_4.to_f64(), 46.06564); - - assert_eq!(get_significant_digits(&many_zeros), 8); - assert_eq!(get_significant_digits(&many_nines), 6); - assert_eq!(get_significant_digits(&zero_nine_zero), 6); - assert_eq!(get_significant_digits(&test_4), 7); + ExitCode::SUCCESS } diff --git a/src/nodes/evaluate.rs b/src/nodes/eval.rs similarity index 68% rename from src/nodes/evaluate.rs rename to src/nodes/eval.rs index c9984bd..27d6907 100644 --- a/src/nodes/evaluate.rs +++ b/src/nodes/eval.rs @@ -5,25 +5,35 @@ use rug::{ use crate::nodes::{Function, Node, Nodes, Operator}; +#[derive(Clone, Debug, thiserror::Error)] +pub enum Error { + #[error("invalid operand: {0}")] + InvalidOperand(Node), + #[error("missing argument, got: {0}")] + MissingArgument(Node), + #[error("collapse failed, left with: {0}")] + CollapseFailed(Nodes), +} + impl Nodes { /// Consume the `Nodes`, collapsing them down to a single number - pub fn evaluate(mut self) -> Float { + pub fn evaluate(mut self) -> Result { let nodes = &mut self.0; while let Some(end) = nodes.iter().position(|n| *n == Node::ParenthesisEnd) { - collapse_toplevel(nodes, end); + collapse_toplevel(nodes, end)?; } let len = nodes.len(); - collapse_toplevel(nodes, len); + collapse_toplevel(nodes, len)?; match nodes.pop().unwrap() { - Node::Number(n) => n, - _ => panic!("failed to collapse nodes: {nodes:?}"), + Node::Number(n) => Ok(n), + _ => Err(Error::CollapseFailed(self)), } } } -fn collapse_toplevel(nodes: &mut Vec, mut end: usize) { +fn collapse_toplevel(nodes: &mut Vec, mut end: usize) -> Result<(), Error> { let start = nodes[0..end] .iter() .rposition(|n| *n == Node::ParenthesisStart) @@ -51,7 +61,7 @@ fn collapse_toplevel(nodes: &mut Vec, mut end: usize) { Function::Secant => n.sec_mut(), Function::CoSecant => n.csc_mut(), }, - n => panic!("function is missing an argument, got: {n:?}"), + n => return Err(Error::MissingArgument(n.clone())), }; nodes.remove(start + index); @@ -64,12 +74,12 @@ fn collapse_toplevel(nodes: &mut Vec, mut end: usize) { { let lhs = match nodes.remove(start + operator - 1) { Node::Number(n) => n, - n => panic!("operator has an invalid operand: {n:?}"), + n => return Err(Error::InvalidOperand(n)), }; end -= 1; match &mut nodes[start..end][operator] { Node::Number(n) => n.pow_from(lhs), - n => panic!("operator has an invalid operand: {n:?}"), + n => return Err(Error::InvalidOperand(n.clone())), }; nodes.remove(start + operator - 1); @@ -82,12 +92,12 @@ fn collapse_toplevel(nodes: &mut Vec, mut end: usize) { { let lhs = match nodes.remove(start + operator - 1) { Node::Number(n) => n, - n => panic!("operator has an invalid operand: {n:?}"), + n => return Err(Error::InvalidOperand(n)), }; end -= 1; match &mut nodes[start..end][operator] { Node::Number(n) => n.div_from(lhs), - n => panic!("operator has an invalid operand: {n:?}"), + n => return Err(Error::InvalidOperand(n.clone())), }; nodes.remove(start + operator - 1); @@ -100,30 +110,12 @@ fn collapse_toplevel(nodes: &mut Vec, mut end: usize) { { let lhs = match nodes.remove(start + operator - 1) { Node::Number(n) => n, - n => panic!("operator has an invalid operand: {n:?}"), + n => return Err(Error::InvalidOperand(n)), }; end -= 1; match &mut nodes[start..end][operator] { Node::Number(n) => n.mul_from(lhs), - n => panic!("operator has an invalid operand: {n:?}"), - }; - - nodes.remove(start + operator - 1); - end -= 1; - } - - while let Some(operator) = nodes[start..end] - .iter() - .position(|n| *n == Node::Operator(Operator::Plus)) - { - let lhs = match nodes.remove(start + operator - 1) { - Node::Number(n) => n, - n => panic!("operator has an invalid operand: {n:?}"), - }; - end -= 1; - match &mut nodes[start..end][operator] { - Node::Number(n) => n.add_from(lhs), - n => panic!("operator has an invalid operand: {n:?}"), + n => return Err(Error::InvalidOperand(n.clone())), }; nodes.remove(start + operator - 1); @@ -136,12 +128,30 @@ fn collapse_toplevel(nodes: &mut Vec, mut end: usize) { { let lhs = match nodes.remove(start + operator - 1) { Node::Number(n) => n, - n => panic!("operator has an invalid operand: {n:?}"), + n => return Err(Error::InvalidOperand(n)), }; end -= 1; match &mut nodes[start..end][operator] { Node::Number(n) => n.sub_from(lhs), - n => panic!("operator has an invalid operand: {n:?}"), + n => return Err(Error::InvalidOperand(n.clone())), + }; + + nodes.remove(start + operator - 1); + end -= 1; + } + + while let Some(operator) = nodes[start..end] + .iter() + .position(|n| *n == Node::Operator(Operator::Plus)) + { + let lhs = match nodes.remove(start + operator - 1) { + Node::Number(n) => n, + n => return Err(Error::InvalidOperand(n)), + }; + end -= 1; + match &mut nodes[start..end][operator] { + Node::Number(n) => n.add_from(lhs), + n => return Err(Error::InvalidOperand(n.clone())), }; nodes.remove(start + operator - 1); @@ -152,4 +162,27 @@ fn collapse_toplevel(nodes: &mut Vec, mut end: usize) { nodes.remove(end); nodes.remove(start); } + + Ok(()) +} + +#[cfg(test)] +use super::PREC; + +#[test] +fn test() { + assert_eq!( + Nodes(vec![ + Node::Function(Function::Sine), + Node::ParenthesisStart, + Node::Number(Float::with_val(PREC, 2)), + Node::ParenthesisEnd, + Node::Operator(Operator::Plus), + Node::Number(Float::with_val(PREC, 1)) + ]) + .evaluate() + .unwrap() + .to_f64(), + 1.9092974268256817, + ); } diff --git a/src/nodes/mod.rs b/src/nodes/mod.rs index f46ad3b..f435db4 100644 --- a/src/nodes/mod.rs +++ b/src/nodes/mod.rs @@ -2,13 +2,29 @@ //! as well as the logic to parse it from a string (`Node::from_str`), //! and the logic to evaluate it into a single number (`Node::evaluate`). +use std::fmt::{Debug, Display}; + use rug::Float; -mod evaluate; -mod from_str; +mod eval; +pub mod parse; #[cfg(test)] mod tests; +#[derive(Clone, Debug, thiserror::Error)] +pub enum Error { + #[error("{0}")] + Parsing(#[from] parse::Error), + #[error("{0}")] + Evaluation(#[from] eval::Error), +} + +/// helper function to parse to Nodes and immediately evaluate +pub fn eval(s: &str) -> Result { + use std::str::FromStr; + Ok(Nodes::from_str(s)?.evaluate()?) +} + pub const PREC: u32 = 512; #[derive(Debug, Default, Clone, PartialEq)] @@ -18,18 +34,28 @@ impl AsRef<[Node]> for Nodes { self.0.as_ref() } } +impl Display for Nodes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(&self.0, f) + } +} #[derive(Debug, Clone, PartialEq)] -enum Node { +pub enum Node { Function(Function), ParenthesisStart, ParenthesisEnd, Number(Float), Operator(Operator), } +impl Display for Node { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(&self, f) + } +} #[derive(Debug, Default, Copy, Clone, PartialEq)] -enum Operator { +pub enum Operator { Plus, Minus, #[default] @@ -39,7 +65,7 @@ enum Operator { } #[derive(Debug, Copy, Clone, PartialEq)] -enum Function { +pub enum Function { Sqrt, Sine, diff --git a/src/nodes/from_str.rs b/src/nodes/parse.rs similarity index 93% rename from src/nodes/from_str.rs rename to src/nodes/parse.rs index 14e4c9a..07f6d4c 100644 --- a/src/nodes/from_str.rs +++ b/src/nodes/parse.rs @@ -81,12 +81,12 @@ impl FromStr for Nodes { /// /// We ignore whitespace since it has no semantic significance in typst /// (aside from variables, which are not implemented) - /// and `&` because it is only used for visual alignment. + /// and `&`, `\` because they are only used for visual alignment. /// /// Newlines are also ignored because they're likely accidental; /// like in the case of `echo 1+1 | tylc`. fn is_not_ignored_char(c: &char) -> bool { - !matches!(c, ' ' | '&' | '\n') + !matches!(c, ' ' | '&' | '\n' | '\\') } let mut nodes = Vec::new(); @@ -188,3 +188,18 @@ impl FromStr for Nodes { Ok(Self(nodes)) } } + +#[test] +fn test() { + assert_eq!( + Nodes::from_str("sin(2) + 1").unwrap().as_ref(), + &[ + Node::Function(Function::Sine), + Node::ParenthesisStart, + Node::Number(Float::with_val(PREC, 2)), + Node::ParenthesisEnd, + Node::Operator(Operator::Plus), + Node::Number(Float::with_val(PREC, 1)) + ], + ) +} diff --git a/src/nodes/tests.rs b/src/nodes/tests.rs index 2acaac0..1d40f50 100644 --- a/src/nodes/tests.rs +++ b/src/nodes/tests.rs @@ -1,78 +1,40 @@ use core::f64; use std::str::FromStr; -use rug::Float; +use crate::nodes::Nodes; -use crate::nodes::{Function, Node, Nodes, Operator, PREC}; +fn eval(s: &str) -> f64 { + Nodes::from_str(s).unwrap().evaluate().unwrap().to_f64() +} + +#[test] +fn plus_minus_order() { + assert_eq!(eval("1 - 4 - 5"), -8.); + assert_eq!(eval("1 - 4 + 5"), 2.); + assert_eq!(eval("1 + 4 - 5"), 0.); +} #[test] fn e_edge_case() { - assert_eq!(Nodes::from_str("1e-1").unwrap().evaluate().to_f64(), 0.1) + assert_eq!(eval("1e-1"), 0.1) } #[test] fn implicit_mult() { - assert_eq!( - Nodes::from_str("10 sin(10)").unwrap().evaluate().to_f64(), - -5.440211108893698 - ); + assert_eq!(eval("10 sin(10)"), -5.440211108893698); - assert_eq!( - Nodes::from_str("(10)(10)").unwrap().evaluate().to_f64(), - 100., - ) + assert_eq!(eval("(10)(10)"), 100.,) } #[test] fn constants() { - assert_eq!( - Nodes::from_str("c").unwrap().evaluate().to_f64(), - 299792458. - ); - assert_eq!( - Nodes::from_str("A").unwrap().evaluate().to_f64(), - 6.02214076e23 - ); - assert_eq!( - Nodes::from_str("pi").unwrap().evaluate().to_f64(), - f64::consts::PI - ); + assert_eq!(eval("c"), 299792458.); + assert_eq!(eval("A"), 6.02214076e23); + assert_eq!(eval("pi"), f64::consts::PI); } #[test] fn unary_operators() { - assert_eq!(Nodes::from_str("-10").unwrap().evaluate().to_f64(), -10.); - assert_eq!(Nodes::from_str("+10").unwrap().evaluate().to_f64(), 10.); -} - -#[test] -fn test_nodes_to_num() { - assert_eq!( - Nodes(vec![ - Node::Function(Function::Sine), - Node::ParenthesisStart, - Node::Number(Float::with_val(PREC, 2)), - Node::ParenthesisEnd, - Node::Operator(Operator::Plus), - Node::Number(Float::with_val(PREC, 1)) - ]) - .evaluate() - .to_f64(), - 1.9092974268256817, - ); -} - -#[test] -fn test_nodes_from_str() { - assert_eq!( - Nodes::from_str("sin(2) + 1").unwrap().as_ref(), - &[ - Node::Function(Function::Sine), - Node::ParenthesisStart, - Node::Number(Float::with_val(PREC, 2)), - Node::ParenthesisEnd, - Node::Operator(Operator::Plus), - Node::Number(Float::with_val(PREC, 1)) - ], - ) + assert_eq!(eval("-10"), -10.); + assert_eq!(eval("+10"), 10.); }