commit 85178007fff28f8df769ae5cd0f509dc160fe95b
parent 39807875e38def044b66ac250be28b7df73d73b1
Author: Zack Newman <zack@philomathiclife.com>
Date: Mon, 24 Apr 2023 19:27:53 -0600
add rand. add evaluator iterator.
Diffstat:
M | Cargo.toml | | | 12 | +++++++++++- |
M | README.md | | | 119 | ++++++++++++++++++++++++++++++++++++++++++------------------------------------- |
M | lang.tex | | | 40 | ++++++++++++++++++++++++---------------- |
A | src/lending_iterator.rs | | | 127 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | src/lib.rs | | | 841 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------- |
M | src/main.rs | | | 233 | ++++++++++++++++++++++++++++++++----------------------------------------------- |
6 files changed, 1046 insertions(+), 326 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0"
name = "calc_rational"
readme = "README.md"
repository = "https://git.philomathiclife.com/repos/calc_rational/"
-version = "0.2.0"
+version = "0.3.0"
[lib]
name = "calc_lib"
@@ -24,6 +24,16 @@ num-bigint = { version = "0.4.3", default-features = false }
num-integer = { version = "0.1.45", default-features = false }
num-rational = { version = "0.4.1", default-features = false, features = ["num-bigint"] }
num-traits = { version = "0.2.15", default-features = false }
+rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"], optional = true }
+
+[features]
+rand = ["dep:rand", "std"]
+std = []
+default = ["std"]
+
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = ["--cfg", "docsrs"]
[badges]
maintenance = { status = "actively-developed" }
diff --git a/README.md b/README.md
@@ -38,32 +38,10 @@ round(@, 6)
> 2/3
> 0.666666667
-@6
-There are only 4 previous results.
-s 1
-Invalid store expression. A store expression must be of the extended regex form: ^[ \t]*s[ \t]*$.
-q a
-Invalid quit expression. A quit expression must be of the extended regex form: ^[ \t]*q[ \t]*$.
-a
-Missing terminal expression at position 0. A terminal expression is a decimal literal expression, recall expression, absolute value expression, parenthetical expression, or a round expression.
-|1
-Invalid absolute value expression ending at position 2. An absolute value expression is an addition expression enclosed in '||'.
-(2
-Invalid parenthetical expression ending at position 2. A parenthetical expression is an addition expression enclosed in '()'.
-round(1,10)
-Invalid round expression ending at position 9. A round expression is of the form 'round(<add expression>, digit)'
-1/(2-2)
-Division by zero ending at position 7.
-2^(1/2)
-Non-integer exponent with a base that was not 0 or 1 ending at position 7.
-0^-1
-Non-negative exponent with a base of 0 ending at position 4.
-(-1)!
-Factorial of a rational number that was not a non-negative integer ending at position 5.
-4.
-Invalid decimal literal expression ending at position 2. A decimal literal expression must be of the extended regex form: [0-9]+(\.[0-9]+)?.
-1+2 a
-Trailing symbols starting at position 4.
+rand()
+> 939435294927814822
+rand(1+9,10!)
+> 2660936
q
[zack@laptop ~]$
```
@@ -71,7 +49,7 @@ q
## Expressions
The following are the list of expressions in descending order of precedence:
- 1. number literals, `@`, `()`, `||`, `round()`
+ 1. number literals, `@`, `()`, `||`, `round()`, `rand()`
2. `!`
3. `^`
4. `-` (unary negation operator)
@@ -111,6 +89,11 @@ With the aforementioned exception of `!`, all spaces and tabs before and after o
`round(expression, digit)` rounds `expression` to `digit`-number of fractional digits. An error will
be displayed if called incorrectly.
+## Rand expression
+
+`rand(expression, expression)` generates a random 64-integer inclusively between the passed expressions.
+An error will be displayed if called incorrectly. `rand()` generates a random 64-bit integer.
+
## Numbers
A number literal is a non-empty sequence of digits or a non-empty sequence of digits immediately followed by `.`
@@ -143,8 +126,8 @@ As emphasized, it does not work on expressions; so both `@@` and `@(1)` are gram
## Character encoding
All inputs must only contain the ASCII encoding of the following Unicode scalar values: `0`-`9`, `.`, `+`, `-`,
-`*`, `/`, `^`, `!`, `|`, `(`, `)`, `round`, `,`, `@`, `s`, <space>, <tab>, <line feed>, <carriage return>,
-`q`, and `d`. Any other byte sequences are grammatically incorrect and will lead to an error message.
+`*`, `/`, `^`, `!`, `|`, `(`, `)`, `round`, `rand`, `,`, `@`, `s`, <space>, <tab>, <line feed>, <carriage return>,
+and `q`. Any other byte sequences are grammatically incorrect and will lead to an error message.
## Errors
@@ -173,18 +156,25 @@ target. Note one must be aware of the ASCII encoding requirement. In particular
### Installing
```bash
-[zack@laptop ~]$ cargo install calc_rational
+[zack@laptop ~]$ cargo install --all-features calc_rational
Updating crates.io index
- Installing calc_rational v0.2.0
+ Installing calc_rational v0.3.0
Compiling autocfg v1.1.0
+ Compiling libc v0.2.142
+ Compiling cfg-if v1.0.0
+ Compiling ppv-lite86 v0.2.17
Compiling num-traits v0.2.15
Compiling num-integer v0.1.45
Compiling num-bigint v0.4.3
Compiling num-rational v0.4.1
- Compiling calc_rational v0.1.2
- Finished release [optimized] target(s) in 6.09s
+ Compiling getrandom v0.2.9
+ Compiling rand_core v0.6.4
+ Compiling rand_chacha v0.3.1
+ Compiling rand v0.8.5
+ Compiling calc_rational v0.3.0 (/home/zack/projects/calc_rational)
+ Finished release [optimized] target(s) in 5.54s
Installing /home/zack/.cargo/bin/calc
- Installed package `calc_rational v0.2.0` (executable `calc`)
+ Installed package `calc_rational v0.3.0` (executable `calc`)
```
### Building and testing
@@ -193,53 +183,70 @@ target. Note one must be aware of the ASCII encoding requirement. In particular
[zack@laptop ~]$ git clone https://git.philomathiclife.com/repos/calc_rational
Cloning into 'calc_rational'...
[zack@laptop ~]$ cd calc_rational/
-[zack@laptop calc_rational]$ cargo build --release
+[zack@laptop calc_rational]$ cargo build --release --all-features
Updating crates.io index
Compiling autocfg v1.1.0
+ Compiling libc v0.2.142
+ Compiling cfg-if v1.0.0
+ Compiling ppv-lite86 v0.2.17
Compiling num-traits v0.2.15
Compiling num-integer v0.1.45
Compiling num-bigint v0.4.3
Compiling num-rational v0.4.1
- Compiling calc_rational v0.2.0 (/home/zack/calc_rational)
+ Compiling getrandom v0.2.9
+ Compiling rand_core v0.6.4
+ Compiling rand_chacha v0.3.1
+ Compiling rand v0.8.5
+ Compiling calc_rational v0.3.0 (/home/zack/calc_rational)
Finished release [optimized] target(s) in 8.25s
-[zack@laptop calc_rational]$ cargo t
+[zack@laptop calc_rational]$ cargo t --release --all-features
Compiling autocfg v1.1.0
+ Compiling libc v0.2.142
+ Compiling cfg-if v1.0.0
+ Compiling ppv-lite86 v0.2.17
Compiling num-traits v0.2.15
Compiling num-integer v0.1.45
Compiling num-bigint v0.4.3
Compiling num-rational v0.4.1
- Compiling calc_rational v0.2.0 (/home/zack/calc_rational)
- Finished test [unoptimized + debuginfo] target(s) in 2.40s
- Running unittests src/lib.rs (target/debug/deps/calc_lib-381715bd94efc206)
+ Compiling getrandom v0.2.9
+ Compiling rand_core v0.6.4
+ Compiling rand_chacha v0.3.1
+ Compiling rand v0.8.5
+ Compiling calc_rational v0.3.0 (/home/zack/projects/calc_rational)
+ Finished release [optimized] target(s) in 10.35s
+ Running unittests src/lib.rs (target/release/deps/calc_lib-fa299178069cd874)
-running 23 tests
+running 26 tests
+test cache::tests::test_len ... ok
+test tests::abs ... ok
+test cache::tests::test_get_unsafe ... ok
test cache::tests::test_get ... ok
+test cache::tests::test_push ... ok
test cache::tests::test_index ... ok
-test cache::tests::test_is_empty ... ok
-test cache::tests::test_get_unsafe ... ok
-test cache::tests::test_len ... ok
test cache::tests::test_new ... ok
+test cache::tests::test_is_empty ... ok
test tests::exit ... ok
test cache::tests::test_index_panic - should panic ... ok
-test tests::abs ... ok
-test cache::tests::test_push ... ok
-test tests::factorial ... ok
+test tests::mult ... ok
+test tests::add ... ok
test tests::neg ... ok
-test tests::recall_expression ... ok
-test tests::par ... ok
-test tests::empty ... ok
+test tests::eval_iter ... ok
+test tests::number_literal ... ok
+test tests::rand ... ok
+test tests::exp ... ok
+test tests::rand_uni ... ignored
test tests::eval ... ok
+test tests::factorial ... ok
+test tests::empty ... ok
test tests::store ... ok
-test tests::exp ... ok
-test tests::add ... ok
-test tests::mult ... ok
test tests::round ... ok
+test tests::recall_expression ... ok
+test tests::par ... ok
test tests::term ... ok
-test tests::number_literal ... ok
-test result: ok. 23 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
+test result: ok. 25 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
- Running unittests src/main.rs (target/debug/deps/calc-42b0141dee0be10c)
+ Running unittests src/main.rs (target/release/deps/calc-c20eab17c4183fda)
running 0 tests
diff --git a/lang.tex b/lang.tex
@@ -6,27 +6,29 @@ We will define the language, \(L\), of our rational number calculator program.
Define the set of non-terminal symbols to be
\begin{align*}
-N = \{&expr, empty, quit, eval, store, add, mult, neg, exp, fact, term,\\
-&r, abs, recall, par, dec, int, ws, digit, prev, l\}.
+N = \{&prog, expr, empty, quit, eval, store, add, mult, neg, exp, fact,\\
+&term, rnd, r, abs, recall, par, dec, int, ws, digit, prev, l\}.
\end{align*}
Define the set of terminal symbols to be
\begin{align*}
\Sigma = \{&0,1,2,3,4,5,6,7,8,9,.,+,-,*,/,\string^,!,|(,),round,,,@,s,space\\
-&tab,lf,cr,q,d\}.
+&rand, tab,lf,cr,q\}.
\end{align*}
Define the production rules, \(P\), as the following:
\begin{enumerate}
+ \item \(prog \rightarrow expr\ |\ expr\ l\ |\ expr\ l\ prog\)
\item \(expr \rightarrow empty\ |\ quit\ |\ eval\ |\ store\)
- \item \(empty \rightarrow ws\ l\)
- \item \(quit \rightarrow ws\ q\ ws\ l\)
- \item \(eval\rightarrow ws\ add\ ws\ l\)
- \item \(store\rightarrow ws\ s\ ws\ l\)
+ \item \(empty \rightarrow ws\)
+ \item \(quit \rightarrow ws\ q\ ws\)
+ \item \(eval\rightarrow ws\ add\ ws\)
+ \item \(store\rightarrow ws\ s\ ws\)
\item \(add \rightarrow add\ ws\ +\ ws\ mult\ |\ add\ ws\ -\ ws\ mult\ |\ mult\)
\item \(mult \rightarrow mult\ ws\ *\ ws\ neg\ |\ mult\ ws\ /\ ws\ neg\ |\ neg\)
\item \(neg \rightarrow -\ ws\ neg\ |\ exp\)
\item \(exp \rightarrow fact\ ws\ \string^\ ws\ neg\ |\ fact\)
\item \(fact \rightarrow fact!\ |\ term\)
\item \(term \rightarrow dec\ |\ par\ |\ recall\ |\ abs\ |\ r\)
+ \item \(rnd \rightarrow rand(ws)\ |\ rand(ws\ add\ ws,ws\ add\ ws)\)
\item \(r \rightarrow round(ws\ add\ ws,ws\ digit\ ws)\)
\item \(abs \rightarrow |ws\ add\ ws|\)
\item \(recall \rightarrow @\ |\ @prev\)
@@ -41,7 +43,7 @@ Define the production rules, \(P\), as the following:
Note that the use of spaces above is purely for visualization purposes (e.g., \(digit\ int\)
does not actually have a space). Define the start symbol to be \(expr\). Define the unambiguous,
context-free grammar to be
-\[ G = (N, \Sigma, P, expr). \]
+\[ G = (N, \Sigma, P, prog). \]
Let \(\mathcal{L}(G)\) be the language generated from \(G\). When \(@\) is not immediately followed by a \(prev\),
let it mean \(@1\). \(@prev\) represents the \(prev^{th}\) most-recent result that has been stored from a \(store\)
expression. \(lf\) is the Unicode scalar value U+000A, \(cr\) is the Unicode scalar value U+000D, \(space\) is the
@@ -49,15 +51,21 @@ Unicode scalar value U+0020, \(tab\) is the Unicode scalar value U+0009, and \(\
define \(\mathbb{Q} \subset L \subset \mathcal{L}(G)\) with \(\mathbb{Q}\) representing the field of rational
numbers such that \(L\) extends \(\mathbb{Q}\) with the ability to recall the previous one to eight \(store\)
results as well as adds the unary operators \(||\), \(-\), and \(!\) as well as the binary operator \(\string^\) to
-mean absolute value, negation, factorial, and exponentiation respectively. It also adds the function \(round\) which
-rounds the passed expression to \(digit\)-number of fractional digits. Note that this means for \(mult / exp\),
-\(exp\) does not evaluate to 0. Similarly, \(term\string^exp\) is valid iff \(term\) evaluates to 1, \(term\)
-evaluates to 0 and \(exp\) evaluates to a non-negative rational number—\(0^{0}\) is defined to be 1—or \(term\)
-evaluates to any other rational number and \(exp\) evaluates to an integer. \(!\) is only defined for non-negative
-integers. \(@prev\) is only defined iff at least \(prev\) number of previous \(store\) expressions have been
-evaluated. From the above grammar, we see the expression precedence in descending order is the following:
+mean absolute value, negation, factorial, and exponentiation respectively. Note that this means for \(mult / exp\),
+\(exp\) does not evaluate to 0. Similarly, \(term\string^exp\) is valid iff \(term\) evaluates to 1, \(term\) evaluates
+to 0 and \(exp\) evaluates to a non-negative rational number—\(0^{0}\) is defined to be 1—or \(term\) evaluates to any
+other rational number and \(exp\) evaluates to an integer. \(!\) is only defined for non-negative integers. \(@prev\) is
+only defined iff at least \(prev\) number of previous \(store\) expressions have been evaluated.
+
+It also adds the function \(round\) which rounds the passed expression to \(digit\)-number of fractional digits.
+The function \(rand\) when passed no arguments generates a random 64-bit integer. When the function is passed two
+arguments, it generates a random 64-bit integer inclusively between the two arguments. In the latter case, the
+second argument must evaluate to a number greater than or equal to the first; and there must be at least one 64-bit
+integer in that interval.
+
+From the above grammar, we see the expression precedence in descending order is the following:
\begin{enumerate}
- \item number literals, \(()\), \(@\), \(||\), \(round()\)
+ \item number literals, \(()\), \(@\), \(||\), \(round()\), \(rand()\)
\item \(!\)
\item \(\string^\)
\item \(-\) (the unary negation operator)
diff --git a/src/lending_iterator.rs b/src/lending_iterator.rs
@@ -0,0 +1,127 @@
+#![deny(
+ unsafe_code,
+ unused,
+ warnings,
+ clippy::all,
+ clippy::cargo,
+ clippy::nursery,
+ clippy::pedantic,
+ clippy::restriction
+)]
+#![allow(
+ clippy::arithmetic_side_effects,
+ clippy::blanket_clippy_restriction_lints,
+ clippy::implicit_return,
+ clippy::integer_arithmetic,
+ clippy::missing_trait_methods,
+ clippy::question_mark_used,
+ clippy::single_char_lifetime_names
+)]
+/// Generalizes [`Iterator`] by allowing one to yield references.
+pub trait LendingIterator {
+ /// Read [`Iterator::Item`].
+ type Item<'a>
+ where
+ Self: 'a;
+ /// Read [`Iterator::next`].
+ fn lend_next(&mut self) -> Option<Self::Item<'_>>;
+ /// Read [`Iterator::size_hint`].
+ #[inline]
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ (0, None)
+ }
+ /// Read [`Iterator::count`].
+ #[inline]
+ fn count(self) -> usize
+ where
+ Self: Sized,
+ {
+ self.lend_fold(0, |count, _| count + 1)
+ }
+ /// Read [`Iterator::advance_by`].
+
+ /// # Errors
+ ///
+ /// Read [`Iterator::advance_by`].
+ #[inline]
+ fn advance_by(&mut self, n: usize) -> Result<(), usize> {
+ for i in 0..n {
+ self.lend_next().ok_or(i)?;
+ }
+ Ok(())
+ }
+ /// Read [`Iterator::by_ref`].
+ #[inline]
+ fn by_ref(&mut self) -> &mut Self
+ where
+ Self: Sized,
+ {
+ self
+ }
+ /// Read [`Iterator::try_fold`].
+ /// # Errors
+ ///
+ /// Read [`Iterator::try_fold`].
+ #[inline]
+ fn lend_try_fold<B, E, F>(&mut self, init: B, mut f: F) -> Result<B, E>
+ where
+ Self: Sized,
+ F: FnMut(B, Self::Item<'_>) -> Result<B, E>,
+ {
+ let mut accum = init;
+ while let Some(x) = self.lend_next() {
+ accum = f(accum, x)?;
+ }
+ Ok(accum)
+ }
+ /// Read [`Iterator::fold`].
+ #[inline]
+ fn lend_fold<B, F>(mut self, init: B, mut f: F) -> B
+ where
+ Self: Sized,
+ F: FnMut(B, Self::Item<'_>) -> B,
+ {
+ let mut accum = init;
+ while let Some(x) = self.lend_next() {
+ accum = f(accum, x);
+ }
+ accum
+ }
+}
+impl<T> LendingIterator for T
+where
+ T: Iterator,
+{
+ type Item<'a> = T::Item where Self: 'a;
+ #[inline]
+ fn lend_next(&mut self) -> Option<Self::Item<'_>> {
+ self.next()
+ }
+ #[inline]
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ self.size_hint()
+ }
+ #[inline]
+ fn count(self) -> usize
+ where
+ Self: Sized,
+ {
+ self.count()
+ }
+ #[inline]
+ fn advance_by(&mut self, n: usize) -> Result<(), usize> {
+ for i in 0..n {
+ self.lend_next().ok_or(i)?;
+ }
+ Ok(())
+ }
+ #[inline]
+ fn by_ref(&mut self) -> &mut Self
+ where
+ Self: Sized,
+ {
+ self
+ }
+ // Due to a bug in GATs, fold and try_fold cannot be
+ // implemented.
+}
diff --git a/src/lib.rs b/src/lib.rs
@@ -1,7 +1,108 @@
-//! This crate contains logic for an application to parse and evaluate
-//! basic rational number arithmetic using standard operator precedence and
-//! associativity.
+//! # `calc_lib`
+//! `calc_lib` is a library for basic rational number arithmetic using standard
+//! operator precedence and associativity. Internally, it is
+//! based on [`Ratio<T>`](https://docs.rs/num/latest/num/rational/struct.Ratio.html)
+//! and [`BigInt`](https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html).
+//!
+//! ## Expressions
+//!
+//! The following are the list of expressions in descending order of precedence:
+//! 1. number literals, `@`, `()`, `||`, `round()`, `rand()`
+//! 2. `!`
+//! 3. `^`
+//! 4. `-` (unary negation operator)
+//! 5. `*`, `/`
+//! 6. `+`, `-`
+//!
+//! All binary operators are left-associative sans `^` which is right-associative.
+//!
+//! Any expression is allowed to be enclosed in `()`. Note that parentheses are purely for grouping expressions;
+//! in particular, you cannot use them to represent multiplication (e.g., `4(2)` is grammatically incorrect and
+//! will result in an error message).
+//!
+//! Any expression is allowed to be enclosed in `||`. This unary operator represents absolute value.
+//!
+//! `!` is the factorial operator. Due to its high precedence, something like *-i!^j!* for *i, j ∈ ℕ* is
+//! the same thing as *-((i!)^(j!))*. If the expression preceding it does not evaluate to a non-negative integer,
+//! then an error will be displayed. Spaces and tabs are *not* ignored; so `1 !` is grammatically incorrect and
+//! will result in an error message.
+//!
+//! `^` is the exponentiation operator. The expression left of the operator can evaluate to any rational number;
+//! however the expression right of the operator must evaluate to an integer unless the expression on the left
+//! evaluates to `0` or `1`. In the event of the former, the expression right of the operator must evaluate to a
+//! non-negative rational number. In the event of the latter, the expression right of the operator can evaluate to
+//! any rational number. Note that `0^0` is defined to be 1.
+//!
+//! The unary operator `-` represents negation.
+//!
+//! The operators `*` and `/` represent multiplication and division respectively. Expressions right of `/`
+//! must evaluate to any non-zero rational number; otherwise an error will be displayed.
+//!
+//! The binary operators `+` and `-` represent addition and subtraction respectively.
+//!
+//! With the aforementioned exception of `!`, all spaces and tabs before and after operators are ignored.
+//!
+//! ## Round expression
+//!
+//! `round(expression, digit)` rounds `expression` to `digit`-number of fractional digits. An error will
+//! be displayed if called incorrectly.
+//!
+//! ## Rand expression
+//!
+//! `rand(expression, expression)` generates a random 64-integer inclusively between the passed expressions.
+//! An error will be displayed if called incorrectly. `rand()` generates a random 64-bit integer.
+//!
+//! ## Numbers
+//!
+//! A number literal is a non-empty sequence of digits or a non-empty sequence of digits immediately followed by `.`
+//! which is immediately followed by a non-empty sequence of digits (e.g., `134.901`). This means that number
+//! literals represent precisely all possible ratios of non-negative integers to non-negative integers who sole
+//! prime factors are 2 or 5. To represent all other rational numbers, the unary operator `-` and binary
+//! operator `/` must be used.
+//!
+//! ## Empty expression
+//!
+//! The empty expression (i.e., expression that at most only consists of spaces and tabs) will return
+//! the result from the previous non-empty and non-store expression in *decimal* form using the minimum number of digits.
+//! In the event an infinite number of digits is required, it will be rounded to 9 fractional digits using normal rounding
+//! rules first.
+//!
+//! ## Store expression
+//!
+//! To store the result of the previous non-empty and non-store expression, one simply passes `s`. In addition to storing the
+//! result which will subsequently be available via `@`, it displays the result. At most 8 results can be stored at once;
+//! at which point, results that are stored overwrite the oldest result.
+//!
+//! ## Recall expression
+//!
+//! `@` is used to recall previously stored results. It can be followed by any *digit* from `1` to `8`.
+//! If such a digit does not immediately follow it, then it will be interpreted as if there were a `1`.
+//! `@i` returns the *i*-th most-previous stored result where *i ∈ {1, 2, 3, 4, 5, 6, 7, 8}*.
+//! Note that spaces and tabs are *not* ignored so `@ 2` is grammatically incorrect and will result in an error message.
+//! As emphasized, it does not work on expressions; so both `@@` and `@(1)` are grammatically incorrect.
+//!
+//! ## Character encoding
+//!
+//! All inputs must only contain the ASCII encoding of the following Unicode scalar values: `0`-`9`, `.`, `+`, `-`,
+//! `*`, `/`, `^`, `!`, `|`, `(`, `)`, `round`, `rand`, `,`, `@`, `s`, <space>, <tab>, <line feed>, <carriage return>,
+//! and `q`. Any other byte sequences are grammatically incorrect and will lead to an error message.
+//!
+//! ## Errors
+//!
+//! Errors due to a language violation (e.g., dividing by `0`) manifest into an error message. `panic!`s
+//! and [`io::Error`](https://doc.rust-lang.org/std/io/struct.Error.html)s caused by writing to the global
+//! standard output stream lead to program abortion.
+//!
+//! ## Exiting
+//!
+//! `q` with any number of spaces and tabs before and after will cause the program to terminate.
+//!
+//! ### Formal language specification
+//!
+//! For a more precise specification of the “calc language”, one can read the
+//! [calc language specification](https://git.philomathiclife.com/calc_rational/lang.pdf).
#![no_std]
+#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(
unsafe_code,
unused,
@@ -19,7 +120,8 @@
clippy::implicit_return,
clippy::integer_arithmetic,
clippy::into_iter_on_ref,
- clippy::question_mark,
+ clippy::missing_trait_methods,
+ clippy::question_mark_used,
clippy::single_char_lifetime_names,
clippy::unseparated_literal_suffix
)]
@@ -34,10 +136,13 @@ use num_bigint::{BigInt, BigUint, Sign};
use num_integer::Integer;
use num_rational::Ratio;
use num_traits::Pow;
-use E::{
+#[cfg(feature = "rand")]
+use num_traits::ToPrimitive;
+#[cfg(feature = "rand")]
+use rand::{rngs::ThreadRng, RngCore};
+use LangErr::{
DivByZero, ExpDivByZero, ExpIsNotInt, InvalidAbs, InvalidDec, InvalidPar, InvalidQuit,
- InvalidRound, InvalidStore, MissingLF, MissingTerm, NotEnoughPrevResults, NotNonNegIntFact,
- TrailingSyms,
+ InvalidRound, InvalidStore, MissingTerm, NotEnoughPrevResults, NotNonNegIntFact, TrailingSyms,
};
use O::{Empty, Eval, Exit, Store};
/// Fixed-sized cache that automatically overwrites the oldest data
@@ -45,12 +150,12 @@ use O::{Empty, Eval, Exit, Store};
/// [`Cache`] as a very limited but more performant [`VecDeque`][alloc::collections::VecDeque] that only
/// adds new data or reads old data.
pub mod cache;
-/// The error that is returned when attempting
-/// to evaluate the input.
-#[derive(Clone, Copy, Debug)]
-pub enum E {
- /// The input was not terminated with a line feed.
- MissingLF,
+/// Generalizes [`Iterator`] by using
+/// generic associated types.
+pub mod lending_iterator;
+/// Error due to a language violation.
+#[derive(Debug)]
+pub enum LangErr {
/// The input began with a `q` but had non-whitespace
/// that followed it.
InvalidQuit,
@@ -92,12 +197,27 @@ pub enum E {
/// The input started with a valid expression but was immediately followed
/// by symbols that could not be chained with the preceding expression.
TrailingSyms(usize),
+ /// The input contained an invalid random expression.
+ #[cfg(feature = "rand")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
+ InvalidRand(usize),
+ /// Error when attempting to generate a random number.
+ #[cfg(feature = "rand")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
+ RandErr(rand::Error),
+ /// Error when the second argument is less than first in the rand function.
+ #[cfg(feature = "rand")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
+ RandInvalidArgs(usize),
+ /// Error when there are no 64-bit integers in the interval passed to the random function.
+ #[cfg(feature = "rand")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
+ RandNoInts(usize),
}
-impl Display for E {
+impl Display for LangErr {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
- MissingLF => f.write_str("The last character is not a line feed."),
InvalidStore => f.write_str("Invalid store expression. A store expression must be of the extended regex form: ^[ \\t]*s[ \\t]*$."),
InvalidQuit => f.write_str("Invalid quit expression. A quit expression must be of the extended regex form: ^[ \\t]*q[ \\t]*$."),
DivByZero(u) => write!(f, "Division by zero ending at position {u}."),
@@ -109,8 +229,19 @@ impl Display for E {
InvalidAbs(u) => write!(f, "Invalid absolute value expression ending at position {u}. An absolute value expression is an addition expression enclosed in '||'."),
InvalidPar(u) => write!(f, "Invalid parenthetical expression ending at position {u}. A parenthetical expression is an addition expression enclosed in '()'."),
InvalidRound(u) => write!(f, "Invalid round expression ending at position {u}. A round expression is of the form 'round(<add expression>, digit)'"),
- MissingTerm(u) => write!(f, "Missing terminal expression at position {u}. A terminal expression is a decimal literal expression, recall expression, absolute value expression, parenthetical expression, or a round expression."),
+ #[cfg(not(feature = "rand"))]
+ MissingTerm(u) => write!(f, "Missing terminal expression at position {u}. A terminal expression is a decimal literal expression, recall expression, absolute value expression, parenthetical expression, or round expression."),
+ #[cfg(feature = "rand")]
+ MissingTerm(u) => write!(f, "Missing terminal expression at position {u}. A terminal expression is a decimal literal expression, recall expression, absolute value expression, parenthetical expression, round expression, or rand expression."),
TrailingSyms(u) => write!(f, "Trailing symbols starting at position {u}."),
+ #[cfg(feature = "rand")]
+ Self::InvalidRand(u) => write!(f, "Invalid rand expression ending at position {u}. A rand expression is of the form 'rand()' or 'rand(<add expression>, <add expression>)'."),
+ #[cfg(feature = "rand")]
+ Self::RandErr(ref e) => e.fmt(f),
+ #[cfg(feature = "rand")]
+ Self::RandInvalidArgs(u) => write!(f, "The second expression passed to the random function evaluated to rational number less than the first ending at position {u}."),
+ #[cfg(feature = "rand")]
+ Self::RandNoInts(u) => write!(f, "There are no 64-bit integers within the interval passed to the random function ending at position {u}."),
}
}
}
@@ -250,8 +381,8 @@ pub struct Evaluator<'input, 'cache, 'prev, 'exp> {
exp_cache: &'exp mut Vec<Ratio<BigInt>>,
}
impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
- /// Creates an `Evaluator<'input, 'cache, 'prev, 'exp>` based on the supplied arguments.
#[inline]
+ /// Creates an `Evaluator<'input, 'cache, 'prev, 'exp>` based on the supplied arguments.
pub fn new(
utf8: &'input [u8],
cache: &'cache mut Cache<Ratio<BigInt>, 8>,
@@ -267,15 +398,17 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
}
}
/// Evaluates the input consuming the `Evaluator<'input, 'cache, 'exp>`.
+ /// Requires the input to contain one expression (i.e., if there are
+ /// multiple newlines, it will error).
/// # Errors
///
- /// Returns an [`E`] iff the input violates the calc language.
+ /// Returns a [`LangErr`] iff the input violates the calc language.
#[inline]
- #[allow(unsafe_code, clippy::indexing_slicing)]
- pub fn evaluate(mut self) -> Result<O<'prev>, E> {
+ #[allow(clippy::indexing_slicing)]
+ pub fn evaluate(mut self) -> Result<O<'prev>, LangErr> {
self.utf8 = if self.utf8.last().map_or(true, |b| *b != b'\n') {
- return Err(MissingLF);
+ self.utf8
} else {
&self.utf8[..self.utf8.len()
- self
@@ -310,11 +443,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
self.get_adds().and_then(move |val| {
self.consume_ws();
if self.i == self.utf8.len() {
- *self.prev = Some(val);
- // SAFETY:
- // We know this is not None since we assign the value
- // directly above.
- Ok(Eval(unsafe { self.prev.as_ref().unwrap_unchecked() }))
+ Ok(Eval(self.prev.insert(val)))
} else {
Err(TrailingSyms(self.i))
}
@@ -340,7 +469,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
/// This function is used for both addition and subtraction operations which
/// themselves are based on multiplication expressions.
#[inline]
- fn get_adds(&mut self) -> Result<Ratio<BigInt>, E> {
+ fn get_adds(&mut self) -> Result<Ratio<BigInt>, LangErr> {
let mut left = self.get_mults()?;
let mut j;
self.consume_ws();
@@ -365,7 +494,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
/// This function is used for both multiplication and division operations which
/// themselves are based on negation expressions.
#[inline]
- fn get_mults(&mut self) -> Result<Ratio<BigInt>, E> {
+ fn get_mults(&mut self) -> Result<Ratio<BigInt>, LangErr> {
let mut left = self.get_neg()?;
let mut right;
let mut j;
@@ -394,7 +523,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
/// Evaluates negation expressions as defined in the calc language.
/// This function is based on exponentiation expressions.
#[inline]
- fn get_neg(&mut self) -> Result<Ratio<BigInt>, E> {
+ fn get_neg(&mut self) -> Result<Ratio<BigInt>, LangErr> {
let mut count = 0usize;
while let Some(b) = self.utf8.get(self.i) {
if *b == b'-' {
@@ -412,7 +541,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
/// This function is based on negation expressions.
#[inline]
#[allow(clippy::unwrap_used, clippy::unwrap_in_result)]
- fn get_exps(&mut self) -> Result<Ratio<BigInt>, E> {
+ fn get_exps(&mut self) -> Result<Ratio<BigInt>, LangErr> {
let mut t = self.get_fact()?;
let ix = self.exp_cache.len();
let mut prev;
@@ -471,7 +600,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
/// Evaluates factorial expressions as defined in the calc language.
/// This function is based on terminal expressions.
#[inline]
- fn get_fact(&mut self) -> Result<Ratio<BigInt>, E> {
+ fn get_fact(&mut self) -> Result<Ratio<BigInt>, LangErr> {
/// Calculates the factorial of `val`.
#[inline]
fn fact(mut val: BigUint) -> BigUint {
@@ -524,7 +653,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
/// recall expressions, absolute value expressions, and round expressions.
#[inline]
#[allow(clippy::option_if_let_else)]
- fn get_term(&mut self) -> Result<Ratio<BigInt>, E> {
+ fn get_term(&mut self) -> Result<Ratio<BigInt>, LangErr> {
match self.get_rational() {
Ok(o) => match o {
Some(r) => Ok(r),
@@ -534,9 +663,20 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
None => match self.get_recall() {
Ok(o3) => match o3 {
Some(r3) => Ok(r3.clone()),
- None => self
- .get_abs()
- .and_then(|o4| o4.map_or_else(|| self.get_round(), Ok)),
+ None => match self.get_abs() {
+ Ok(o4) => match o4 {
+ Some(r4) => Ok(r4),
+ #[cfg(feature = "rand")]
+ None => self
+ .get_round()
+ .and_then(|o5| o5.map_or_else(|| self.get_rand(), Ok)),
+ #[cfg(not(feature = "rand"))]
+ None => self.get_round().and_then(|o5| {
+ o5.map_or_else(|| Err(MissingTerm(self.i)), Ok)
+ }),
+ },
+ Err(e4) => Err(e4),
+ },
},
Err(e3) => Err(e3),
},
@@ -547,18 +687,138 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
Err(e) => Err(e),
}
}
- /// Rounds a value to the specified number of fractional digits.
+ /// Generates a random 64-bit integer.
/// This function is based on addition expressions.
/// This is the last terminal expression attempted when needing
/// a terminal expression; as a result, it is the only terminal expression
/// that does not return an `Option`.
+ #[cfg(feature = "rand")]
#[inline]
- fn get_round(&mut self) -> Result<Ratio<BigInt>, E> {
+ fn get_rand(&mut self) -> Result<Ratio<BigInt>, LangErr> {
+ /// Generates a random 64-bit integer.
+ #[inline]
+ fn rand(rng: &mut ThreadRng) -> Result<i64, rand::Error> {
+ let mut bytes = [0u8; 8];
+ rng.try_fill_bytes(&mut bytes)
+ .map(|_| i64::from_ne_bytes(bytes))
+ }
+ /// Generates a random 64-bit integer inclusively between the passed arguments.
+ #[inline]
+ #[allow(
+ clippy::as_conversions,
+ clippy::cast_lossless,
+ clippy::cast_possible_truncation,
+ clippy::cast_possible_wrap,
+ clippy::cast_sign_loss,
+ clippy::needless_pass_by_value
+ )]
+ fn rand_range(
+ rng: &mut ThreadRng,
+ lower: Ratio<BigInt>,
+ upper: Ratio<BigInt>,
+ i: usize,
+ ) -> Result<i64, LangErr> {
+ if lower > upper {
+ return Err(LangErr::RandInvalidArgs(i));
+ }
+ let lo = lower.ceil();
+ let up = upper.floor();
+ let lo_int = lo.numer();
+ let up_int = up.numer();
+ if lo_int > &BigInt::from(i64::MAX) || up_int < &BigInt::from(i64::MIN) {
+ return Err(LangErr::RandNoInts(i));
+ }
+ let lo_min = lo_int.to_i64().unwrap_or(i64::MIN);
+ let up_max = up_int.to_i64().unwrap_or(i64::MAX);
+ if up_max > lo_min || upper.is_integer() || lower.is_integer() {
+ let low = lo_min as i128;
+ // range is [1, 2^64].
+ let modulus = (up_max as i128 - low + 1) as u128;
+ // range is [0, i64::MAX].
+ // rem represents how many values need to be removed
+ // when generating a random i64 in order for uniformity.
+ let rem = (0x0001_0000_0000_0000_0000 % modulus) as u64;
+ let mut low_adj;
+ loop {
+ match rand(rng) {
+ Ok(v) => {
+ low_adj = v as u64;
+ // Since rem is in [0, i64::MAX],
+ // this is the same as low_adj < 0
+ // || low_adj >= rem.
+ if low_adj >= rem {
+ return Ok(
+ // range is [i64::MIN, i64::MAX]; thus casts are safe.
+ // modulus is up_max - low + 1; so as low grows,
+ // % shrinks by the same factor. i64::MAX happens
+ // when low = up_max = i64::MAX or when low = 0,
+ // up_max = i64::MAX and low_adj is i64::MAX.
+ ((low_adj as u128 % modulus) as i128 + low) as i64,
+ );
+ }
+ }
+ Err(e) => return Err(LangErr::RandErr(e)),
+ }
+ }
+ } else {
+ Err(LangErr::RandNoInts(i))
+ }
+ }
// This is the last kind of terminal expression that is attempted.
// If there is no more data, then we have a missing terminal expression.
- let Some(b) = self.utf8.get(self.i..self.i.saturating_add(6)) else {
+ let Some(b) = self.utf8.get(self.i..self.i.saturating_add(5)) else {
return Err(MissingTerm(self.i))
};
+ if b == b"rand(" {
+ self.i += 5;
+ self.consume_ws();
+ let i = self.i;
+ self.utf8.get(self.i).map_or_else(
+ || Err(LangErr::InvalidRand(i)),
+ |p| {
+ if *p == b')' {
+ self.i += 1;
+ rand(&mut rand::thread_rng()).map_or_else(
+ |e| Err(LangErr::RandErr(e)),
+ |v| Ok(Ratio::from_integer(BigInt::from(v))),
+ )
+ } else {
+ let add = self.get_adds()?;
+ let Some(b2) = self.utf8.get(self.i) else {
+ return Err(LangErr::InvalidRand(self.i))
+ };
+ if *b2 == b',' {
+ self.i += 1;
+ self.consume_ws();
+ let add2 = self.get_adds()?;
+ self.consume_ws();
+ let Some(b3) = self.utf8.get(self.i) else {
+ return Err(LangErr::InvalidRand(self.i))
+ };
+ if *b3 == b')' {
+ self.i += 1;
+ rand_range(&mut rand::thread_rng(), add, add2, self.i)
+ .map(|v| Ratio::from_integer(BigInt::from(v)))
+ } else {
+ Err(LangErr::InvalidRand(self.i))
+ }
+ } else {
+ Err(LangErr::InvalidRand(self.i))
+ }
+ }
+ },
+ )
+ } else {
+ Err(MissingTerm(self.i))
+ }
+ }
+ /// Rounds a value to the specified number of fractional digits.
+ /// This function is based on addition expressions.
+ #[inline]
+ fn get_round(&mut self) -> Result<Option<Ratio<BigInt>>, LangErr> {
+ let Some(b) = self.utf8.get(self.i..self.i.saturating_add(6)) else {
+ return Ok(None)
+ };
if b == b"round(" {
self.i += 6;
self.consume_ws();
@@ -589,7 +849,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
self.i += 1;
let mult =
BigInt::from_biguint(Sign::Plus, BigUint::new(vec![10])).pow(r);
- Ok((val * &mult).round() / &mult)
+ Ok(Some((val * &mult).round() / &mult))
} else {
Err(InvalidRound(self.i))
}
@@ -599,13 +859,13 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
Err(InvalidRound(self.i))
}
} else {
- Err(MissingTerm(self.i))
+ Ok(None)
}
}
/// Evaluates absolute value expressions as defined in the calc language.
/// This function is based on addition expressions.
#[inline]
- fn get_abs(&mut self) -> Result<Option<Ratio<BigInt>>, E> {
+ fn get_abs(&mut self) -> Result<Option<Ratio<BigInt>>, LangErr> {
let Some(b) = self.utf8.get(self.i) else {
return Ok(None)
};
@@ -635,7 +895,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
/// Evaluates recall expressions as defined in the calc language.
#[inline]
#[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
- fn get_recall(&mut self) -> Result<Option<&Ratio<BigInt>>, E> {
+ fn get_recall(&mut self) -> Result<Option<&Ratio<BigInt>>, LangErr> {
let Some(b) = self.utf8.get(self.i) else {
return Ok(None)
};
@@ -661,7 +921,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
/// Evaluates parenthetical expressions as defined in the calc language.
/// This function is based on addition expressions.
#[inline]
- fn get_par(&mut self) -> Result<Option<Ratio<BigInt>>, E> {
+ fn get_par(&mut self) -> Result<Option<Ratio<BigInt>>, LangErr> {
let Some(b) = self.utf8.get(self.i) else {
return Ok(None)
};
@@ -691,7 +951,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
clippy::cast_lossless,
clippy::indexing_slicing
)]
- fn get_rational(&mut self) -> Result<Option<Ratio<BigInt>>, E> {
+ fn get_rational(&mut self) -> Result<Option<Ratio<BigInt>>, LangErr> {
#[inline]
// ControlFlow makes more sense to use in try_fold; however due to a lack
// of a map_or_else function, it is easier to simply return a Result with
@@ -745,8 +1005,126 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> {
}
}
}
+/// Reads data from `R` passing each line to an [`Evaluator`] to be evaluated.
+#[allow(unused)]
+pub struct EvalIter<R> {
+ /// Reader that contains input data.
+ reader: R,
+ /// Buffer that is used by `reader` to read
+ /// data into.
+ input_buffer: Vec<u8>,
+ /// Cache of stored results.
+ cache: Cache<Ratio<BigInt>, 8>,
+ /// Result of the previous expression.
+ prev: Option<Ratio<BigInt>>,
+ /// Buffer used by [`Evaluator`] to process
+ /// sub-expressions.
+ exp_buffer: Vec<Ratio<BigInt>>,
+ /// Random number generator.
+ #[cfg(feature = "rand")]
+ rng: ThreadRng,
+}
+impl<R> EvalIter<R> {
+ /// Creates a new `EvalIter`.
+ #[inline]
+ #[cfg(feature = "rand")]
+ pub fn new(reader: R) -> Self {
+ Self {
+ reader,
+ input_buffer: Vec::new(),
+ cache: Cache::new(),
+ prev: None,
+ exp_buffer: Vec::new(),
+ rng: rand::thread_rng(),
+ }
+ }
+ /// Creates a new `EvalIter`.
+ #[inline]
+ #[cfg(not(feature = "rand"))]
+ pub fn new(reader: R) -> Self {
+ Self {
+ reader,
+ input_buffer: Vec::new(),
+ cache: Cache::new(),
+ prev: None,
+ exp_buffer: Vec::new(),
+ }
+ }
+}
+#[cfg(feature = "std")]
+extern crate std;
+#[cfg(feature = "std")]
+use std::io::{BufRead, Error};
+#[cfg(feature = "std")]
+#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
+/// Error returned from [`EvalIter`] when
+/// an expression has an error.
+#[derive(Debug)]
+pub enum E {
+ /// Error containing [`Error`] which is returned
+ /// from [`EvalIter`] when reading from the supplied
+ /// [`BufRead`]er.
+ Error(Error),
+ /// Error containing [`LangErr`] which is returned
+ /// from [`EvalIter`] when evaluating a single expression.
+ LangErr(LangErr),
+}
+#[cfg(feature = "std")]
+impl Display for E {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Error(ref e) => e.fmt(f),
+ Self::LangErr(ref e) => e.fmt(f),
+ }
+ }
+}
+#[cfg(feature = "std")]
+use crate::lending_iterator::LendingIterator;
+#[cfg(feature = "std")]
+#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
+impl<R> LendingIterator for EvalIter<R>
+where
+ R: BufRead,
+{
+ type Item<'a> = Result<O<'a>, E> where Self: 'a;
+ #[inline]
+ fn lend_next(&mut self) -> Option<Result<O<'_>, E>> {
+ self.input_buffer.clear();
+ self.exp_buffer.clear();
+ self.reader
+ .read_until(b'\n', &mut self.input_buffer)
+ .map_or_else(
+ |e| Some(Err(E::Error(e))),
+ |c| {
+ if c == 0 {
+ None
+ } else {
+ Evaluator::new(
+ self.input_buffer.as_slice(),
+ &mut self.cache,
+ &mut self.prev,
+ &mut self.exp_buffer,
+ )
+ .evaluate()
+ .map_or_else(
+ |e| Some(Err(E::LangErr(e))),
+ |o| match o {
+ O::Empty(_) | O::Eval(_) | O::Store(_) => Some(Ok(o)),
+ O::Exit => None,
+ },
+ )
+ }
+ },
+ )
+ }
+}
#[cfg(test)]
mod tests {
+ use super::LangErr::{
+ DivByZero, ExpDivByZero, ExpIsNotInt, InvalidAbs, InvalidDec, InvalidPar, InvalidQuit,
+ InvalidRound, MissingTerm, NotEnoughPrevResults, NotNonNegIntFact, TrailingSyms,
+ };
use super::*;
#[test]
fn empty() {
@@ -851,13 +1229,19 @@ mod tests {
== "> -20.3"
);
assert!(
- match Evaluator::new(b" ", &mut Cache::new(), &mut None, &mut Vec::new())
- .evaluate()
- .unwrap_err()
- {
- E::MissingLF => true,
- _ => false,
- }
+ Evaluator::new(
+ &[0u8; 0],
+ &mut Cache::new(),
+ &mut Some(Ratio::new(
+ BigInt::from_biguint(Sign::Minus, BigUint::new(vec![203])),
+ BigInt::from_biguint(Sign::Plus, BigUint::new(vec![10]))
+ )),
+ &mut Vec::new()
+ )
+ .evaluate()
+ .unwrap()
+ .to_string()
+ == "> -20.3"
);
}
#[test]
@@ -936,7 +1320,7 @@ mod tests {
.get_rational()
.unwrap_err()
{
- E::InvalidDec(i) => i == 2,
+ InvalidDec(i) => i == 2,
_ => false,
}
);
@@ -947,7 +1331,7 @@ mod tests {
.get_rational()
.unwrap_err()
{
- E::InvalidDec(i) => i == 2,
+ InvalidDec(i) => i == 2,
_ => false,
}
);
@@ -1005,7 +1389,7 @@ mod tests {
.get_par()
.unwrap_err()
{
- E::InvalidPar(i) => i == 2,
+ InvalidPar(i) => i == 2,
_ => false,
}
);
@@ -1014,7 +1398,7 @@ mod tests {
.get_par()
.unwrap_err()
{
- E::InvalidPar(i) => i == 9,
+ InvalidPar(i) => i == 9,
_ => false,
}
);
@@ -1081,7 +1465,7 @@ mod tests {
.get_recall()
.unwrap_err()
{
- E::NotEnoughPrevResults(count) => count == 0,
+ NotEnoughPrevResults(count) => count == 0,
_ => false,
}
);
@@ -1091,7 +1475,7 @@ mod tests {
.get_recall()
.unwrap_err()
{
- E::NotEnoughPrevResults(count) => count == 0,
+ NotEnoughPrevResults(count) => count == 0,
_ => false,
}
);
@@ -1102,7 +1486,7 @@ mod tests {
.get_recall()
.unwrap_err()
{
- E::NotEnoughPrevResults(count) => count == 0,
+ NotEnoughPrevResults(count) => count == 0,
_ => false,
}
);
@@ -1176,7 +1560,7 @@ mod tests {
.get_recall()
.unwrap_err()
{
- E::NotEnoughPrevResults(count) => count == 1,
+ NotEnoughPrevResults(count) => count == 1,
_ => false,
}
);
@@ -1207,7 +1591,7 @@ mod tests {
.get_recall()
.unwrap_err()
{
- E::NotEnoughPrevResults(count) => count == 2,
+ NotEnoughPrevResults(count) => count == 2,
_ => false,
}
);
@@ -1273,7 +1657,7 @@ mod tests {
.get_abs()
.unwrap_err()
{
- E::InvalidAbs(i) => i == 2,
+ InvalidAbs(i) => i == 2,
_ => false,
}
);
@@ -1282,7 +1666,7 @@ mod tests {
.get_abs()
.unwrap_err()
{
- E::InvalidAbs(i) => i == 8,
+ InvalidAbs(i) => i == 8,
_ => false,
}
);
@@ -1336,7 +1720,7 @@ mod tests {
.get_round()
.unwrap_err()
{
- E::InvalidRound(i) => i == 7,
+ InvalidRound(i) => i == 7,
_ => false,
}
);
@@ -1345,7 +1729,7 @@ mod tests {
.get_round()
.unwrap_err()
{
- E::InvalidRound(i) => i == 8,
+ InvalidRound(i) => i == 8,
_ => false,
}
);
@@ -1354,7 +1738,7 @@ mod tests {
.get_round()
.unwrap_err()
{
- E::InvalidRound(i) => i == 9,
+ InvalidRound(i) => i == 9,
_ => false,
}
);
@@ -1367,7 +1751,7 @@ mod tests {
.get_round()
.unwrap_err()
{
- E::InvalidRound(i) => i == 9,
+ InvalidRound(i) => i == 9,
_ => false,
});
assert!(
@@ -1375,23 +1759,10 @@ mod tests {
.get_round()
.unwrap_err()
{
- E::InvalidRound(i) => i == 8,
+ InvalidRound(i) => i == 8,
_ => false,
}
);
- // If the input does not start with 'round(', then it's invalid since get_round must only be called as the last terminal expression which means whitespace must be consumed already.
- assert!(match Evaluator::new(
- b" round(2/3,2)",
- &mut Cache::new(),
- &mut None,
- &mut Vec::new()
- )
- .get_round()
- .unwrap_err()
- {
- E::MissingTerm(i) => i == 0,
- _ => false,
- });
assert!(
Evaluator::new(
b"round(2, 7)",
@@ -1401,7 +1772,10 @@ mod tests {
)
.get_round()
.unwrap()
- == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2])))
+ == Some(Ratio::from_integer(BigInt::from_biguint(
+ Sign::Plus,
+ BigUint::new(vec![2])
+ )))
);
assert!(
Evaluator::new(
@@ -1412,11 +1786,141 @@ mod tests {
)
.get_round()
.unwrap()
- == Ratio::new(
+ == Some(Ratio::new(
BigInt::from_biguint(Sign::Plus, BigUint::new(vec![27])),
BigInt::from_biguint(Sign::Plus, BigUint::new(vec![10]))
- )
+ ))
+ );
+ }
+ #[test]
+ #[cfg(feature = "rand")]
+ fn rand() {
+ assert!(
+ match Evaluator::new(b"rand(1", &mut Cache::new(), &mut None, &mut Vec::new())
+ .get_rand()
+ .unwrap_err()
+ {
+ LangErr::InvalidRand(i) => i == 6,
+ _ => false,
+ }
+ );
+ assert!(
+ match Evaluator::new(b"rand(1,2", &mut Cache::new(), &mut None, &mut Vec::new())
+ .get_rand()
+ .unwrap_err()
+ {
+ LangErr::InvalidRand(i) => i == 8,
+ _ => false,
+ }
);
+ assert!(match Evaluator::new(
+ b"rand(1/2,3/4)",
+ &mut Cache::new(),
+ &mut None,
+ &mut Vec::new()
+ )
+ .get_rand()
+ .unwrap_err()
+ {
+ LangErr::RandNoInts(i) => i == 13,
+ _ => false,
+ });
+ assert!(match Evaluator::new(
+ b"rand(-100000000000000000000000,-1000000000000000000000)",
+ &mut Cache::new(),
+ &mut None,
+ &mut Vec::new()
+ )
+ .get_rand()
+ .unwrap_err()
+ {
+ LangErr::RandNoInts(i) => i == 55,
+ _ => false,
+ });
+ assert!(match Evaluator::new(
+ b"rand(2/3,1/3)",
+ &mut Cache::new(),
+ &mut None,
+ &mut Vec::new()
+ )
+ .get_rand()
+ .unwrap_err()
+ {
+ LangErr::RandInvalidArgs(i) => i == 13,
+ _ => false,
+ });
+ // If the input does not start with 'rand(', then it's invalid since get_rand must only be called as the last terminal expression which means whitespace must be consumed already.
+ assert!(match Evaluator::new(
+ b" rand(2/3,2)",
+ &mut Cache::new(),
+ &mut None,
+ &mut Vec::new()
+ )
+ .get_rand()
+ .unwrap_err()
+ {
+ MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ assert!(
+ Evaluator::new(b"rand(2, 7)", &mut Cache::new(), &mut None, &mut Vec::new())
+ .get_rand()
+ .map(|r| {
+ let int = r.numer();
+ int >= &BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))
+ && *int <= BigInt::from_biguint(Sign::Plus, BigUint::new(vec![7]))
+ })
+ .unwrap()
+ );
+ assert!(
+ Evaluator::new(b"rand()", &mut Cache::new(), &mut None, &mut Vec::new())
+ .get_rand()
+ .map(|r| {
+ let int = r.numer();
+ int >= &BigInt::from(i64::MIN) && *int <= BigInt::from(i64::MAX)
+ })
+ .unwrap()
+ );
+ for _ in 0..100 {
+ assert!(
+ Evaluator::new(b"rand(2, 2)", &mut Cache::new(), &mut None, &mut Vec::new())
+ .get_rand()
+ .map(|r| *r.numer() == BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2])))
+ .unwrap()
+ );
+ }
+ }
+ #[cfg(feature = "rand")]
+ #[test]
+ #[ignore]
+ fn rand_uni() {
+ // Test rand on an interval that is not a power of 2 in size.
+ // This causes rand to adjust the interval to enforce uniformity.
+ let mut vals = [0u32; 3];
+ let mut vec = Vec::new();
+ let mut cache = Cache::new();
+ let mut none = None;
+ const COUNT: u32 = 999999;
+ for _ in 1..COUNT {
+ vals[(Evaluator::new(b"rand(-1, 1)", &mut cache, &mut none, &mut vec)
+ .get_rand()
+ .unwrap()
+ .numer()
+ .to_i32()
+ .unwrap()
+ + 1) as usize] += 1;
+ }
+ // Test that the distribution is within 1% of what is expected.
+ assert!(vals
+ .into_iter()
+ .try_fold(false, |_, r| {
+ if r < COUNT * 33 / 100 || r > COUNT * 101 / 300 {
+ Err(false)
+ } else {
+ Ok(true)
+ }
+ })
+ .unwrap());
}
#[test]
fn term() {
@@ -1449,6 +1953,31 @@ mod tests {
BigInt::from_biguint(Sign::Plus, BigUint::new(vec![100]))
)
);
+ #[cfg(feature = "rand")]
+ assert!(
+ Evaluator::new(b"rand()", &mut Cache::new(), &mut None, &mut Vec::new())
+ .get_term()
+ .is_ok()
+ );
+ #[cfg(feature = "rand")]
+ assert!(Evaluator::new(
+ b"rand(-13/93, 833)",
+ &mut Cache::new(),
+ &mut None,
+ &mut Vec::new()
+ )
+ .get_term()
+ .is_ok());
+ #[cfg(not(feature = "rand"))]
+ assert!(
+ match Evaluator::new(b"rand()", &mut Cache::new(), &mut None, &mut Vec::new())
+ .get_term()
+ .unwrap_err()
+ {
+ MissingTerm(i) => i == 0,
+ _ => false,
+ }
+ );
assert!(
Evaluator::new(b"|4|", &mut Cache::new(), &mut None, &mut Vec::new())
.get_term()
@@ -1461,7 +1990,7 @@ mod tests {
.get_term()
.unwrap_err()
{
- E::MissingTerm(i) => i == 0,
+ MissingTerm(i) => i == 0,
_ => false,
}
);
@@ -1488,7 +2017,7 @@ mod tests {
.get_fact()
.unwrap_err()
{
- E::NotNonNegIntFact(i) => i == 5,
+ NotNonNegIntFact(i) => i == 5,
_ => false,
}
);
@@ -1498,7 +2027,7 @@ mod tests {
.get_fact()
.unwrap_err()
{
- E::NotNonNegIntFact(i) => i == 4,
+ NotNonNegIntFact(i) => i == 4,
_ => false,
}
);
@@ -1582,7 +2111,7 @@ mod tests {
.get_fact()
.unwrap_err()
{
- E::MissingTerm(i) => i == 0,
+ MissingTerm(i) => i == 0,
_ => false,
}
);
@@ -1591,7 +2120,7 @@ mod tests {
.get_fact()
.unwrap_err()
{
- E::MissingTerm(i) => i == 0,
+ MissingTerm(i) => i == 0,
_ => false,
}
);
@@ -1601,7 +2130,7 @@ mod tests {
.get_fact()
.unwrap_err()
{
- E::MissingTerm(i) => i == 0,
+ MissingTerm(i) => i == 0,
_ => false,
}
);
@@ -1793,7 +2322,7 @@ mod tests {
.get_exps()
.unwrap_err()
{
- E::ExpDivByZero(i) => i == 6,
+ ExpDivByZero(i) => i == 6,
_ => false,
}
);
@@ -1803,7 +2332,7 @@ mod tests {
.get_exps()
.unwrap_err()
{
- E::ExpIsNotInt(i) => i == 7,
+ ExpIsNotInt(i) => i == 7,
_ => false,
}
);
@@ -1833,7 +2362,7 @@ mod tests {
.get_exps()
.unwrap_err()
{
- E::MissingTerm(i) => i == 0,
+ MissingTerm(i) => i == 0,
_ => false,
}
);
@@ -1842,7 +2371,7 @@ mod tests {
.get_exps()
.unwrap_err()
{
- E::MissingTerm(i) => i == 0,
+ MissingTerm(i) => i == 0,
_ => false,
}
);
@@ -1890,7 +2419,7 @@ mod tests {
.get_neg()
.unwrap_err()
{
- E::MissingTerm(i) => i == 0,
+ MissingTerm(i) => i == 0,
_ => false,
}
);
@@ -1899,7 +2428,7 @@ mod tests {
.get_neg()
.unwrap_err()
{
- E::MissingTerm(i) => i == 0,
+ MissingTerm(i) => i == 0,
_ => false,
}
);
@@ -2011,7 +2540,7 @@ mod tests {
.get_mults()
.unwrap_err()
{
- E::MissingTerm(i) => i == 0,
+ MissingTerm(i) => i == 0,
_ => false,
}
);
@@ -2020,7 +2549,7 @@ mod tests {
.get_mults()
.unwrap_err()
{
- E::MissingTerm(i) => i == 0,
+ MissingTerm(i) => i == 0,
_ => false,
}
);
@@ -2030,7 +2559,7 @@ mod tests {
.get_mults()
.unwrap_err()
{
- E::DivByZero(i) => i == 3,
+ DivByZero(i) => i == 3,
_ => false,
}
);
@@ -2133,7 +2662,7 @@ mod tests {
.get_adds()
.unwrap_err()
{
- E::MissingTerm(i) => i == 0,
+ MissingTerm(i) => i == 0,
_ => false,
}
);
@@ -2142,7 +2671,7 @@ mod tests {
.get_adds()
.unwrap_err()
{
- E::MissingTerm(i) => i == 0,
+ MissingTerm(i) => i == 0,
_ => false,
}
);
@@ -2218,7 +2747,7 @@ mod tests {
.evaluate()
.unwrap_err()
{
- E::MissingTerm(i) => i == 0,
+ MissingTerm(i) => i == 0,
_ => false,
}
);
@@ -2236,7 +2765,7 @@ mod tests {
.evaluate()
.unwrap_err()
{
- E::InvalidQuit => true,
+ InvalidQuit => true,
_ => false,
}
);
@@ -2245,16 +2774,16 @@ mod tests {
.evaluate()
.unwrap_err()
{
- E::MissingTerm(i) => i == 0,
+ MissingTerm(i) => i == 0,
_ => false,
}
);
assert!(
match Evaluator::new(b"q", &mut Cache::new(), &mut None, &mut Vec::new())
.evaluate()
- .unwrap_err()
+ .unwrap()
{
- E::MissingLF => true,
+ O::Exit => true,
_ => false,
}
);
@@ -2418,7 +2947,7 @@ mod tests {
.evaluate()
.unwrap_err()
{
- E::MissingTerm(i) => i == 0,
+ MissingTerm(i) => i == 0,
_ => false,
}
);
@@ -2427,7 +2956,7 @@ mod tests {
.evaluate()
.unwrap_err()
{
- E::TrailingSyms(i) => i == 1,
+ TrailingSyms(i) => i == 1,
_ => false,
}
);
@@ -2437,7 +2966,7 @@ mod tests {
.evaluate()
.unwrap_err()
{
- E::TrailingSyms(i) => i == 1,
+ TrailingSyms(i) => i == 1,
_ => false,
}
);
@@ -2473,13 +3002,97 @@ mod tests {
_ => false,
}
);
- // Empty input does not cause a panic!.
- assert!(match Evaluator::new(&[], &mut cache, &mut prev, &mut exp)
- .evaluate()
- .unwrap_err()
- {
- E::MissingLF => true,
+ assert!(
+ match Evaluator::new(&[0u8; 0], &mut cache, &mut prev.clone(), &mut exp)
+ .evaluate()
+ .unwrap()
+ {
+ O::Empty(o) => *o == prev,
+ _ => false,
+ }
+ );
+ }
+ #[test]
+ #[cfg(feature = "rand")]
+ fn eval_iter() {
+ use super::*;
+ use num_traits::ToPrimitive;
+ use std::io::{self, BufRead, Error, ErrorKind, Read};
+ struct Reader<'a> {
+ data: &'a [u8],
+ err: bool,
+ }
+ impl<'a> Reader<'a> {
+ fn new(data: &'a [u8]) -> Self {
+ Self { data, err: true }
+ }
+ }
+ impl<'a> Read for Reader<'a> {
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ if self.err {
+ self.err = false;
+ Err(Error::new(ErrorKind::Other, ""))
+ } else {
+ let len = usize::min(buf.len(), self.data.len());
+ buf[..len].copy_from_slice(&self.data[..len]);
+ Ok(len)
+ }
+ }
+ }
+ impl<'a> BufRead for Reader<'a> {
+ fn fill_buf(&mut self) -> io::Result<&[u8]> {
+ if self.err {
+ self.err = false;
+ Err(Error::new(ErrorKind::Other, ""))
+ } else {
+ Ok(self.data)
+ }
+ }
+ fn consume(&mut self, amt: usize) {
+ self.data = &self.data[amt..];
+ }
+ }
+ let mut iter = EvalIter::new(Reader::new(
+ b"1+2\n4\n\nq\n5\ns\nrand() + rand(-139/@, 2984/134)\nab",
+ ));
+ assert!(match iter.lend_next().unwrap().unwrap_err() {
+ E::Error(_) => true,
+ _ => false,
+ });
+ assert!(match iter.lend_next().unwrap().unwrap() {
+ O::Eval(r) => r.numer().to_i32().unwrap() == 3,
+ _ => false,
+ });
+ assert!(match iter.lend_next().unwrap().unwrap() {
+ O::Eval(r) => r.numer().to_i32().unwrap() == 4,
+ _ => false,
+ });
+ assert!(match iter.lend_next().unwrap().unwrap() {
+ O::Empty(r) => r.as_ref().unwrap().numer().to_i32().unwrap() == 4,
+ _ => false,
+ });
+ assert!(iter.lend_next().is_none());
+ assert!(match iter.lend_next().unwrap().unwrap() {
+ O::Eval(r) => r.numer().to_i32().unwrap() == 5,
+ _ => false,
+ });
+ assert!(match iter.lend_next().unwrap().unwrap() {
+ O::Store(r) => r.as_ref().unwrap().numer().to_i32().unwrap() == 5,
+ _ => false,
+ });
+ assert!(match iter.lend_next().unwrap().unwrap() {
+ O::Eval(r) => r.is_integer(),
+ _ => false,
+ });
+ assert!(match iter.lend_next().unwrap().unwrap_err() {
+ E::LangErr(e) => match e {
+ LangErr::MissingTerm(_) => true,
+ _ => false,
+ },
_ => false,
});
+ assert!(iter.lend_next().is_none());
+ assert!(iter.lend_next().is_none());
+ assert!(iter.lend_next().is_none());
}
}
diff --git a/src/main.rs b/src/main.rs
@@ -1,13 +1,11 @@
-//! # calc_rational
-//!
-//! calc_rational consists of a binary crate `calc` and a library crate
-//! [`calc_lib`](https://docs.rs/calc_rational/latest/calc_lib). `calc` is a CLI calculator for basic
-//! rational number arithmetic using standard operator precedence and associativity. Internally, it is
+//! # `calc`
+//! `calc` is a CLI calculator for basic rational number arithmetic using standard
+//! operator precedence and associativity. Internally, it is
//! based on [`Ratio<T>`](https://docs.rs/num/latest/num/rational/struct.Ratio.html)
-//! and [`BigInt`](https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html).
-//!
-//! ## Calc in action
-//!
+//! and [`BigInt`](https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html).
+//!
+//! ## Calc in action
+//!
//! ```bash
//! [zack@laptop ~]$ calc
//! 2.71828^0^3.14159 + -1!
@@ -38,126 +36,109 @@
//! > 2/3
//!
//! > 0.666666667
-//! @6
-//! There are only 4 previous results.
-//! s 1
-//! Invalid store expression. A store expression must be of the extended regex form: ^[ \t]*s[ \t]*$.
-//! q a
-//! Invalid quit expression. A quit expression must be of the extended regex form: ^[ \t]*q[ \t]*$.
-//! a
-//! Missing terminal expression at position 0. A terminal expression is a decimal literal expression, recall expression, absolute value expression, parenthetical expression, or a round expression.
-//! |1
-//! Invalid absolute value expression ending at position 2. An absolute value expression is an addition expression enclosed in '||'.
-//! (2
-//! Invalid parenthetical expression ending at position 2. A parenthetical expression is an addition expression enclosed in '()'.
-//! round(1,10)
-//! Invalid round expression ending at position 9. A round expression is of the form 'round(<add expression>, digit)'
-//! 1/(2-2)
-//! Division by zero ending at position 7.
-//! 2^(1/2)
-//! Non-integer exponent with a base that was not 0 or 1 ending at position 7.
-//! 0^-1
-//! Non-negative exponent with a base of 0 ending at position 4.
-//! (-1)!
-//! Factorial of a rational number that was not a non-negative integer ending at position 5.
-//! 4.
-//! Invalid decimal literal expression ending at position 2. A decimal literal expression must be of the extended regex form: [0-9]+(\.[0-9]+)?.
-//! 1+2 a
-//! Trailing symbols starting at position 4.
+//! rand()
+//! > 939435294927814822
+//! rand(1+9,10!)
+//! > 2660936
//! q
//! [zack@laptop ~]$
//! ```
-//!
-//! ## Expressions
-//!
-//! The following are the list of expressions in descending order of precedence:
-//! 1. number literals, `@`, `()`, `||`, `round()`
-//! 2. `!`
-//! 3. `^`
-//! 4. `-` (unary negation operator)
-//! 5. `*`, `/`
-//! 6. `+`, `-`
-//!
-//! All binary operators are left-associative sans `^` which is right-associative.
-//!
+//!
+//! ## Expressions
+//!
+//! The following are the list of expressions in descending order of precedence:
+//! 1. number literals, `@`, `()`, `||`, `round()`, `rand()`
+//! 2. `!`
+//! 3. `^`
+//! 4. `-` (unary negation operator)
+//! 5. `*`, `/`
+//! 6. `+`, `-`
+//!
+//! All binary operators are left-associative sans `^` which is right-associative.
+//!
//! Any expression is allowed to be enclosed in `()`. Note that parentheses are purely for grouping expressions;
//! in particular, you cannot use them to represent multiplication (e.g., `4(2)` is grammatically incorrect and
-//! will result in an error message).
-//!
-//! Any expression is allowed to be enclosed in `||`. This unary operator represents absolute value.
-//!
+//! will result in an error message).
+//!
+//! Any expression is allowed to be enclosed in `||`. This unary operator represents absolute value.
+//!
//! `!` is the factorial operator. Due to its high precedence, something like *-i!^j!* for *i, j ∈ ℕ* is
//! the same thing as *-((i!)^(j!))*. If the expression preceding it does not evaluate to a non-negative integer,
//! then an error will be displayed. Spaces and tabs are *not* ignored; so `1 !` is grammatically incorrect and
-//! will result in an error message.
-//!
+//! will result in an error message.
+//!
//! `^` is the exponentiation operator. The expression left of the operator can evaluate to any rational number;
//! however the expression right of the operator must evaluate to an integer unless the expression on the left
//! evaluates to `0` or `1`. In the event of the former, the expression right of the operator must evaluate to a
//! non-negative rational number. In the event of the latter, the expression right of the operator can evaluate to
-//! any rational number. Note that `0^0` is defined to be 1.
-//!
-//! The unary operator `-` represents negation.
-//!
+//! any rational number. Note that `0^0` is defined to be 1.
+//!
+//! The unary operator `-` represents negation.
+//!
//! The operators `*` and `/` represent multiplication and division respectively. Expressions right of `/`
-//! must evaluate to any non-zero rational number; otherwise an error will be displayed.
-//!
-//! The binary operators `+` and `-` represent addition and subtraction respectively.
-//!
-//! With the aforementioned exception of `!`, all spaces and tabs before and after operators are ignored.
-//!
-//! ## Round expression
-//!
+//! must evaluate to any non-zero rational number; otherwise an error will be displayed.
+//!
+//! The binary operators `+` and `-` represent addition and subtraction respectively.
+//!
+//! With the aforementioned exception of `!`, all spaces and tabs before and after operators are ignored.
+//!
+//! ## Round expression
+//!
//! `round(expression, digit)` rounds `expression` to `digit`-number of fractional digits. An error will
-//! be displayed if called incorrectly.
-//!
-//! ## Numbers
-//!
+//! be displayed if called incorrectly.
+//!
+//! ## Rand expression
+//!
+//! `rand(expression, expression)` generates a random 64-integer inclusively between the passed expressions.
+//! An error will be displayed if called incorrectly. `rand()` generates a random 64-bit integer.
+//!
+//! ## Numbers
+//!
//! A number literal is a non-empty sequence of digits or a non-empty sequence of digits immediately followed by `.`
//! which is immediately followed by a non-empty sequence of digits (e.g., `134.901`). This means that number
//! literals represent precisely all possible ratios of non-negative integers to non-negative integers who sole
//! prime factors are 2 or 5. To represent all other rational numbers, the unary operator `-` and binary
-//! operator `/` must be used.
-//!
-//! ## Empty expression
-//!
+//! operator `/` must be used.
+//!
+//! ## Empty expression
+//!
//! The empty expression (i.e., expression that at most only consists of spaces and tabs) will return
//! the result from the previous non-empty and non-store expression in *decimal* form using the minimum number of digits.
//! In the event an infinite number of digits is required, it will be rounded to 9 fractional digits using normal rounding
-//! rules first.
-//!
-//! ## Store expression
-//!
+//! rules first.
+//!
+//! ## Store expression
+//!
//! To store the result of the previous non-empty and non-store expression, one simply passes `s`. In addition to storing the
//! result which will subsequently be available via `@`, it displays the result. At most 8 results can be stored at once;
-//! at which point, results that are stored overwrite the oldest result.
-//!
-//! ## Recall expression
-//!
+//! at which point, results that are stored overwrite the oldest result.
+//!
+//! ## Recall expression
+//!
//! `@` is used to recall previously stored results. It can be followed by any *digit* from `1` to `8`.
//! If such a digit does not immediately follow it, then it will be interpreted as if there were a `1`.
//! `@i` returns the *i*-th most-previous stored result where *i ∈ {1, 2, 3, 4, 5, 6, 7, 8}*.
//! Note that spaces and tabs are *not* ignored so `@ 2` is grammatically incorrect and will result in an error message.
-//! As emphasized, it does not work on expressions; so both `@@` and `@(1)` are grammatically incorrect.
-//!
-//! ## Character encoding
-//!
+//! As emphasized, it does not work on expressions; so both `@@` and `@(1)` are grammatically incorrect.
+//!
+//! ## Character encoding
+//!
//! All inputs must only contain the ASCII encoding of the following Unicode scalar values: `0`-`9`, `.`, `+`, `-`,
-//! `*`, `/`, `^`, `!`, `|`, `(`, `)`, `round`, `,`, `@`, `s`, <space>, <tab>, <line feed>, <carriage return>,
-//! `q`, and `d`. Any other byte sequences are grammatically incorrect and will lead to an error message.
-//!
-//! ## Errors
-//!
+//! `*`, `/`, `^`, `!`, `|`, `(`, `)`, `round`, `rand`, `,`, `@`, `s`, <space>, <tab>, <line feed>, <carriage return>,
+//! and `q`. Any other byte sequences are grammatically incorrect and will lead to an error message.
+//!
+//! ## Errors
+//!
//! Errors due to a language violation (e.g., dividing by `0`) manifest into an error message. `panic!`s
//! and [`io::Error`](https://doc.rust-lang.org/std/io/struct.Error.html)s caused by writing to the global
-//! standard output stream lead to program abortion.
-//!
-//! ## Exiting
-//!
-//! `q` with any number of spaces and tabs before and after will cause the program to terminate.
-//!
-//! ### Formal language specification
-//!
+//! standard output stream lead to program abortion.
+//!
+//! ## Exiting
+//!
+//! `q` with any number of spaces and tabs before and after will cause the program to terminate.
+//!
+//! ### Formal language specification
+//!
//! For a more precise specification of the “calc language”, one can read the
//! [calc language specification](https://git.philomathiclife.com/calc_rational/lang.pdf).
#![deny(
@@ -170,50 +151,24 @@
clippy::pedantic,
clippy::restriction
)]
-#![allow(clippy::blanket_clippy_restriction_lints, clippy::implicit_return)]
-use calc_lib::cache::Cache;
-use calc_lib::Evaluator;
-use calc_lib::O::{Empty, Eval, Exit, Store};
-use std::io::{self, BufRead, Error, Write};
+#![allow(
+ clippy::blanket_clippy_restriction_lints,
+ clippy::implicit_return,
+ clippy::question_mark_used
+)]
+use calc_lib::lending_iterator::LendingIterator;
+use calc_lib::EvalIter;
+use std::io::{self, Error, Write};
/// Entry point to the calc program.
/// # Errors
///
-/// Returns [`Error`] iff [`write`] returns one when writing
+/// Returns [`Error`] iff [`writeln`] returns one when writing
/// to the global standard output stream.
-#[allow(clippy::significant_drop_in_scrutinee)]
fn main() -> Result<(), Error> {
- let mut stored = Cache::new();
- let mut prev = None;
- let input = io::stdin();
- let mut in_lock = input.lock();
- let mut buffer = Vec::new();
- let out = io::stdout();
- let mut out_lock = out.lock();
- let mut exp_cache = Vec::new();
- loop {
- writeln!(
- &mut out_lock,
- "{}",
- match in_lock.read_until(b'\n', &mut buffer) {
- Ok(_) => match Evaluator::new(&buffer, &mut stored, &mut prev, &mut exp_cache)
- .evaluate()
- {
- Ok(o) => match o {
- Empty(_) | Eval(_) | Store(_) => o.to_string(),
- Exit => return Ok(()),
- },
- Err(e) => e.to_string(),
- },
- Err(e) => {
- let mut err = String::with_capacity(128);
- err.push_str("There was an error when reading the passed expression: ");
- err.push_str(e.to_string().as_str());
- err
- }
- }
- )?;
- buffer.clear();
- exp_cache.clear();
- }
+ let mut out = io::stdout().lock();
+ EvalIter::new(io::stdin().lock()).lend_try_fold((), |_, res| match res {
+ Ok(o) => writeln!(&mut out, "{o}"),
+ Err(e) => writeln!(&mut out, "{e}"),
+ })
}