diff --git a/Makefile b/Makefile index 5c8f26e..e531272 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ .POSIX: .PRAGMA: posix_202x # future POSIX standard support à la pdpmake(1) +.PRAGMA: command_comment # breaks without this? PREFIX=/usr/local @@ -51,7 +52,7 @@ build/o/libsysexits.rlib: build "$$(printf '#include \n' \ | cpp -M -idirafter "build/include" - \ | sed 's/ /\n/g' | grep sysexits.h)" \ - | $(RUSTC) $(RUSTCFLAGS) --crate-type lib -o build/o/libsysexits.rlib - + | $(RUSTC) $(RUSTFLAGS) --crate-type lib -o build/o/libsysexits.rlib - build/o/libgetopt.rlib: src/getopt-rs/lib.rs $(RUSTC) $(RUSTCFLAGS) --crate-type=lib --crate-name=getopt \ @@ -79,6 +80,13 @@ intcmp: build/bin/intcmp build/bin/intcmp: src/intcmp.c build $(CC) $(CFLAGS) -o $@ src/intcmp.c +.PHONY: rpn +rpn: build/bin/rpn +build/bin/rpn: src/rpn.rs build build/o/libsysexits.rlib + $(RUSTC) $(RUSTFLAGS) \ + --extern sysexits=build/o/libsysexits.rlib \ + -o $@ src/rpn.rs + .PHONY: scrut scrut: build/bin/scrut build/bin/scrut: src/scrut.c build diff --git a/docs/rpn.1 b/docs/rpn.1 new file mode 100644 index 0000000..2197fbe --- /dev/null +++ b/docs/rpn.1 @@ -0,0 +1,70 @@ +.\" Copyright (c) 2024 Emma Tebibyte +.\" Copyright (c) 2024 DTB +.\" +.\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, +.\" visit . + +.TH rpn 1 + +.SH NAME + +rpn \(en reverse polish notation evaluation + +.SH SYNOPSIS + +rpn +.RB [numbers...]\ [operators...] + +.SH DESCRIPTION + +Rpn evaluates reverse polish notation expressions either read from the standard +input or parsed from provided arguments. See the STANDARD INPUT section. + +Upon evaluation, rpn will print the resulting number on the stack to the +standard output. Any further specified numbers will be placed at the end of the +stack. + +For information on for reverse polish notation syntax, see rpn(7). + +.SH STANDARD INPUT + +If arguments are passed to rpn, it interprets them as an expression to be +evaluated. Otherwise, it reads whitespace-delimited numbers and operations from +the standard input. + +.SH DIAGNOSTICS + +If encountering a syntax error, rpn will exit with the appropriate error code +as defined by sysexits.h(3) and print an error message. + +.SH CAVEATS + +Due to precision constraints and the way floats are represented in accordance +with the IEEE Standard for Floating Point Arithmetic (IEEE 754), floating-point +arithmetic has rounding errors. This is somewhat curbed by using the +machine epsilon as provided by the Rust standard library to which to round +numbers. Because of this, variation is expected in the number of decimal places +rpn can handle based on the platform and hardware of any given machine. + +.SH RATIONALE + +An infix notation calculation utility, bc(1p), is included in the POSIX +standard, but does not accept expressions as arguments; in scripts, any +predefined, non-interactive input must be piped into the program. A dc(1) +pre-dates the standardized bc(1p), the latter originally being a preprocessor +for the former, and was included in UNIX v2 onward. While it implements reverse +polish notation, it still suffers from being unable to accept an expression as +an argument. + +.SH AUTHOR + +Written by Emma Tebibyte . + +.SH COPYRIGHT + +Copyright (c) 2024 Emma Tebibyte. License AGPLv3+: GNU AGPL version 3 or later +. + +.SH SEE ALSO + +bc(1p), dc(1), rpn(7), IEEE 754 diff --git a/src/rpn.rs b/src/rpn.rs new file mode 100644 index 0000000..0168972 --- /dev/null +++ b/src/rpn.rs @@ -0,0 +1,248 @@ +/* + * 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::stdin, + process::ExitCode, +}; + +use CalcType::*; + +extern crate sysexits; + +use sysexits::EX_DATAERR; + +#[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: i32, +} + +// 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 * 100.0; + +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; + + 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(x.powf(y)), + 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, precision: usize) -> f64 { + let multiplier = 10_f64.powi(precision as i32); + (value * multiplier).round() / multiplier +} + +fn main() -> ExitCode { + let argv = args().collect::>(); + let mut stack = VecDeque::new(); + let mut buf = String::new(); + // Set floating-point precision for correcting rounding errors based on + // machine epsilon + let precision = (-f64::EPSILON.log10() * PRECISION_MOD).ceil() as usize; + + if argv.get(1).is_none() { + while let Ok(_) = stdin().read_line(&mut buf) { + match eval(&buf.trim(), stack) { + Ok(s) => { + buf.clear(); + stack = s.0.clone(); + + let val = match stack.iter().last() { + Some(v) => v, + None => break, + }; + + if s.1 == false { continue; } + + println!("{}", round_precise(val, precision).to_string()); + }, + Err(err) => { + eprintln!("{}: {}", argv[0], err.message); + return ExitCode::from(err.code as u8); + }, + }; + } + } else { + let input = argv + .iter() + .skip(1) + .map(|x| x.to_owned()) + .collect::>() + .join(" "); + + match eval(&input, stack) { + Ok(s) => { + stack = s.0.clone(); + + let val = match stack.iter().last() { + Some(v) => v, + None => return ExitCode::from(0), + }; + + println!("{}", round_precise(val, precision).to_string()) + }, + Err(err) => { + eprintln!("{}: {}", argv[0], err.message); + return ExitCode::from(err.code as u8); + }, + }; + } + ExitCode::from(0) +}