merged rpn(1); closes #2
This commit is contained in:
commit
0f5247e722
10
Makefile
10
Makefile
@ -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
70
docs/rpn.1
Normal 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
248
src/rpn.rs
Normal 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user