merged rpn(1); closes #2

This commit is contained in:
Emma Tebibyte 2024-02-07 19:23:51 -07:00
commit 0f5247e722
Signed by untrusted user: emma
GPG Key ID: 06FA419A1698C270
3 changed files with 327 additions and 1 deletions

View File

@ -9,6 +9,7 @@
.POSIX: .POSIX:
.PRAGMA: posix_202x # future POSIX standard support à la pdpmake(1) .PRAGMA: posix_202x # future POSIX standard support à la pdpmake(1)
.PRAGMA: command_comment # breaks without this?
PREFIX=/usr/local PREFIX=/usr/local
@ -51,7 +52,7 @@ build/o/libsysexits.rlib: build
"$$(printf '#include <sysexits.h>\n' \ "$$(printf '#include <sysexits.h>\n' \
| cpp -M -idirafter "build/include" - \ | cpp -M -idirafter "build/include" - \
| sed 's/ /\n/g' | grep sysexits.h)" \ | 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 build/o/libgetopt.rlib: src/getopt-rs/lib.rs
$(RUSTC) $(RUSTCFLAGS) --crate-type=lib --crate-name=getopt \ $(RUSTC) $(RUSTCFLAGS) --crate-type=lib --crate-name=getopt \
@ -79,6 +80,13 @@ intcmp: build/bin/intcmp
build/bin/intcmp: src/intcmp.c build build/bin/intcmp: src/intcmp.c build
$(CC) $(CFLAGS) -o $@ src/intcmp.c $(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 .PHONY: scrut
scrut: build/bin/scrut scrut: build/bin/scrut
build/bin/scrut: src/scrut.c build build/bin/scrut: src/scrut.c build

70
docs/rpn.1 Normal file
View File

@ -0,0 +1,70 @@
.\" Copyright (c) 2024 Emma Tebibyte <emma@tebibyte.media>
.\" Copyright (c) 2024 DTB <trinity@trinity.moe>
.\"
.\" This work is licensed under CC BY-SA 4.0. To see a copy of this license,
.\" visit <http://creativecommons.org/licenses/by-sa/4.0/>.
.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 <emma@tebibyte.media>.
.SH COPYRIGHT
Copyright (c) 2024 Emma Tebibyte. License AGPLv3+: GNU AGPL version 3 or later
<https://gnu.org/licenses/agpl.html>.
.SH SEE ALSO
bc(1p), dc(1), rpn(7), IEEE 754

248
src/rpn.rs Normal file
View File

@ -0,0 +1,248 @@
/*
* Copyright (c) 2024 Emma Tebibyte <emma@tebibyte.media>
* 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::<f64>() {
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,
}
// Im 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<f64>,
) -> Result<(VecDeque<f64>, 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<CalcType> = input
.split_whitespace()
.rev()
.map(|t| CalcType::from(t))
.collect();
let mut ops: VecDeque<CalcType> = 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::<Vec<String>>();
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::<Vec<String>>()
.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)
}