/* * Copyright (c) 2024 Emma Tebibyte * SPDX-License-Identifier: AGPL-3.0-or-later * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. * * This file incorporates work covered by the following copyright and permission * notice: * * MIT License * * Copyright (c) 2022 Lilly Cham * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files * (the "Software"), to deal in the Software without restriction, including * without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software * is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE * USE OR OTHER DEALINGS IN THE SOFTWARE. */ use std::{ collections::VecDeque, env::args, fmt::{ self, Display, Formatter }, io::{ Error, Write, stdin, stdout }, process::ExitCode, }; use CalcType::*; extern crate sysexits; use sysexits::{ EX_DATAERR, EX_IOERR }; #[cfg(target_os="openbsd")] use sysexits::EX_OSERR; #[cfg(target_os="openbsd")] extern crate strerror; #[cfg(target_os="openbsd")] extern crate openbsd; #[cfg(target_os="openbsd")] use strerror::StrError; #[cfg(target_os="openbsd")] use openbsd::{ Promises, pledge, unveil }; #[derive(Clone, PartialEq, PartialOrd, Debug)] /* enum CalcType is a type containing operations used in the calculator */ enum CalcType { Add, Subtract, Multiply, Divide, Power, Floor, Modulo, Val(f64), Invalid(String), } impl From<&str> for CalcType { fn from(value: &str) -> Self { match value { "+" => Add, "-" | "−" => Subtract, "*" | "×" => Multiply, "/" | "÷" => Divide, "^" | "**" => Power, "//" => Floor, "%" => Modulo, _ => { match value.parse::() { Ok(x) => Val(x), Err(_) => Invalid(value.to_owned()), } }, } } } impl Display for CalcType { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let y: String; write!(f, "{}", match self { Add => "addition", Subtract => "subtraction", Multiply => "multiplication", Divide => "division", Power => "exponentiation", Floor => "floor division", Modulo => "modulus", Val(x) => { y = x.to_string(); &y }, Invalid(i) => { y = i.to_string(); &y }, }) } } #[derive(Debug, Clone)] struct EvaluationError { message: String, code: u8, } impl StrError for EvaluationError { fn strerror(&self) -> String { self.message.clone() } } /* I’m no math nerd but I want the highest possible approximation of 0.9 * repeating and it seems this can give it to me */ const PRECISION_MOD: f64 = 0.9 + f64::EPSILON; fn err(argv0: &String, e: &T, code: Option) -> ExitCode { eprintln!("{}: {}", argv0, e.strerror()); ExitCode::from(code.unwrap_or(1 /* unknown error */)) } fn eval( input: &str, initial_stack: VecDeque, ) -> Result<(VecDeque, bool), EvaluationError> { let mut stack = initial_stack; let mut oper = false; if input.is_empty() { stack.clear(); return Ok((stack, oper)); } /* Split the input into tokens. */ let mut toks: VecDeque = input .split_whitespace() .rev() .map(|t| CalcType::from(t)) .collect(); let mut ops: VecDeque = VecDeque::new(); while let Some(n) = toks.pop_back() { match n { Val(v) => stack.push_back(v), Invalid(i) => { return Err(EvaluationError { message: format!("{}: invalid token", i), code: EX_DATAERR, }) }, op => { ops.push_back(op.clone()); oper = true; /* this is an operation */ let vals = (stack.pop_back(), stack.pop_back()); if let (Some(x), Some(y)) = vals { match op { Add => stack.push_back(y + x), Subtract => stack.push_back(y - x), Multiply => stack.push_back(y * x), Divide => stack.push_back(y / x), Power => stack.push_back(y.powf(x)), Floor => stack.push_back((y / x).floor()), Modulo => stack.push_back(y % x), _ => {}, }; } else { return Err(EvaluationError { message: format!("{}: unexpected operation", op), code: EX_DATAERR, }) } }, }; } Ok((stack, oper)) } /* Round a float to the given precision level */ fn round_precise(value: &f64) -> f64 { /* Set floating-point precision for correcting rounding errors based on * machine epsilon */ let precision = (-f64::EPSILON.log10() * PRECISION_MOD).ceil() as i32; let multiplier = 10_f64.powi(precision as i32); (value * multiplier).round() / multiplier } /* print the stack and let the caller know if evaluation should continue */ fn unstack(stack: VecDeque, op: bool) -> Result { if let Some(val) = stack.iter().last() { if !op { return Ok(true); } let out = round_precise(val).to_string() + &'\n'.to_string(); return stdout().write_all(out.as_bytes()).map(|_| true); } else { return Ok(false); } } fn main() -> ExitCode { let argv = args().collect::>(); #[cfg(target_os="openbsd")] { let promises = Promises::new("stdio unveil"); if let Err(e) = pledge(Some(promises), None) { return err(&argv[0], &e, Some(EX_OSERR)); } if let Err(e) = unveil(None, None) { return err(&argv[0], &e, Some(EX_OSERR)); } } let mut stack = VecDeque::new(); let mut buf = String::new(); if argv.get(1).is_some() { /* read expressions from argv */ /* join argv into an owned String joined by spaces minus argv[0] */ let input = argv .iter() .skip(1) .map(|x| x.to_owned()) .collect::>() .join(" "); match eval(&input, stack) { Ok(s) => { /* we can ignore the return value of unstack() because we are * not continually evaluating from stdin */ if let Err(e) = unstack(s.0.clone(), s.1.clone()) { return err(&argv[0], &e, Some(EX_IOERR)); } return ExitCode::SUCCESS; }, Err(e) => { return err(&argv[0], &e, Some(e.code)); }, }; } /* else, read from stdin */ loop { /* take input until EOF */ if let Err(e) = stdin().read_line(&mut buf) { return err(&argv[0], &e, Some(EX_IOERR)); } match eval(&buf.trim(), stack) { Ok(s) => { buf.clear(); stack = s.0.clone(); /* out with the old, in with the new */ match unstack(s.0, s.1) { Ok(b) if b => continue, Ok(_) => break, Err(e) => { return err(&argv[0], &e, Some(EX_IOERR)) }, }; }, Err(e) => { return err(&argv[0], &e, Some(e.code)); }, }; } ExitCode::SUCCESS }