calc_rational

CLI calculator for rational numbers.
git clone https://git.philomathiclife.com/repos/calc_rational
Log | Files | Refs | README

commit 364689aa69f11fd058b1f9295b36996d9d99e284
parent 85178007fff28f8df769ae5cd0f509dc160fe95b
Author: Zack Newman <zack@philomathiclife.com>
Date:   Mon,  1 May 2023 17:03:21 -0600

add sqrt. add mod.

Diffstat:
MCargo.toml | 5++++-
MREADME.md | 88+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Abuild.rs | 12++++++++++++
Mlang.tex | 41++++++++++++++++++++++++-----------------
Msrc/cache.rs | 4++--
Msrc/lib.rs | 455++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/main.rs | 45+++++++++++++++++++++++++++++++--------------
7 files changed, 460 insertions(+), 190 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.3.0" +version = "0.4.0" [lib] name = "calc_lib" @@ -26,6 +26,9 @@ num-rational = { version = "0.4.1", default-features = false, features = ["num-b num-traits = { version = "0.2.15", default-features = false } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"], optional = true } +[build-dependencies] +rustc_version = "0.4.0" + [features] rand = ["dep:rand", "std"] std = [] diff --git a/README.md b/README.md @@ -42,6 +42,18 @@ rand() > 939435294927814822 rand(1+9,10!) > 2660936 +1+4 mod 2 + 1 +> 2 +-5 mod 2 +> 1 +-5 mod -2 +> 1 +5 mod -2 +> 1 +9^0.5 +> 3 +(4/9)^(-1/2) +> 3/2 q [zack@laptop ~]$ ``` @@ -53,7 +65,7 @@ The following are the list of expressions in descending order of precedence: 2. `!` 3. `^` 4. `-` (unary negation operator) - 5. `*`, `/` + 5. `*`, `/`, `mod` 6. `+`, `-` All binary operators are left-associative sans `^` which is right-associative. @@ -70,9 +82,9 @@ then an error will be displayed. Spaces and tabs are *not* ignored; so `1 !` is 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 +however the expression right of the operator must evaluate to an integer or (+/-) 1/2 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. @@ -80,6 +92,9 @@ 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 operator `mod` represents modulo such that *n mod m = r = n - m\*q* for *n,q ∈ ℤ, m ∈ ℤ\\{0}, and r ∈ ℕ* +where *r* is the minimum non-negative solution. + The binary operators `+` and `-` represent addition and subtraction respectively. With the aforementioned exception of `!`, all spaces and tabs before and after operators are ignored. @@ -98,20 +113,20 @@ An error will be displayed if called incorrectly. `rand()` generates a random 64 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. +literals represent precisely all rational numbers that are equivalent to a ratio of a non-negative integer to +a positive integer whose 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. +the result from the previous non-(empty/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 +To store the result of the previous non-(empty/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. @@ -126,8 +141,9 @@ 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`, `rand`, `,`, `@`, `s`, &lt;space&gt;, &lt;tab&gt;, &lt;line feed&gt;, &lt;carriage return&gt;, -and `q`. Any other byte sequences are grammatically incorrect and will lead to an error message. +`*`, `/`, `^`, `!`, `mod`, `|`, `(`, `)`, `round`, `rand`, `,`, `@`, `s`, &lt;space&gt;, &lt;tab&gt;, +&lt;line feed&gt;, &lt;carriage return&gt;, and `q`. Any other byte sequences are grammatically incorrect and will +lead to an error message. ## Errors @@ -158,7 +174,7 @@ target. Note one must be aware of the ASCII encoding requirement. In particular ```bash [zack@laptop ~]$ cargo install --all-features calc_rational Updating crates.io index - Installing calc_rational v0.3.0 + Installing calc_rational v0.4.0 Compiling autocfg v1.1.0 Compiling libc v0.2.142 Compiling cfg-if v1.0.0 @@ -171,10 +187,10 @@ target. Note one must be aware of the ASCII encoding requirement. In particular 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) + Compiling calc_rational v0.4.0 (/home/zack/calc_rational) Finished release [optimized] target(s) in 5.54s Installing /home/zack/.cargo/bin/calc - Installed package `calc_rational v0.3.0` (executable `calc`) + Installed package `calc_rational v0.4.0` (executable `calc`) ``` ### Building and testing @@ -197,56 +213,58 @@ Cloning into 'calc_rational'... 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) + Compiling calc_rational v0.4.0 (/home/zack/calc_rational) Finished release [optimized] target(s) in 8.25s -[zack@laptop calc_rational]$ cargo t --release --all-features + [zack@laptop calc_rational]$ cargo t --all-features Compiling autocfg v1.1.0 Compiling libc v0.2.142 + Compiling semver v1.0.17 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 rustc_version v0.4.0 + Compiling calc_rational v0.4.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/projects/calc_rational) - Finished release [optimized] target(s) in 10.35s - Running unittests src/lib.rs (target/release/deps/calc_lib-fa299178069cd874) + Finished test [unoptimized + debuginfo] target(s) in 2.80s + Running unittests src/lib.rs (target/debug/deps/calc_lib-71003fccf12c1022) 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_get ... 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_index ... ok +test tests::abs ... 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::mult ... ok +test tests::factorial ... ok +test tests::empty ... ok +test tests::rand_uni ... ignored test tests::add ... ok +test tests::store ... ok test tests::neg ... ok +test tests::par ... 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::recall_expression ... ok test tests::eval ... ok -test tests::factorial ... ok -test tests::empty ... ok -test tests::store ... ok test tests::round ... ok -test tests::recall_expression ... ok -test tests::par ... ok +test tests::number_literal ... ok test tests::term ... ok +test tests::exp ... ok +test tests::mult ... ok +test tests::rand ... ok test result: ok. 25 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s - Running unittests src/main.rs (target/release/deps/calc-c20eab17c4183fda) + Running unittests src/main.rs (target/debug/deps/calc-1d58b73f30400d2f) running 0 tests diff --git a/build.rs b/build.rs @@ -0,0 +1,12 @@ +use rustc_version::{version_meta, Channel}; + +fn main() { + // Set cfg flags depending on release channel + let channel = match version_meta().unwrap().channel { + Channel::Stable => "CHANNEL_STABLE", + Channel::Beta => "CHANNEL_BETA", + Channel::Nightly => "CHANNEL_NIGHTLY", + Channel::Dev => "CHANNEL_DEV", + }; + println!("cargo:rustc-cfg={}", channel) +} diff --git a/lang.tex b/lang.tex @@ -6,13 +6,13 @@ We will define the language, \(L\), of our rational number calculator program. Define the set of non-terminal symbols to be \begin{align*} -N = \{&prog, expr, empty, quit, eval, store, add, mult, neg, exp, fact,\\ -&term, rnd, 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\\ -&rand, tab,lf,cr,q\}. +\Sigma = \{&0,1,2,3,4,5,6,7,8,9,.,+,-,*,/,\string^,!,mod,|(,),round,,,@,s,\\ +&space,rand, tab,lf,cr,q\}. \end{align*} Define the production rules, \(P\), as the following: \begin{enumerate} @@ -20,10 +20,10 @@ Define the production rules, \(P\), as the following: \item \(expr \rightarrow empty\ |\ quit\ |\ eval\ |\ store\) \item \(empty \rightarrow ws\) \item \(quit \rightarrow ws\ q\ ws\) - \item \(eval\rightarrow ws\ add\ ws\) + \item \(eval\rightarrow ws\ modulo\ 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 \(mult \rightarrow mult\ ws\ *\ ws\ neg\ |\ mult\ ws\ /\ ws\ neg\ |\ mult\ ws\ mod\ ws\ neg\ |\ neg\) \item \(neg \rightarrow -\ ws\ neg\ |\ exp\) \item \(exp \rightarrow fact\ ws\ \string^\ ws\ neg\ |\ fact\) \item \(fact \rightarrow fact!\ |\ term\) @@ -32,7 +32,7 @@ Define the production rules, \(P\), as the following: \item \(r \rightarrow round(ws\ add\ ws,ws\ digit\ ws)\) \item \(abs \rightarrow |ws\ add\ ws|\) \item \(recall \rightarrow @\ |\ @prev\) - \item \(par \rightarrow (ws\ add\ ws)\) + \item \(par \rightarrow (ws\ add \ ws)\) \item \(dec \rightarrow int\ |\ int.int\) \item \(int \rightarrow digit\ |\ digit\ int\) \item \(ws \rightarrow space\ ws\ |\ tab\ ws\ |\ \epsilon\) @@ -41,7 +41,7 @@ Define the production rules, \(P\), as the following: \item \(l \rightarrow lf\ |\ cr\ lf\) \end{enumerate} 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, +does not actually have a space). Define the start symbol to be \(prog\). Define the unambiguous, context-free grammar to be \[ G = (N, \Sigma, P, prog). \] Let \(\mathcal{L}(G)\) be the language generated from \(G\). When \(@\) is not immediately followed by a \(prev\), @@ -50,12 +50,19 @@ expression. \(lf\) is the Unicode scalar value U+000A, \(cr\) is the Unicode sca Unicode scalar value U+0020, \(tab\) is the Unicode scalar value U+0009, and \(\epsilon\) is the empty string. We 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. 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. +results as well as adds the unary operators \(||\), \(-\), and \(!\) as well as the binary operators \(\string^\) +and \(mod\) to mean absolute value, negation, factorial, exponentiation, and modulo 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. + +\(mod\) is defined iff the left operand evaluates to an integer and the right operand evaluates to a non-zero +integer. This operator returns the minimum non-negative integer, \(r\), that satisfies the equation +\[ n\ mod\ m = r = n - q*m \] +for \(n,q\in\mathbb{Z}\), \(m\in\mathbb{Z}\backslash\{0\}\), and \(r\in\mathbb{N}\). 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 @@ -69,11 +76,11 @@ From the above grammar, we see the expression precedence in descending order is \item \(!\) \item \(\string^\) \item \(-\) (the unary negation operator) - \item \(*\), \(/\) + \item \(*\), \(/\), \(mod\) \item \(+\), \(-\) \end{enumerate} -with \(\string^\) being right-associative and the rest of the binary operators being left-associative. Last, -for \(j \in \mathbb{N}\) and \(d_{j} \in \{0,1,2,3,4,5,6,7,8,9\}\subset \mathbb{Z}\), we have +with \(\string^\) being right-associative and the rest of the binary operators being left-associative. +Last, for \(j \in \mathbb{N}\) and \(d_{j} \in \{0,1,2,3,4,5,6,7,8,9\}\subset \mathbb{Z}\), we have \begin{align*} d_{0}d_{1}\cdots d_{n}.d_{n+1}\cdots d_{n+i} = (&d_{0}*10^{n} + d_{1}*10^{n-1} +\cdots + d_{n}*10^{0}\\ &+d_{n+1}*10^{-1} +\cdots + d_{n+i}*10^{-i}) diff --git a/src/cache.rs b/src/cache.rs @@ -23,14 +23,14 @@ pub struct Cache<T, const N: usize> { vals: [T; N], /// The number of `T`s in our cache. Once /// `N` number of `T`s have been cached, this will - /// always return `N`. + /// always be `N`. len: usize, /// The index position of the next insert. next_head: usize, } impl<T, const N: usize> Cache<T, N> { /// Returns the number of items cached. - /// This will always be `N` once at least + /// This will always return `N` once at least /// `N` items have been cached. #[inline] pub const fn len(&self) -> usize { diff --git a/src/lib.rs b/src/lib.rs @@ -1,7 +1,8 @@ -//! # `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) +//! # `calc_lib` +//! +//! `calc_lib` is a library for performing 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 @@ -11,7 +12,7 @@ //! 2. `!` //! 3. `^` //! 4. `-` (unary negation operator) -//! 5. `*`, `/` +//! 5. `*`, `/`, `mod` //! 6. `+`, `-` //! //! All binary operators are left-associative sans `^` which is right-associative. @@ -28,9 +29,9 @@ //! 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 +//! however the expression right of the operator must evaluate to an integer or (+/-) 1/2 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. @@ -38,6 +39,9 @@ //! 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 operator `mod` represents modulo such that *n mod m = r = n - m\*q* for *n,q ∈ ℤ, m ∈ ℤ\\{0}, and r ∈ ℕ* +//! where *r* is the minimum non-negative solution. +//! //! The binary operators `+` and `-` represent addition and subtraction respectively. //! //! With the aforementioned exception of `!`, all spaces and tabs before and after operators are ignored. @@ -56,20 +60,20 @@ //! //! 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. +//! literals represent precisely all rational numbers that are equivalent to a ratio of a non-negative integer +//! to a positive integer whose 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. +//! the result from the previous non-(empty/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 +//! To store the result of the previous non-(empty/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. //! @@ -84,8 +88,9 @@ //! ## Character encoding //! //! All inputs must only contain the ASCII encoding of the following Unicode scalar values: `0`-`9`, `.`, `+`, `-`, -//! `*`, `/`, `^`, `!`, `|`, `(`, `)`, `round`, `rand`, `,`, `@`, `s`, &lt;space&gt;, &lt;tab&gt;, &lt;line feed&gt;, &lt;carriage return&gt;, -//! and `q`. Any other byte sequences are grammatically incorrect and will lead to an error message. +//! `*`, `/`, `^`, `!`, `mod`, `|`, `(`, `)`, `round`, `rand`, `,`, `@`, `s`, &lt;space&gt;, &lt;tab&gt;, +//! &lt;line feed&gt;, &lt;carriage return&gt;, and `q`. Any other byte sequences are grammatically incorrect and will +//! lead to an error message. //! //! ## Errors //! @@ -102,7 +107,7 @@ //! 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))] +#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] #![deny( unsafe_code, unused, @@ -116,7 +121,6 @@ #![allow( clippy::arithmetic_side_effects, clippy::blanket_clippy_restriction_lints, - clippy::exhaustive_enums, clippy::implicit_return, clippy::integer_arithmetic, clippy::into_iter_on_ref, @@ -132,17 +136,19 @@ use alloc::vec::Vec; use cache::Cache; use core::convert; use core::fmt::{self, Display, Formatter}; +use core::ops::Index; use num_bigint::{BigInt, BigUint, Sign}; use num_integer::Integer; use num_rational::Ratio; -use num_traits::Pow; #[cfg(feature = "rand")] use num_traits::ToPrimitive; +use num_traits::{Inv, Pow}; #[cfg(feature = "rand")] use rand::{rngs::ThreadRng, RngCore}; use LangErr::{ - DivByZero, ExpDivByZero, ExpIsNotInt, InvalidAbs, InvalidDec, InvalidPar, InvalidQuit, - InvalidRound, InvalidStore, MissingTerm, NotEnoughPrevResults, NotNonNegIntFact, TrailingSyms, + DivByZero, ExpDivByZero, ExpIsNotIntOrOneHalf, InvalidAbs, InvalidDec, InvalidPar, InvalidQuit, + InvalidRound, InvalidStore, MissingTerm, ModIsNotInt, ModZero, NotEnoughPrevResults, + NotNonNegIntFact, SqrtDoesNotExist, TrailingSyms, }; use O::{Empty, Eval, Exit, Store}; /// Fixed-sized cache that automatically overwrites the oldest data @@ -154,6 +160,7 @@ pub mod cache; /// generic associated types. pub mod lending_iterator; /// Error due to a language violation. +#[non_exhaustive] #[derive(Debug)] pub enum LangErr { /// The input began with a `q` but had non-whitespace @@ -167,13 +174,20 @@ pub enum LangErr { DivByZero(usize), /// A sub-expression in the input would have led /// to a rational number that was not 0 or 1 to be - /// raised to a non-integer power. - ExpIsNotInt(usize), + /// raised to a non-integer power that is not (+/-) 1/2. + ExpIsNotIntOrOneHalf(usize), /// A sub-expression in the input would have led /// to 0 being raised to a negative power which itself /// would have led to a division by zero. ExpDivByZero(usize), /// A sub-expression in the input would have led + /// to a number modulo 0. + ModZero(usize), + /// A sub-expression in the input would have led + /// to the mod of two expressions with at least one + /// not being an integer. + ModIsNotInt(usize), + /// A sub-expression in the input would have led /// to a non-integer factorial or a negative integer factorial. NotNonNegIntFact(usize), /// The input contained a non-empty sequence of digits followed @@ -194,24 +208,23 @@ pub enum LangErr { /// recall expression, absolute value expression, parenthetical /// expression, or round expression. MissingTerm(usize), + /// The expression that was passed to the square root does not have a solution + /// in the field of rational numbers. + SqrtDoesNotExist(usize), /// 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 LangErr { @@ -221,21 +234,24 @@ impl Display for LangErr { 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}."), - ExpIsNotInt(u) => write!(f, "Non-integer exponent with a base that was not 0 or 1 ending at position {u}."), + ExpIsNotIntOrOneHalf(u) => write!(f, "Non-integer exponent that is not (+/-) 1/2 with a base that was not 0 or 1 ending at position {u}."), ExpDivByZero(u) => write!(f, "Non-negative exponent with a base of 0 ending at position {u}."), + ModZero(u) => write!(f, "A number modulo 0 ending at position {u}."), + ModIsNotInt(u) => write!(f, "The modulo expression was applied to at least one non-integer ending at position {u}."), NotNonNegIntFact(u) => write!(f, "Factorial of a rational number that was not a non-negative integer ending at position {u}."), InvalidDec(u) => write!(f, "Invalid decimal literal expression ending at position {u}. A decimal literal expression must be of the extended regex form: [0-9]+(\\.[0-9]+)?."), NotEnoughPrevResults(len) => write!(f, "There are only {len} previous results."), 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)'"), + InvalidRound(u) => write!(f, "Invalid round expression ending at position {u}. A round expression is of the form 'round(<mod expression>, digit)'"), + SqrtDoesNotExist(u) => write!(f, "The square root of the passed expression does not have a solution in the field of rational numbers ending at position {u}."), #[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>)'."), + Self::InvalidRand(u) => write!(f, "Invalid rand expression ending at position {u}. A rand expression is of the form 'rand()' or 'rand(<mod expression>, <mod expression>)'."), #[cfg(feature = "rand")] Self::RandErr(ref e) => e.fmt(f), #[cfg(feature = "rand")] @@ -246,6 +262,7 @@ impl Display for LangErr { } } /// A successful evaluation of an input. +#[allow(clippy::exhaustive_enums)] #[derive(Debug)] pub enum O<'a> { /// The input only contained whitespace. @@ -361,7 +378,7 @@ impl<'a> Display for O<'a> { } } /// Evaluates the supplied input. -pub struct Evaluator<'input, 'cache, 'prev, 'exp> { +pub struct Evaluator<'input, 'cache, 'prev, 'scratch> { /// The input to be evaluated. utf8: &'input [u8], /// The index within `utf8` that evaluation needs to continue. @@ -373,28 +390,24 @@ pub struct Evaluator<'input, 'cache, 'prev, 'exp> { cache: &'cache mut Cache<Ratio<BigInt>, 8>, /// The last result. prev: &'prev mut Option<Ratio<BigInt>>, - /// Buffer used to evaluate exponentiation sub-expressions. - /// We require buffering exponentiation sub-expressions due - /// to their being right-associative. Instead of repeatedly - /// creating temporary internal buffers, we rely on a supplied - /// buffer that can be re-used across multiple evaluations. - exp_cache: &'exp mut Vec<Ratio<BigInt>>, + /// Buffer used to evaluate right-associative sub-expressions. + scratch: &'scratch mut Vec<Ratio<BigInt>>, } -impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { +impl<'input, 'cache, 'prev, 'scratch> Evaluator<'input, 'cache, 'prev, 'scratch> { #[inline] - /// Creates an `Evaluator<'input, 'cache, 'prev, 'exp>` based on the supplied arguments. + /// Creates an `Evaluator<'input, 'cache, 'prev, 'scratch>` based on the supplied arguments. pub fn new( utf8: &'input [u8], cache: &'cache mut Cache<Ratio<BigInt>, 8>, prev: &'prev mut Option<Ratio<BigInt>>, - exp_cache: &'exp mut Vec<Ratio<BigInt>>, - ) -> Evaluator<'input, 'cache, 'prev, 'exp> { + scratch: &'scratch mut Vec<Ratio<BigInt>>, + ) -> Evaluator<'input, 'cache, 'prev, 'scratch> { Self { utf8, i: 0, cache, prev, - exp_cache, + scratch, } } /// Evaluates the input consuming the `Evaluator<'input, 'cache, 'exp>`. @@ -494,10 +507,13 @@ 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] + #[allow(clippy::else_if_without_else)] fn get_mults(&mut self) -> Result<Ratio<BigInt>, LangErr> { let mut left = self.get_neg()?; let mut right; let mut j; + let mut mod_val; + let mut numer; self.consume_ws(); while let Some(i) = self.utf8.get(self.i) { j = *i; @@ -514,6 +530,34 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { return Err(DivByZero(self.i)); } left /= right; + } else if let Some(k) = self.utf8.get(self.i..self.i.saturating_add(3)) { + if k == b"mod" { + if !left.is_integer() { + return Err(ModIsNotInt(self.i)); + } + self.i += 3; + self.consume_ws(); + right = self.get_neg()?; + if !right.is_integer() { + return Err(ModIsNotInt(self.i)); + } + numer = right.numer(); + if numer.sign() == Sign::NoSign { + return Err(ModZero(self.i)); + } + mod_val = left.numer() % numer; + left = Ratio::from_integer(if mod_val.sign() == Sign::Minus { + if numer.sign() == Sign::Minus { + mod_val - numer + } else { + mod_val + numer + } + } else { + mod_val + }); + } else { + break; + } } else { break; } @@ -537,18 +581,64 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { self.get_exps() .map(|val| if count & 1 == 0 { val } else { -val }) } + /// Gets the square root of value so long as a solution exists. + #[inline] + #[allow(clippy::unwrap_used)] + fn sqrt(val: Ratio<BigInt>) -> Option<Ratio<BigInt>> { + /// Returns the square root of `n` if one exists; otherwise + /// returns `None`. + // MUST NOT pass 0. + #[inline] + #[allow(clippy::suspicious_operation_groupings)] + fn calc(n: &BigUint) -> Option<BigUint> { + let mut shift = n.bits(); + shift += shift & 1; + let mut result = BigUint::new(Vec::new()); + let one = BigUint::new(vec![1]); + let zero = BigUint::new(Vec::new()); + loop { + shift -= 2; + result <<= 1u32; + result |= &one; + result ^= if &result * &result > (n >> shift) { + &one + } else { + &zero + }; + if shift == 0 { + break (&result * &result == *n).then_some(result); + } + } + } + let numer = val.numer(); + if numer.sign() == Sign::NoSign { + Some(val) + } else { + numer.try_into().map_or_else( + |_| None, + |num| { + calc(&num).and_then(|n| { + calc(&val.denom().try_into().unwrap()) + .map(|d| Ratio::new(n.into(), d.into())) + }) + }, + ) + } + } /// Evaluates exponentiation expressions as defined in the calc language. /// This function is based on negation expressions. #[inline] - #[allow(clippy::unwrap_used, clippy::unwrap_in_result)] fn get_exps(&mut self) -> Result<Ratio<BigInt>, LangErr> { let mut t = self.get_fact()?; - let ix = self.exp_cache.len(); + let ix = self.scratch.len(); let mut prev; let mut numer; - self.exp_cache.push(t); + self.scratch.push(t); self.consume_ws(); let mut j; + let one = BigInt::new(Sign::Plus, vec![1]); + let min_one = BigInt::new(Sign::Minus, vec![1]); + let two = BigInt::new(Sign::Plus, vec![2]); while let Some(i) = self.utf8.get(self.i) { j = *i; self.consume_ws(); @@ -556,46 +646,62 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { self.i += 1; self.consume_ws(); t = self.get_neg()?; - // This is safe since we always push a value into it - // before entering the loop. - prev = self.exp_cache.last().unwrap(); + // Safe since we always push at least one value, and we always + // return immediately once we encounter an error. + prev = self.scratch.index(self.scratch.len() - 1); numer = prev.numer(); // Equiv to checking if prev is 0. if numer.sign() == Sign::NoSign { if t.numer().sign() == Sign::Minus { + self.scratch.clear(); return Err(ExpDivByZero(self.i)); } - self.exp_cache.push(t); + self.scratch.push(t); } else if prev.is_integer() { - if numer == &BigInt::new(Sign::Plus, vec![1]) { - // 1 raised to anything is 1, so we don't bother - // storing the exponent. - } else if t.is_integer() { - self.exp_cache.push(t); + let t_numer = t.numer(); + // 1 raised to anything is 1, so we don't bother + // storing the exponent. + if *numer == one { + } else if t.is_integer() + || ((*t_numer == one || *t_numer == min_one) && *t.denom() == two) + { + self.scratch.push(t); } else { - return Err(ExpIsNotInt(self.i)); + self.scratch.clear(); + return Err(ExpIsNotIntOrOneHalf(self.i)); } - } else if t.is_integer() { - self.exp_cache.push(t); + } else if t.is_integer() + || ((*t.numer() == one || *t.numer() == min_one) && *t.denom() == two) + { + self.scratch.push(t); } else { - return Err(ExpIsNotInt(self.i)); + self.scratch.clear(); + return Err(ExpIsNotIntOrOneHalf(self.i)); } } else { break; } } - self.exp_cache.drain(ix..).try_rfold( - Ratio::from_integer(BigInt::new(Sign::Plus, vec![1])), - |exp, base| { + self.scratch + .drain(ix..) + .try_rfold(Ratio::from_integer(one.clone()), |exp, base| { if exp.is_integer() { Ok(base.pow(exp.numer())) } else if base.numer().sign() == Sign::NoSign { Ok(base) + } else if *exp.denom() == two { + if *exp.numer() == one { + Self::sqrt(base).map_or_else(|| Err(SqrtDoesNotExist(self.i)), Ok) + } else if *exp.numer() == min_one { + Self::sqrt(base) + .map_or_else(|| Err(SqrtDoesNotExist(self.i)), |v| Ok(v.inv())) + } else { + Err(ExpIsNotIntOrOneHalf(self.i)) + } } else { - Err(ExpIsNotInt(self.i)) + Err(ExpIsNotIntOrOneHalf(self.i)) } - }, - ) + }) } /// Evaluates factorial expressions as defined in the calc language. /// This function is based on terminal expressions. @@ -623,8 +729,8 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { // We can make a copy of self.i here, or call map_or instead // of map_or_else. let i = self.i; - t.numer().to_biguint().map_or_else( - || Err(NotNonNegIntFact(i)), + t.numer().try_into().map_or_else( + |_| Err(NotNonNegIntFact(i)), |val| { let mut factorial = fact(val); while let Some(b2) = self.utf8.get(self.i) { @@ -650,45 +756,50 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { } /// Evaluates terminal expressions as defined in the calc language. /// This function is based on number literal expressions, parenthetical expressions, - /// recall expressions, absolute value expressions, and round expressions. + /// recall expressions, absolute value expressions, round expressions, and possibly + /// rand expressions if that feature is enabled. #[inline] - #[allow(clippy::option_if_let_else)] fn get_term(&mut self) -> Result<Ratio<BigInt>, LangErr> { - match self.get_rational() { - Ok(o) => match o { - Some(r) => Ok(r), - None => match self.get_par() { - Ok(o2) => match o2 { - Some(r2) => Ok(r2), - None => match self.get_recall() { - Ok(o3) => match o3 { - Some(r3) => Ok(r3.clone()), - 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), - }, + self.get_rational().map_or_else(Err, |o| { + o.map_or_else( + || { + self.get_par().map_or_else(Err, |o2| { + o2.map_or_else( + || { + self.get_recall().map_or_else(Err, |o3| { + o3.map_or_else( + || { + self.get_abs().map_or_else(Err, |o4| { + o4.map_or_else( + || { + self.get_round().and_then(|o5| { + o5.map_or_else( + #[cfg(not(feature = "rand"))] + || Err(MissingTerm(self.i)), + #[cfg(feature = "rand")] + || self.get_rand(), + Ok, + ) + }) + }, + Ok, + ) + }) + }, + Ok, + ) + }) }, - Err(e3) => Err(e3), - }, - }, - Err(e2) => Err(e2), + Ok, + ) + }) }, - }, - Err(e) => Err(e), - } + Ok, + ) + }) } /// Generates a random 64-bit integer. - /// This function is based on addition expressions. + /// This function is based on modulo 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`. @@ -813,7 +924,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { } } /// Rounds a value to the specified number of fractional digits. - /// This function is based on addition expressions. + /// This function is based on modulo 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 { @@ -863,7 +974,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { } } /// Evaluates absolute value expressions as defined in the calc language. - /// This function is based on addition expressions. + /// This function is based on modulo expressions. #[inline] fn get_abs(&mut self) -> Result<Option<Ratio<BigInt>>, LangErr> { let Some(b) = self.utf8.get(self.i) else { @@ -893,9 +1004,13 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { } } /// Evaluates recall expressions as defined in the calc language. + // This does not return a Result<Option<&Ratio<BigInt>>, LangErr> + // since the only place this function is called is in get_term which + // would end up needing to clone the Ratio anyway. By not forcing + // get_term to clone, it can rely on map_or_else over match expressions. #[inline] #[allow(clippy::as_conversions, clippy::cast_possible_truncation)] - fn get_recall(&mut self) -> Result<Option<&Ratio<BigInt>>, LangErr> { + fn get_recall(&mut self) -> Result<Option<Ratio<BigInt>>, LangErr> { let Some(b) = self.utf8.get(self.i) else { return Ok(None) }; @@ -912,14 +1027,14 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { })) .map_or_else( || Err(NotEnoughPrevResults(self.cache.len() as u8)), - |p| Ok(Some(p)), + |p| Ok(Some(p.clone())), ) } else { Ok(None) } } /// Evaluates parenthetical expressions as defined in the calc language. - /// This function is based on addition expressions. + /// This function is based on modulo expressions. #[inline] fn get_par(&mut self) -> Result<Option<Ratio<BigInt>>, LangErr> { let Some(b) = self.utf8.get(self.i) else { @@ -1056,9 +1171,9 @@ 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. +#[allow(clippy::exhaustive_enums)] #[derive(Debug)] pub enum E { /// Error containing [`Error`] which is returned @@ -1082,7 +1197,6 @@ impl Display for E { #[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, @@ -1122,8 +1236,9 @@ where #[cfg(test)] mod tests { use super::LangErr::{ - DivByZero, ExpDivByZero, ExpIsNotInt, InvalidAbs, InvalidDec, InvalidPar, InvalidQuit, - InvalidRound, MissingTerm, NotEnoughPrevResults, NotNonNegIntFact, TrailingSyms, + DivByZero, ExpDivByZero, ExpIsNotIntOrOneHalf, InvalidAbs, InvalidDec, InvalidPar, + InvalidQuit, InvalidRound, MissingTerm, ModIsNotInt, ModZero, NotEnoughPrevResults, + NotNonNegIntFact, TrailingSyms, }; use super::*; #[test] @@ -1504,7 +1619,7 @@ mod tests { .get_recall() .unwrap() .unwrap() - == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) + == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) ); // Invalid characters are ignored at this stage. assert!( @@ -1512,7 +1627,7 @@ mod tests { .get_recall() .unwrap() .unwrap() - == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) + == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) ); // 0 is not a valid stored value only 1-8 are. assert!( @@ -1520,7 +1635,7 @@ mod tests { .get_recall() .unwrap() .unwrap() - == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) + == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) ); // 9 is not a valid stored value only 1-8 are. assert!( @@ -1528,7 +1643,7 @@ mod tests { .get_recall() .unwrap() .unwrap() - == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) + == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) ); // Spaces are not cleaned; otherwise this would error since we only have 1 stored value. assert!( @@ -1536,7 +1651,7 @@ mod tests { .get_recall() .unwrap() .unwrap() - == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) + == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) ); // Tabs are not cleaned; otherwise this would error since we only have 1 stored value. assert!( @@ -1544,7 +1659,7 @@ mod tests { .get_recall() .unwrap() .unwrap() - == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) + == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) ); // One digits are looked at so this is not @<ten>. assert!( @@ -1552,7 +1667,7 @@ mod tests { .get_recall() .unwrap() .unwrap() - == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) + == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) ); // Invalid recall expression since there is only one stored result. assert!( @@ -1576,14 +1691,14 @@ mod tests { .get_recall() .unwrap() .unwrap() - == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))) + == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))) ); assert!( Evaluator::new(b"@2", &mut cache, &mut prev, &mut Vec::new()) .get_recall() .unwrap() .unwrap() - == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) + == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) ); // Invalid recall expression since there are only three stored results. assert!( @@ -1614,7 +1729,7 @@ mod tests { .get_recall() .unwrap() .unwrap() - == &Ratio::from_integer(BigInt::from_biguint( + == Ratio::from_integer(BigInt::from_biguint( Sign::Plus, BigUint::new(vec![(b'9' - i) as u32]) )) @@ -1626,7 +1741,7 @@ mod tests { .get_recall() .unwrap() .unwrap() - == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![8]))) + == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![8]))) ); Evaluator::new(b"9\r\n", &mut cache, &mut prev, &mut Vec::new()) .evaluate() @@ -1642,7 +1757,7 @@ mod tests { .get_recall() .unwrap() .unwrap() - == &Ratio::from_integer(BigInt::from_biguint( + == Ratio::from_integer(BigInt::from_biguint( Sign::Plus, BigUint::new(vec![((b'9' + 1) - i) as u32]) )) @@ -2316,6 +2431,20 @@ mod tests { BigInt::from_biguint(Sign::Plus, BigUint::new(vec![8])) ) ); + assert!( + Evaluator::new( + b"(4/9)^(-1/2)", + &mut Cache::new(), + &mut None, + &mut Vec::new() + ) + .get_exps() + .unwrap() + == Ratio::new( + BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3])), + BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2])) + ) + ); // Error since 0 cannot be raised to a negative power. assert!( match Evaluator::new(b"0^(-1)", &mut Cache::new(), &mut None, &mut Vec::new()) @@ -2326,13 +2455,23 @@ mod tests { _ => false, } ); - // Error anything other than 0 or 1 cannot be raised to a non-integer power. + // Error since anything other than 0 or 1 cannot be raised to a non-integer power or (+/-) 1/2. + assert!( + match Evaluator::new(b"2^(1/3)", &mut Cache::new(), &mut None, &mut Vec::new()) + .get_exps() + .unwrap_err() + { + ExpIsNotIntOrOneHalf(i) => i == 7, + _ => false, + } + ); + // When exponent is (+/-) 1/2, base has to be the square of a rational number. assert!( match Evaluator::new(b"2^(1/2)", &mut Cache::new(), &mut None, &mut Vec::new()) .get_exps() .unwrap_err() { - ExpIsNotInt(i) => i == 7, + SqrtDoesNotExist(i) => i == 7, _ => false, } ); @@ -2553,6 +2692,46 @@ mod tests { _ => false, } ); + assert!( + Evaluator::new( + b"4.0\t mod 6", + &mut Cache::new(), + &mut None, + &mut Vec::new() + ) + .get_mults() + .unwrap() + == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4])),) + ); + assert!( + Evaluator::new(b"5 mod 3", &mut Cache::new(), &mut None, &mut Vec::new()) + .get_mults() + .unwrap() + == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))) + ); + assert!( + Evaluator::new(b"-5 mod 3", &mut Cache::new(), &mut None, &mut Vec::new()) + .get_mults() + .unwrap() + == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) + ); + assert!( + Evaluator::new(b"5 mod -3", &mut Cache::new(), &mut None, &mut Vec::new()) + .get_mults() + .unwrap() + == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))) + ); + assert!( + Evaluator::new( + b"-5 mod\t -3", + &mut Cache::new(), + &mut None, + &mut Vec::new() + ) + .get_mults() + .unwrap() + == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) + ); // Cannot divide by 0. assert!( match Evaluator::new(b"2/0", &mut Cache::new(), &mut None, &mut Vec::new()) @@ -2563,6 +2742,34 @@ mod tests { _ => false, } ); + assert!( + match Evaluator::new(b"2 mod 0", &mut Cache::new(), &mut None, &mut Vec::new()) + .get_mults() + .unwrap_err() + { + ModZero(i) => i == 7, + _ => false, + } + ); + // Right and left operands of mod must be integers. + assert!( + match Evaluator::new(b"3.2 mod 1", &mut Cache::new(), &mut None, &mut Vec::new()) + .get_mults() + .unwrap_err() + { + ModIsNotInt(i) => i == 4, + _ => false, + } + ); + assert!( + match Evaluator::new(b"3 mod 3.2", &mut Cache::new(), &mut None, &mut Vec::new()) + .get_mults() + .unwrap_err() + { + ModIsNotInt(i) => i == 9, + _ => false, + } + ); // multiplication has lower precedence than exponentiation. assert!( Evaluator::new(b"2*2^2", &mut Cache::new(), &mut None, &mut Vec::new()) @@ -2576,6 +2783,12 @@ mod tests { .unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))) ); + assert!( + Evaluator::new(b"8 mod 3^2", &mut Cache::new(), &mut None, &mut Vec::new()) + .get_mults() + .unwrap() + == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![8]))) + ); } #[test] fn add() { diff --git a/src/main.rs b/src/main.rs @@ -1,6 +1,7 @@ -//! # `calc` -//! `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). //! @@ -40,6 +41,18 @@ //! > 939435294927814822 //! rand(1+9,10!) //! > 2660936 +//! 1+4 mod 2 + 1 +//! > 2 +//! -5 mod 2 +//! > 1 +//! -5 mod -2 +//! > 1 +//! 5 mod -2 +//! > 1 +//! 9^0.5 +//! > 3 +//! (4/9)^(-1/2) +//! > 3/2 //! q //! [zack@laptop ~]$ //! ``` @@ -51,7 +64,7 @@ //! 2. `!` //! 3. `^` //! 4. `-` (unary negation operator) -//! 5. `*`, `/` +//! 5. `*`, `/`, `mod` //! 6. `+`, `-` //! //! All binary operators are left-associative sans `^` which is right-associative. @@ -68,9 +81,9 @@ //! 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 +//! however the expression right of the operator must evaluate to an integer or (+/-) 1/2 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. @@ -78,6 +91,9 @@ //! 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 operator `mod` represents modulo such that *n mod m = r = n - m\*q* for *n,q ∈ ℤ, m ∈ ℤ\\{0}, and r ∈ ℕ* +//! where *r* is the minimum non-negative solution. +//! //! The binary operators `+` and `-` represent addition and subtraction respectively. //! //! With the aforementioned exception of `!`, all spaces and tabs before and after operators are ignored. @@ -96,20 +112,20 @@ //! //! 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. +//! literals represent precisely all rational numbers that are equivalent to a ratio of a non-negative integer +//! to a positive integer whose 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. +//! the result from the previous non-(empty/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 +//! To store the result of the previous non-(empty/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. //! @@ -124,8 +140,9 @@ //! ## Character encoding //! //! All inputs must only contain the ASCII encoding of the following Unicode scalar values: `0`-`9`, `.`, `+`, `-`, -//! `*`, `/`, `^`, `!`, `|`, `(`, `)`, `round`, `rand`, `,`, `@`, `s`, &lt;space&gt;, &lt;tab&gt;, &lt;line feed&gt;, &lt;carriage return&gt;, -//! and `q`. Any other byte sequences are grammatically incorrect and will lead to an error message. +//! `*`, `/`, `^`, `!`, `mod`, `|`, `(`, `)`, `round`, `rand`, `,`, `@`, `s`, &lt;space&gt;, &lt;tab&gt;, +//! &lt;line feed&gt;, &lt;carriage return&gt;, and `q`. Any other byte sequences are grammatically incorrect and will +//! lead to an error message. //! //! ## Errors //!