sci_calc/
lib.rs

1//! An simple, comprehensive scientific calculator library
2//! 
3//! An easy-to-use scientific calculator crate that evaluates mathematical 
4//! expressions in strings. Includes variable assignment and recall, comprehensive
5//! built-in functions and constants, and elegant error handling. The `calculate()`
6//! function is passed a `Context` object that is used for keeping state between
7//! calculations such as user-defined variables.
8//! 
9//! # Example
10//! 
11//! ```
12//! # use sci_calc::{calculate, context::Context};
13//! let mut ctx = Context::new();
14//! assert_eq!(calculate("5 + 5", &mut ctx), Ok(10.0));
15//! ```
16
17use std::fmt;
18use lalrpop_util::{lalrpop_mod, ParseError};
19use libm::tgamma;
20
21pub mod context;
22use context::*;
23
24mod ast;
25use ast::*;
26
27// defining lalrpop's parsing module
28lalrpop_mod!(grammar);
29
30/// Attempts to calculate a string containing a mathematical expression
31/// 
32/// On top of the input string, this function also takes a mutable reference to
33/// a `Context` object, this object is used to keep track of state between calls,
34/// tracking things like user-defined variables and previous answers. Returns a
35/// result containing the solution to the expression if successful, or a `CalcError`
36/// struct if not.
37/// 
38/// # Example
39/// 
40/// ```
41/// # use sci_calc::{calculate, context::Context};
42/// # let mut ctx = Context::new();
43/// assert_eq!(calculate("5 + 5", &mut ctx), Ok(10.0));
44/// ```
45pub fn calculate(input_str: &str, ctx: &mut Context) -> Result<f64, CalcError> {
46	
47	let input_str = if let Some(stripped) = input_str.strip_suffix('\n') { stripped } else { input_str };
48
49	// invoking grammar parser generated by lalrpop
50	let parser = grammar::targetParser::new();
51	let (tree, assignment) = match parser.parse(input_str) {
52		Ok(res) => { res }
53		Err(e) => {
54			
55			let msg = match &e {
56				ParseError::InvalidToken { location } => {
57					let pad = std::iter::repeat(" ").take(*location).collect::<String>();
58					format!("Invalid token\n| {input_str}\n| {pad}└── here")
59				},
60				ParseError::UnrecognizedEof { location: _, expected: _ } => {
61					String::from("Unexpected EOI")
62				},
63				ParseError::UnrecognizedToken { token, expected: _ } => {
64					let pad = std::iter::repeat(" ").take(token.0).collect::<String>();
65					format!("Unexpected token\n| {input_str}\n| {pad}└── here")
66				},
67				ParseError::ExtraToken { token} => {
68					let pad = std::iter::repeat(" ").take(token.0).collect::<String>();
69					format!("Extra token\n| {input_str}\n| {pad}└── here")
70				},
71				_ => String::from("Parser error"),
72			};
73			return Err(CalcError {
74				error_type: CalcErrorType::ParserError,
75				msg,
76			});
77		}
78	};
79	//print!("Tree: {tree}\r\n");
80
81	let res = evaluate_ast(*tree, ctx);
82
83	if res.is_ok() {
84		let solution = res.clone().unwrap();
85
86	
87		if assignment.is_some() {
88			// handling assignment
89			let assign_var = assignment.unwrap();
90			if let Err(e) = ctx.assign_var(&assign_var, solution) {
91				return Err(e);
92			}
93		} else {
94			// setting `ans` variable
95			ctx.prev_ans = Some(solution);
96		}
97	}
98
99	return res;
100}
101
102/// Recursive function used to evaluate the abstract syntax tree generated by
103/// the lalrpop parser
104fn evaluate_ast(root: Expr, ctx: &Context) -> Result<f64, CalcError> {
105	match root {
106		Expr::Num(n) => {
107			Ok(n)
108		}
109		Expr::Op(left_e, op, right_e) => {
110			// evaluation inner expressions
111			let lhs = match evaluate_ast(*left_e, ctx) {
112				Ok(n) => n,
113				Err(e) => { return Err(e) },
114			};
115			let rhs = match evaluate_ast(*right_e, ctx) {
116				Ok(n) => n,
117				Err(e) => { return Err(e) },
118			};
119			// performing operation
120			let res = match op {
121				Operation::Add => { lhs + rhs }
122				Operation::Sub => { lhs - rhs }
123				Operation::Mul => { lhs * rhs }
124				Operation::Div => { lhs / rhs }
125				Operation::FloorDiv => { f64::floor(lhs / rhs) }
126				Operation::Mod => { lhs % rhs }
127				Operation::Exp => { lhs.powf(rhs) }
128			};
129			Ok(res)
130		}
131		Expr::Func(name, arg_list) => {
132			let mut args: Vec<f64> = Vec::new();
133			for arg in arg_list {
134				let val = match evaluate_ast(*arg, ctx) {
135					Ok(n) => n,
136					Err(e) => { return Err(e) },
137				};
138				args.push(val);
139			}
140			if let Some(res) = ctx.try_function(&name, args) {
141				return res;
142			}
143			return Err(CalcError {
144				error_type: CalcErrorType::UndefinedIdentifier,
145				msg: format!("Unknown function \"{name}()\""),
146			})
147		}
148		Expr::Var(name) => {
149			if let Some(res) = ctx.lookup_var(&name) {
150				return res;
151			}
152			return Err(CalcError {
153				error_type: CalcErrorType::UndefinedIdentifier,
154				msg: format!("Unknown variable \"{name}\""),
155			})
156		}
157		Expr::Fac(e) => {
158			let num = match evaluate_ast(*e, ctx) {
159				Ok(n) => n,
160				Err(e) => { return Err(e) },
161			};
162			return Ok(tgamma(num + 1.0));
163		}
164	}
165}
166
167/// Custom error handling struct
168#[derive(Debug, Clone, PartialEq)]
169pub struct CalcError {
170	/// Broad type of error
171	pub error_type: CalcErrorType,
172	/// Description of error
173	pub msg: String,
174}
175impl fmt::Display for CalcError {
176	fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
177		write!(formatter, "{}: {}\n", self.error_type, self.msg)
178	}
179}
180
181/// Error types
182#[derive(Debug, Clone, Copy, PartialEq)]
183pub enum CalcErrorType {
184	/// Error generated during parsing of the input
185	ParserError,
186	/// Error generated when encountering an unknown variable or function
187	UndefinedIdentifier,
188	/// Error generated when assigning a value to a variable fails
189	AssignmentError,
190	/// Error generated from passing invalid arguments into a function
191	ArgumentError,
192	/// Error generated during calculation
193	CalculationError,
194}
195impl fmt::Display for CalcErrorType {
196	fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
197		write!(formatter, "{}", match *self {
198			Self::ParserError => { "Parser error" },
199			Self::UndefinedIdentifier => { "Undefined identifier" },
200			Self::AssignmentError => { "Assignment error" },
201			Self::ArgumentError => { "Argument error" },
202			Self::CalculationError => { "Calculation error" },
203		})
204	}
205}