commit 5be9d112bcb3810a2ffd1b661695440ad5f737fd
Author: Zack Newman <zack@philomathiclife.com>
Date: Thu, 13 Apr 2023 22:34:19 -0600
init
Diffstat:
A | .gitignore | | | 4 | ++++ |
A | Cargo.toml | | | 34 | ++++++++++++++++++++++++++++++++++ |
A | LICENSE-APACHE | | | 177 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | LICENSE-MIT | | | 20 | ++++++++++++++++++++ |
A | README.md | | | 9 | +++++++++ |
A | lang.pdf | | | 0 | |
A | lang.tex | | | 86 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/cache.rs | | | 311 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/lib.rs | | | 1161 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/main.rs | | | 167 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
10 files changed, 1969 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,4 @@
+Cargo.lock
+target/**
+lang.aux
+lang.log
diff --git a/Cargo.toml b/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+authors = ["Zack Newman <zack@philomathiclife.com>"]
+categories = ["algorithms", "mathematics", "science", "command-line-utilities"]
+description = "CLI calculator for rational numbers."
+documentation = "https://docs.rs/calc_rational"
+edition = "2021"
+keywords = ["calculator", "mathematics", "numerics"]
+license = "MIT OR Apache-2.0"
+name = "calc_rational"
+readme = "README.md"
+repository = "https://git.philomathiclife.com/repos/calc_rational/"
+version = "0.1.0"
+
+[lib]
+name = "calc_lib"
+path = "src/lib.rs"
+
+[[bin]]
+name = "calc"
+path = "src/main.rs"
+
+[dependencies]
+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 }
+
+[badges]
+maintenance = { status = "actively-developed" }
+
+[profile.release]
+lto = true
+panic = 'abort'
+strip = 'symbols'
diff --git a/LICENSE-APACHE b/LICENSE-APACHE
@@ -0,0 +1,177 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/LICENSE-MIT b/LICENSE-MIT
@@ -0,0 +1,20 @@
+Copyright © 2023 Zack Newman
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+“Software”), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
@@ -0,0 +1,9 @@
+calc_rational
+----------------
+calc_rational consists of a binary crate calc and a library crate
+calc_lib. calc is a CLI calculator that only deals with arithmetic
+defined on the rational numbers. 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).
+For a formal specification of the calc language, one can read
+[lang.pdf](https://git.philomathiclife.com/calc_rational/lang.pdf).
diff --git a/lang.pdf b/lang.pdf
Binary files differ.
diff --git a/lang.tex b/lang.tex
@@ -0,0 +1,86 @@
+\documentclass{article}
+\usepackage{amsfonts, amsmath, parskip}
+\pagestyle{empty}
+\begin{document}
+We will define the language, \(L\), of our rational number calculator program.
+
+Define the set of non-terminal symbols to be
+\[ N = \{expr, add, mult, neg, exp, fact, term, s, dec, int, at, digit, prev\}. \]
+Define the set of terminal symbols to be
+\[ \Sigma = \{0,1,2,3,4,5,6,7,8,9,.,+,-,*,/,\string^,!,|(,),@,=,space,lf,q\}. \]
+Define the production rules, \(P\), as the following:
+\begin{enumerate}
+ \item \(expr \rightarrow s\ add\ s\ lf\ |\ s\ = \ s\ at \ s\ lf\ |\ s\ = \ s\ digit\ at \ s\ lf\ |\ s\ lf\ |\ s\ q\ s\ lf\)
+ \item \(add \rightarrow add\ s\ +\ s\ mult\ |\ add\ s\ -\ s\ mult\ |\ mult\)
+ \item \(mult \rightarrow mult\ s\ *\ s\ neg\ |\ mult\ s\ /\ s\ neg\ |\ neg\)
+ \item \(neg \rightarrow -\ s\ neg\ |\ exp\)
+ \item \(exp \rightarrow fact\ s\ \string^\ s\ neg\ |\ fact\)
+ \item \(fact \rightarrow fact!\ |\ term\)
+ \item \(term \rightarrow dec\ |\ at\ |\ |s\ add\ s|\ |\ (s\ add\ s)\)
+ \item \(s \rightarrow space\ s\ |\ \epsilon\)
+ \item \(dec \rightarrow int\ |\ int.int\)
+ \item \(int \rightarrow digit\ |\ digit\ int\)
+ \item \(at \rightarrow @\ |\ @prev\)
+ \item \(digit \rightarrow prev\ |\ 0\ |\ 9\)
+ \item \(prev \rightarrow 1\ |\ 2\ |\ 3\ |\ 4\ |\ 5\ |\ 6\ |\ 7\ |\ 8\)
+\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,
+context-free grammar to be
+\[ G = (N, \Sigma, P, expr). \]
+Let \(\mathcal{L}(G)\) be the language generated from \(G\). Let \(@ = @1\), and \(@prev\) represent the
+\(prev^{th}\) most-recent result. \(lf\) is the Unicode scalar value U+000A, \(space\) is the Unicode scalar value
+U+0020, 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 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
+expressions have been evaluated. From the above grammar, we see the operator precedence in descending order is the
+following:
+\begin{enumerate}
+ \item \(()\), \(||\)
+ \item \(!\)
+ \item \(\string^\)
+ \item \(-\) (the unary negation operator)
+ \item \(*\), \(/\)
+ \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
+\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})
+\end{align*}
+where for \(k\in\mathbb{N}\)
+\[ 10^{k} = \overbrace{10 * 10 *\cdots *10}^{k} \]
+and for \(l\in\mathbb{Z}^{-}\)
+\[ 10^{l} = \overbrace{1/10 * 1/10 *\cdots *1/10}^{|l|}. \]
+As a consequence of above, we have the following example:
+\[ 1/1.5 = 1/(3/2) = 2/3 \neq 1/6 = 1/3/2. \]
+For \(n\in\mathbb{N}\) we define the factorial operator as
+\[ n! = n * (n - 1) *\cdots * 1 \]
+which of course equals 1 when \(n = 0\).
+
+For the empty expression and the exit (i.e., \(q\)) and “recall” statements (i.e., statements that have \(=\)),
+the previous results are left in tact; all other expressions push the evaluated result to be the next previous
+result. Recall statements are used purely to display a previous value with the option to round to \(digit\) number
+of fractional digits using normal rounding rules. For example,
+\begin{align*}
+&4\\
+&@\\
+&4+@2
+\end{align*}
+returns 4, stores 4 as the previous result, returns 4, pushes 4 to be the second-most previous result, pushes
+4 to be the previous result, returns 8, pushes 4 to be the third-most previous result, pushes 4 to be the
+second-most previous result, and pushes 8 to be the most previous result. In contrast,
+\begin{align*}
+&4\\
+&=@\\
+&4+@2
+\end{align*}
+returns 4, stores 4 as the previous result, returns 4, and fails since the last line is not part of our
+language \(L\) since there is no second-previous result.
+\end{document}
diff --git a/src/cache.rs b/src/cache.rs
@@ -0,0 +1,311 @@
+#![deny(
+ unsafe_code,
+ unused,
+ warnings,
+ clippy::all,
+ clippy::cargo,
+ clippy::nursery,
+ clippy::pedantic
+)]
+#![allow(
+ clippy::arithmetic_side_effects,
+ clippy::implicit_return,
+ clippy::integer_arithmetic
+)]
+use core::ops::Index;
+/// A cache of `N` `T`s. When the cache is filled up,
+/// a new entry overwrites the oldest entry. `Cache<T, N>`
+/// cannot be expanded or shrunk.
+pub struct Cache<T, const N: usize> {
+ /// The cache of `T`s.
+ 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`.
+ 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
+ /// `N` items have been cached.
+ #[inline]
+ pub const fn len(&self) -> usize {
+ self.len
+ }
+ /// Returns true iff no items have been cached.
+ #[inline]
+ pub const fn is_empty(&self) -> bool {
+ self.len == 0
+ }
+}
+/// Implements the functions `get`, `get_unsafe`, and `push` as well as `Index<usize, Output = T>`
+/// for each of the passed usize literals.
+/// Only powers of two are allowed to be passed due to how the above functions are implemented.
+/// If any other value is passed, the implementation of those functions will be wrong.
+macro_rules! cache {
+ ( $( $x:literal),* ) => {
+ $(
+impl<T> Cache<T, $x> {
+ /// Returns `Some(&T)` iff there is an item located
+ /// at index `idx`. Indexing begins at the newest
+ /// entry (i.e., `self.get(0)` will return the most recent entry
+ /// iff at least one item has been cached).
+ #[inline]
+ pub fn get(&self, idx: usize) -> Option<&T> {
+ (idx < self.len).then(|| self.vals.index(self.next_head.wrapping_sub(1).wrapping_sub(idx) & ($x - 1)))
+ }
+ /// Returns a `&T` at index position `idx % N`. In the event
+ /// `(idx % N) >= self.len()`, then `&T` references the
+ /// default value of `T` that was inserted at creation but *not*
+ /// actually inserted via `push`.
+
+ /// # Safety
+ ///
+ /// `idx < self.len()`.
+ #[inline]
+ #[allow(unsafe_code)]
+ pub unsafe fn get_unsafe(&self, idx: usize) -> &T {
+ self.vals.index(self.next_head.wrapping_sub(1).wrapping_sub(idx) & ($x - 1))
+ }
+ /// Adds `val` to the cache. In the event `N` values have already been cached,
+ /// the oldest entry is overwritten. Returns a shared reference to the
+ /// value it inserts.
+ #[inline]
+ #[allow(clippy::indexing_slicing)]
+ pub fn push(&mut self, val: T) -> &T {
+ if self.len < $x {
+ self.len += 1;
+ }
+ // next_head is always less than or equal to N since we only implement
+ // this function when N is a power of 2.
+ self.vals[self.next_head] = val;
+ let ret = self.vals.index(self.next_head);
+ self.next_head = (self.next_head + 1) & ($x - 1);
+ ret
+ }
+}
+impl<T> Index<usize> for Cache<T, $x> {
+ type Output = T;
+ #[inline]
+ fn index(&self, index: usize) -> &Self::Output {
+ self.get(index).unwrap()
+ }
+}
+ )*
+ };
+}
+// MUST only pass powers of two! Anything else will lead to wrong code.
+cache!(
+ 0x1,
+ 0x2,
+ 0x4,
+ 0x8,
+ 0x10,
+ 0x20,
+ 0x40,
+ 0x80,
+ 0x100,
+ 0x200,
+ 0x400,
+ 0x800,
+ 0x1000,
+ 0x2000,
+ 0x4000,
+ 0x8000,
+ 0x0001_0000,
+ 0x0002_0000,
+ 0x0004_0000,
+ 0x0008_0000,
+ 0x0010_0000,
+ 0x0020_0000,
+ 0x0040_0000,
+ 0x0080_0000,
+ 0x0100_0000,
+ 0x0200_0000,
+ 0x0400_0000,
+ 0x0800_0000,
+ 0x1000_0000,
+ 0x2000_0000,
+ 0x4000_0000,
+ 0x8000_0000,
+ 0x0001_0000_0000,
+ 0x0002_0000_0000,
+ 0x0004_0000_0000,
+ 0x0008_0000_0000,
+ 0x0010_0000_0000,
+ 0x0020_0000_0000,
+ 0x0040_0000_0000,
+ 0x0080_0000_0000,
+ 0x0100_0000_0000,
+ 0x0200_0000_0000,
+ 0x0400_0000_0000,
+ 0x0800_0000_0000,
+ 0x1000_0000_0000,
+ 0x2000_0000_0000,
+ 0x4000_0000_0000,
+ 0x8000_0000_0000,
+ 0x0001_0000_0000_0000,
+ 0x0002_0000_0000_0000,
+ 0x0004_0000_0000_0000,
+ 0x0008_0000_0000_0000,
+ 0x0010_0000_0000_0000,
+ 0x0020_0000_0000_0000,
+ 0x0040_0000_0000_0000,
+ 0x0080_0000_0000_0000,
+ 0x0100_0000_0000_0000,
+ 0x0200_0000_0000_0000,
+ 0x0400_0000_0000_0000,
+ 0x0800_0000_0000_0000,
+ 0x1000_0000_0000_0000,
+ 0x2000_0000_0000_0000,
+ 0x4000_0000_0000_0000,
+ 0x8000_0000_0000_0000
+);
+impl<T, const N: usize> Cache<T, N>
+where
+ [T; N]: Default,
+{
+ /// Returns an instance of itself pre-loaded with
+ /// the `N` instances of the default value of `T`.
+ /// Note these default instances are treated as though
+ /// they don't exist (i.e., `self.is_empty()` will return true).
+ #[inline]
+ #[must_use]
+ pub fn new() -> Self {
+ Self::default()
+ }
+}
+impl<T, const N: usize> Default for Cache<T, N>
+where
+ [T; N]: Default,
+{
+ #[inline]
+ fn default() -> Self {
+ Self {
+ vals: Default::default(),
+ len: 0,
+ next_head: 0,
+ }
+ }
+}
+#[cfg(test)]
+mod tests {
+ use super::Cache;
+ #[test]
+ fn test_len() {
+ let mut c = Cache::<bool, 1>::new();
+ assert_eq!(0, c.len());
+ c.push(false);
+ assert_eq!(1, c.len());
+ c.push(false);
+ assert_eq!(1, c.len());
+ }
+ #[test]
+ fn test_is_empty() {
+ let mut c = Cache::<bool, 1>::new();
+ assert!(c.is_empty());
+ c.push(false);
+ assert!(!c.is_empty());
+ }
+ #[test]
+ fn test_get() {
+ let mut c = Cache::<bool, 4>::new();
+ assert!(c.get(0).is_none());
+ assert!(c.get(1).is_none());
+ assert!(c.get(2).is_none());
+ assert!(c.get(3).is_none());
+ assert!(c.get(4).is_none());
+ assert!(c.get(5).is_none());
+ assert!(c.get(usize::MAX).is_none());
+ c.push(true);
+ assert!(c.get(0).unwrap());
+ assert!(c.get(1).is_none());
+ assert!(c.get(2).is_none());
+ assert!(c.get(3).is_none());
+ assert!(c.get(4).is_none());
+ assert!(c.get(5).is_none());
+ assert!(c.get(usize::MAX).is_none());
+ c.push(false);
+ assert!(!c.get(0).unwrap());
+ assert!(c.get(1).unwrap());
+ assert!(c.get(2).is_none());
+ assert!(c.get(3).is_none());
+ assert!(c.get(4).is_none());
+ assert!(c.get(5).is_none());
+ assert!(c.get(usize::MAX).is_none());
+ c.push(false);
+ assert!(!c.get(0).unwrap());
+ assert!(!c.get(1).unwrap());
+ assert!(c.get(2).unwrap());
+ assert!(c.get(3).is_none());
+ assert!(c.get(4).is_none());
+ assert!(c.get(5).is_none());
+ assert!(c.get(usize::MAX).is_none());
+ c.push(true);
+ assert!(c.get(0).unwrap());
+ assert!(!c.get(1).unwrap());
+ assert!(!c.get(2).unwrap());
+ assert!(c.get(3).unwrap());
+ assert!(c.get(4).is_none());
+ assert!(c.get(5).is_none());
+ assert!(c.get(usize::MAX).is_none());
+ c.push(true);
+ assert!(c.get(0).unwrap());
+ assert!(c.get(1).unwrap());
+ assert!(!c.get(2).unwrap());
+ assert!(!c.get(3).unwrap());
+ assert!(c.get(4).is_none());
+ assert!(c.get(5).is_none());
+ assert!(c.get(usize::MAX).is_none());
+ }
+ #[test]
+ #[allow(unsafe_code)]
+ fn test_get_unsafe() {
+ let mut c = Cache::<bool, 4>::new();
+ unsafe {
+ assert!(!c.get_unsafe(0));
+ assert!(!c.get_unsafe(1));
+ assert!(!c.get_unsafe(2));
+ assert!(!c.get_unsafe(3));
+ assert!(!c.get_unsafe(4));
+ }
+ c.push(true);
+ unsafe {
+ assert!(c.get_unsafe(0));
+ assert!(!c.get_unsafe(1));
+ assert!(!c.get_unsafe(2));
+ assert!(!c.get_unsafe(3));
+ assert!(c.get_unsafe(4));
+ }
+ }
+ #[test]
+ fn test_index() {
+ let mut c = Cache::<bool, 4>::new();
+ c.push(true);
+ assert!(c[0]);
+ }
+ #[test]
+ #[should_panic]
+ fn test_index_panic() {
+ let c = Cache::<bool, 4>::new();
+ assert!(c[0]);
+ }
+ #[test]
+ fn test_push() {
+ let mut c = Cache::<bool, 4>::new();
+ assert!(c.push(true));
+ assert!(c.push(true));
+ assert!(!c.push(false));
+ assert!(c.push(true));
+ assert!(!c.push(false));
+ assert!(!c.push(false));
+ }
+ #[test]
+ fn test_new() {
+ _ = Cache::<bool, 0>::new();
+ _ = Cache::<bool, 32>::new();
+ _ = Cache::<bool, 31>::new();
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
@@ -0,0 +1,1161 @@
+//! This crate contains logic for an application to parse and evaluate
+//! basic rational number arithmetic using standard operator precedence and
+//! associativity.
+#![no_std]
+#![deny(
+ unsafe_code,
+ unused,
+ warnings,
+ clippy::all,
+ clippy::cargo,
+ clippy::nursery,
+ clippy::pedantic
+)]
+#![allow(
+ clippy::arithmetic_side_effects,
+ clippy::exhaustive_enums,
+ clippy::implicit_return,
+ clippy::integer_arithmetic,
+ clippy::into_iter_on_ref,
+ clippy::question_mark,
+ clippy::single_char_lifetime_names,
+ clippy::unseparated_literal_suffix
+)]
+extern crate alloc;
+use alloc::string::{String, ToString};
+use alloc::vec;
+use alloc::vec::Vec;
+use cache::Cache;
+use core::convert;
+use core::fmt::{self, Display, Formatter};
+use num_bigint::{BigInt, BigUint, Sign};
+use num_rational::Ratio;
+use num_integer::Integer;
+use num_traits::Pow;
+use E::{
+ DivByZero, ExpDivByZero, ExpIsNotInt, InvalidAbs, InvalidDec, InvalidPar, InvalidQuit,
+ InvalidRecall, MissingLF, MissingTerm, NotEnoughPrevResults, NotNonNegIntFact, TrailingSyms,
+};
+use O::{Empty, Exit, Prev, Val};
+/// Fixed-sized cache that automatically overwrites the oldest data
+/// when a new item is added and the cache is full. One can think of
+/// [`Cache`] as a very limited but more performant [`VecDeque`][alloc::collections::VecDeque] that only
+/// adds new data or reads old data.
+pub mod cache;
+/// A value from a previous expression that is to be
+/// displayed potentially in decimal form.
+#[derive(Debug)]
+pub struct PrevVal<'a> {
+ /// A cached value from a previous expression.
+ val: &'a Ratio<BigInt>,
+ /// None iff the previous value is to be displayed
+ /// in fractional form; otherwise contains the number
+ /// of fractional digits from 0 to 9 `val` will be rounded
+ /// to.
+ round: Option<u8>,
+}
+impl<'a> Display for PrevVal<'a> {
+ #[inline]
+ #[allow(unsafe_code, clippy::as_conversions, clippy::indexing_slicing)]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ if let Some(rem) = self.round {
+ let r = rem as usize;
+ let mult = BigInt::from_biguint(Sign::Plus, BigUint::new(vec![10])).pow(r);
+ // int < 0 iff val <= -1. frac < 0 iff val is a negative non-integer.
+ let (int, frac) = (self.val * &mult).round().numer().div_rem(&mult);
+ let int_str = int.to_string().into_bytes();
+ let frac_str = frac.to_string().into_bytes();
+ // The max length is the length of int_str plus the number of fractional digits
+ // plus a possible negative sign plus a possible decimal point.
+ // At most we will over-allocate by 2 precisely when int is negative
+ // and r is 0.
+ let mut v = Vec::with_capacity(int_str.len() + 2 + r);
+ // In the case val is a negative value greater than -1,
+ // we have to add the negative sign since 0 is unsigned.
+ // Technically this can panic! if one of the Strings returned from
+ // to_string is empty; however that is a bug, or at least undesired
+ // behavior, in the to_string implementation from BigInt.
+ let frac_val = match (int_str[0] == b'-', frac_str[0] == b'-') {
+ (false, true) => {
+ v.push(b'-');
+ &frac_str[1..]
+ }
+ (true, true) => &frac_str[1..],
+ _ => frac_str.as_slice(),
+ };
+ v.extend_from_slice(int_str.as_slice());
+ if r > 0 {
+ v.push(b'.');
+ let len = v.len();
+ // frac_val.len() is <= r.
+ while v.len() < len + r - frac_val.len() {
+ v.push(b'0');
+ }
+ v.extend_from_slice(frac_val);
+ }
+ // SAFETY:
+ // v contains precisely the UTF-8 code units returned from Strings
+ // returned from the to_string function on the integer and fraction part of
+ // val plus optionally the single byte encodings of ".", "-", and "0".
+ writeln!(f, "> {}", unsafe { String::from_utf8_unchecked(v) })
+ } else {
+ writeln!(f, "> {}", self.val)
+ }
+ }
+}
+/// 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,
+ /// The input began with a `q` but had other non-spaces
+ /// that followed it.
+ InvalidQuit,
+ /// The input began with an `=` but
+ /// was otherwise syntactically invalid.
+ InvalidRecall,
+ /// A sub-expression in the input would have led
+ /// to a division by zero.
+ 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),
+ /// 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 non-integer factorial or a negative integer factorial.
+ NotNonNegIntFact(usize),
+ /// The input contained a non-empty sequence of digits followed
+ /// by `.` which was not followed by a non-empty sequence of digits.
+ InvalidDec(usize),
+ /// A recall expression was used to recall the *i*-th previous result,
+ /// but there are fewer than *i* previous results where
+ /// *i ∈ {1, 2, 3, 4, 5, 6, 7, 8}*.
+ NotEnoughPrevResults(u8),
+ /// The input did not contain a closing `|`.
+ InvalidAbs(usize),
+ /// The input did not contain a closing `)`.
+ InvalidPar(usize),
+ /// A sub-expression in the input had a missing terminal expression
+ /// where a terminal expression is a decimal literal expression,
+ /// recall expression, absolute value expression, or a parenthetical
+ /// expression.
+ MissingTerm(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),
+}
+impl Display for E {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ MissingLF => f.write_str("The last character is not a line feed.\n"),
+ InvalidRecall => f.write_str("Invalid recall statement. A recall statement must be of the extended regex form: ^ *= *[0-9]?@[1-8]? *$.\n"),
+ InvalidQuit => f.write_str("Invalid quit statement. A quit statement must be of the extended regex form: ^ *q *$.\n"),
+ DivByZero(u) => writeln!(f, "Division by zero ending at position {u}."),
+ ExpIsNotInt(u) => writeln!(f, "Non-integer exponent with a base that was not 0 or 1 ending at position {u}."),
+ ExpDivByZero(u) => writeln!(f, "Non-negative exponent with a base of 0 ending at position {u}."),
+ NotNonNegIntFact(u) => writeln!(f, "Factorial of a rational number that was not a non-negative integer ending at position {u}."),
+ InvalidDec(u) => writeln!(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) => writeln!(f, "There are only {len} previous results."),
+ InvalidAbs(u) => writeln!(f, "Invalid absolute value expression ending at position {u}. An absolute value expression is an addition expression enclosed in '||'."),
+ InvalidPar(u) => writeln!(f, "Invalid parenthetical expression ending at position {u}. A parenthetical expression is an addition expression enclosed in '()'."),
+ MissingTerm(u) => writeln!(f, "Missing terminal expression at position {u}. A terminal expression is a decimal literal expression, recall expression, or addition expression enclosed in vertical bars or parentheses."),
+ TrailingSyms(u) => writeln!(f, "Trailing symbols starting at position {u}."),
+ }
+ }
+}
+/// A successful evaluation of an input.
+#[derive(Debug)]
+pub enum O<'a> {
+ /// The result of a passed expression.
+ Val(&'a Ratio<BigInt>),
+ /// The previous result of the requested
+ /// recall statement.
+ Prev(PrevVal<'a>),
+ /// The input contained all spaces which is part of our language
+ /// but does not do anything.
+ Empty,
+ /// A quit statement was issued to terminate the program.
+ Exit,
+}
+impl<'a> Display for O<'a> {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Val(r) => writeln!(f, "> {r}"),
+ Prev(ref p) => p.fmt(f),
+ Empty | Exit => Ok(()),
+ }
+ }
+}
+/// Evaluates the supplied input.
+pub struct Evaluator<'input, 'cache, 'exp> {
+ /// The input to be evaluated.
+ utf8: &'input [u8],
+ /// The index within `utf8` that evaluation needs to continue.
+ /// We use this instead of slicing from `utf8` since we want
+ /// to be able to report the position within the input
+ /// that an error occurs.
+ i: usize,
+ /// The cache of previously evaluated expressions.
+ prev: &'cache mut Cache<Ratio<BigInt>, 8>,
+ /// 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>>,
+}
+impl<'input, 'cache, 'exp> Evaluator<'input, 'cache, 'exp> {
+ /// Creates an `Evaluator<'input, 'cache, 'exp>` based on the supplied arguments.
+ #[inline]
+ pub fn new(
+ utf8: &'input [u8],
+ prev: &'cache mut Cache<Ratio<BigInt>, 8>,
+ exp_cache: &'exp mut Vec<Ratio<BigInt>>,
+ ) -> Evaluator<'input, 'cache, 'exp> {
+ Self {
+ utf8,
+ i: 0,
+ prev,
+ exp_cache,
+ }
+ }
+ /// Evaluates the input consuming the `Evaluator<'input, 'cache, 'exp>`.
+
+ /// # Errors
+ ///
+ /// Returns an [`E`] iff the input violates the calc language.
+ #[inline]
+ #[allow(clippy::indexing_slicing)]
+ pub fn evaluate(mut self) -> Result<O<'cache>, E> {
+ self.utf8 = if self.utf8.last().map_or(true, |b| *b != b'\n') {
+ return Err(MissingLF)
+ } else {
+ &self.utf8[..(self.utf8.len() - 1)]
+ };
+ self.consume_ws();
+ let Some(b) = self.utf8.get(self.i) else {
+ return Ok(Empty)
+ };
+ if *b == b'=' {
+ self.i += 1;
+ self.consume_ws();
+ let Some(b2) = self.utf8.get(self.i) else {
+ return Err(InvalidRecall)
+ };
+ let b3 = *b2;
+ let round = b3.is_ascii_digit().then(|| {
+ self.i += 1;
+ b3 - b'0'
+ });
+ self.get_recall().map(|val| Prev(PrevVal { val, round }))
+ } else if *b == b'q' {
+ self.i += 1;
+ self.consume_ws();
+ if self.i == self.utf8.len() {
+ Ok(Exit)
+ } else {
+ Err(InvalidQuit)
+ }
+ } else {
+ self.get_adds().and_then(move |r| {
+ self.consume_ws();
+ if self.i == self.utf8.len() {
+ Ok(Val(self.prev.push(r)))
+ } else {
+ Err(TrailingSyms(self.i))
+ }
+ })
+ }
+ }
+ /// Reads from the input until the next non-space byte value.
+ #[inline]
+ #[allow(clippy::indexing_slicing)]
+ fn consume_ws(&mut self) {
+ // 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
+ // Err taking the role of ControlFlow::Break.
+ self.i += self.utf8[self.i..]
+ .into_iter()
+ .try_fold(0, |val, b| {
+ if *b == b' ' {
+ Ok(val + 1)
+ } else {
+ // Only an "error" in the sense
+ // we want try_fold to terminate.
+ Err(val)
+ }
+ })
+ .map_or_else(convert::identity, convert::identity);
+ }
+ /// Evaluates addition expressions as defined in the calc language.
+ /// 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> {
+ let mut left = self.get_mults()?;
+ let mut j;
+ self.consume_ws();
+ while let Some(i) = self.utf8.get(self.i) {
+ j = *i;
+ self.consume_ws();
+ if j == b'+' {
+ self.i += 1;
+ self.consume_ws();
+ left += self.get_mults()?;
+ } else if j == b'-' {
+ self.i += 1;
+ self.consume_ws();
+ left -= self.get_mults()?;
+ } else {
+ break;
+ }
+ }
+ Ok(left)
+ }
+ /// Evaluates multiplication expressions as defined in the calc language.
+ /// 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> {
+ let mut left = self.get_neg()?;
+ let mut right;
+ let mut j;
+ self.consume_ws();
+ while let Some(i) = self.utf8.get(self.i) {
+ j = *i;
+ self.consume_ws();
+ if j == b'*' {
+ self.i += 1;
+ self.consume_ws();
+ left *= self.get_neg()?;
+ } else if j == b'/' {
+ self.i += 1;
+ self.consume_ws();
+ right = self.get_neg()?;
+ if right.numer().sign() == Sign::NoSign {
+ return Err(DivByZero(self.i));
+ }
+ left /= right;
+ } else {
+ break;
+ }
+ }
+ Ok(left)
+ }
+ /// 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> {
+ let mut count = 0usize;
+ while let Some(b) = self.utf8.get(self.i) {
+ if *b == b'-' {
+ self.i += 1;
+ self.consume_ws();
+ count += 1;
+ } else {
+ break;
+ }
+ }
+ self.get_exps()
+ .map(|val| if count & 1 == 0 { val } else { -val })
+ }
+ /// 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>, E> {
+ let mut t = self.get_fact()?;
+ let ix = self.exp_cache.len();
+ let mut prev;
+ let mut numer;
+ self.exp_cache.push(t);
+ self.consume_ws();
+ let mut j;
+ while let Some(i) = self.utf8.get(self.i) {
+ j = *i;
+ self.consume_ws();
+ if j == b'^' {
+ 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();
+ numer = prev.numer();
+ // Equiv to checking if prev is 0.
+ if numer.sign() == Sign::NoSign {
+ if t.numer().sign() == Sign::Minus {
+ return Err(ExpDivByZero(self.i));
+ }
+ self.exp_cache.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);
+ } else {
+ return Err(ExpIsNotInt(self.i));
+ }
+ } else if t.is_integer() {
+ self.exp_cache.push(t);
+ } else {
+ return Err(ExpIsNotInt(self.i));
+ }
+ } else {
+ break;
+ }
+ }
+ self.exp_cache.drain(ix..).try_rfold(
+ Ratio::from_integer(BigInt::new(Sign::Plus, vec![1])),
+ |exp, base| {
+ if exp.is_integer() {
+ Ok(base.pow(exp.numer()))
+ } else if base.numer().sign() == Sign::NoSign {
+ Ok(base)
+ } else {
+ Err(ExpIsNotInt(self.i))
+ }
+ },
+ )
+ }
+ /// 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> {
+ /// Calculates the factorial of `val`.
+ #[inline]
+ fn fact(mut val: BigUint) -> BigUint {
+ let zero = BigUint::new(Vec::new());
+ let one = BigUint::new(vec![1]);
+ let mut calc = BigUint::new(vec![1]);
+ while val > zero {
+ calc *= &val;
+ val -= &one;
+ }
+ calc
+ }
+ let t = self.get_term()?;
+ let Some(b) = self.utf8.get(self.i) else {
+ return Ok(t)
+ };
+ if *b == b'!' {
+ self.i += 1;
+ if t.is_integer() {
+ // 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)), |val| {
+ let mut factorial = fact(val);
+ while let Some(b2) = self.utf8.get(self.i) {
+ if *b2 == b'!' {
+ self.i += 1;
+ factorial = fact(factorial);
+ } else {
+ break;
+ }
+ }
+ Ok(Ratio::from_integer(BigInt::from_biguint(
+ Sign::Plus,
+ factorial,
+ )))
+ })
+ } else {
+ Err(NotNonNegIntFact(self.i))
+ }
+ } else {
+ Ok(t)
+ }
+ }
+ /// Evaluates terminal expressions as defined in the calc language.
+ /// This function is based on parenthetical expressions, absolute value
+ /// expressions, recall expressions, and numer literal expressions.
+ #[inline]
+ #[allow(clippy::option_if_let_else)]
+ fn get_term(&mut self) -> Result<Ratio<BigInt>, E> {
+ match self.get_rational() {
+ Ok(o) => match o {
+ Some(r) => Ok(r),
+ None => match self.get_prev() {
+ Ok(o2) => match o2 {
+ Some(r2) => Ok(r2.clone()),
+ None => self
+ .get_abs()
+ .and_then(|o3| o3.map_or_else(|| self.get_par(), Ok)),
+ },
+ Err(e2) => Err(e2),
+ },
+ },
+ Err(e) => Err(e),
+ }
+ }
+ /// Evaluates parenthetical expressions as defined in the calc language.
+ /// 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`.
+ #[inline]
+ fn get_par(&mut self) -> Result<Ratio<BigInt>, E> {
+ // 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) else {
+ return Err(MissingTerm(self.i));
+ };
+ // This is always the last terminal expression we call; so if there is a missing open parenthesis,
+ // then the sentence is grammatically incorrect.
+ if *b == b'(' {
+ self.i += 1;
+ self.consume_ws();
+ let r = self.get_adds()?;
+ self.consume_ws();
+ let Some(b2) = self.utf8.get(self.i) else {
+ return Err(InvalidPar(self.i))
+ };
+ let b3 = *b2;
+ if b3 == b')' {
+ self.i += 1;
+ Ok(r)
+ } else {
+ Err(InvalidPar(self.i))
+ }
+ } else {
+ Err(MissingTerm(self.i))
+ }
+ }
+ /// 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> {
+ let Some(b) = self.utf8.get(self.i) else {
+ return Ok(None)
+ };
+ if *b == b'|' {
+ self.i += 1;
+ self.consume_ws();
+ let r = self.get_adds()?;
+ self.consume_ws();
+ let Some(b2) = self.utf8.get(self.i) else {
+ return Err(InvalidAbs(self.i))
+ };
+ let b3 = *b2;
+ if b3 == b'|' {
+ self.i += 1;
+ Ok(Some(if r.numer().sign() == Sign::Minus {
+ -r
+ } else {
+ r
+ }))
+ } else {
+ Err(InvalidAbs(self.i))
+ }
+ } else {
+ Ok(None)
+ }
+ }
+ /// Obtains the index within `self.prev` that a recall
+ /// expression is to return. This is a separate function
+ /// from [`get_prev`] due to [`get_recall`] also using this
+ /// logic.
+ #[inline]
+ #[allow(clippy::as_conversions)]
+ fn get_prev_idx(&mut self) -> Option<usize> {
+ let Some(b) = self.utf8.get(self.i) else {
+ return None
+ };
+ (*b == b'@').then(|| {
+ self.i += 1;
+ self.utf8.get(self.i).map_or(0, |b2| {
+ if (b'1'..b'9').contains(b2) {
+ self.i += 1;
+ (*b2 - b'1') as usize
+ } else {
+ 0
+ }
+ })
+ })
+ }
+ /// Evaluates recall expressions as defined in the calc language.
+ #[inline]
+ #[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
+ fn get_prev(&mut self) -> Result<Option<&Ratio<BigInt>>, E> {
+ self.get_prev_idx().map_or_else(|| Ok(None), |i| {
+ self.prev
+ .get(i)
+ .map_or_else(|| Err(NotEnoughPrevResults(self.prev.len() as u8)), |r| Ok(Some(r)))
+ })
+ }
+ /// Evaluates recall statements as defined in the calc language.
+ /// This function has to be separate from [`get_prev`] since it returns
+ /// a shared reference with 'cache lifetime which requires the function to move self
+ /// since it has an exclusive reference to the cache.
+ #[inline]
+ #[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
+ fn get_recall(mut self) -> Result<&'cache Ratio<BigInt>, E> {
+ self.get_prev_idx().map_or_else(|| Err(InvalidRecall), move |i| {
+ self.consume_ws();
+ self.prev.get(i).map_or_else(|| Err(NotEnoughPrevResults(self.prev.len() as u8)), |val| {
+ if self.i == self.utf8.len() {
+ Ok(val)
+ } else {
+ Err(InvalidRecall)
+ }
+ })
+ })
+ }
+ /// Evaluates number literal expressions as defined in the calc language.
+ #[inline]
+ #[allow(
+ clippy::as_conversions,
+ clippy::cast_lossless,
+ clippy::indexing_slicing
+ )]
+ fn get_rational(&mut self) -> Result<Option<Ratio<BigInt>>, E> {
+ #[inline]
+ /// Used to parse a sequence of digits into an unsigned integer.
+ fn to_biguint(v: &[u8]) -> (BigUint, usize) {
+ v.into_iter()
+ .try_fold((BigUint::new(Vec::new()), 0), |mut prev, d| {
+ if d.is_ascii_digit() {
+ prev.1 += 1;
+ prev.0 = prev.0 * 10u8 + (*d - b'0');
+ Ok(prev)
+ } else {
+ Err(prev)
+ }
+ })
+ .map_or_else(convert::identity, convert::identity)
+ }
+ let (int, len) = to_biguint(&self.utf8[self.i..]);
+ if len == 0 {
+ return Ok(None);
+ }
+ self.i += len;
+ if let Some(b) = self.utf8.get(self.i) {
+ if *b == b'.' {
+ self.i += 1;
+ let (numer, len2) = to_biguint(&self.utf8[self.i..]);
+ if len2 == 0 {
+ Err(InvalidDec(self.i))
+ } else {
+ self.i += len2;
+ Ok(Some(
+ Ratio::from_integer(BigInt::from_biguint(Sign::Plus, int))
+ + Ratio::new(
+ BigInt::from_biguint(Sign::Plus, numer),
+ BigInt::from_biguint(Sign::Plus, BigUint::new(vec![10]).pow(len2)),
+ ),
+ ))
+ }
+ } else {
+ Ok(Some(Ratio::from_integer(BigInt::from_biguint(
+ Sign::Plus,
+ int,
+ ))))
+ }
+ } else {
+ Ok(Some(Ratio::from_integer(BigInt::from_biguint(
+ Sign::Plus,
+ int,
+ ))))
+ }
+ }
+}
+#[cfg(test)]
+mod tests {
+ use super::*;
+ #[test]
+ fn prev_string() {
+ assert!(PrevVal { val: &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![12]))), round: None }.to_string() == "> 12\n");
+ assert!(PrevVal { val: &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![12]))), round: Some(0) }.to_string() == "> 12\n");
+ assert!(PrevVal { val: &Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![12]))), round: Some(1) }.to_string() == "> -12.0\n");
+ assert!(PrevVal { val: &Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3]))), round: None }.to_string() == "> 2/3\n");
+ assert!(PrevVal { val: &Ratio::new(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![2])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3]))), round: Some(0)}.to_string() == "> -1\n");
+ assert!(PrevVal { val: &Ratio::new(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![2])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3]))), round: Some(1)}.to_string() == "> -0.7\n");
+ assert!(PrevVal { val: &Ratio::new(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![4])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![6]))), round: Some(2)}.to_string() == "> -0.67\n");
+ }
+ #[test]
+ fn number_literal() {
+ // Normal 0 is fine.
+ assert!(Evaluator::new(b"0", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::NoSign, BigUint::new(Vec::new()))));
+ // Leading 0s and trailing 0s are fine.
+ assert!(Evaluator::new(b"0000.00000", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::NoSign, BigUint::new(Vec::new()))));
+ // Parsing stops at first non-(digt/period).
+ assert!(Evaluator::new(b"1 0", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ let int = b"3397450981271938475135134759823759835414";
+ let frac = b"913759810573549872354897210539127530981570";
+ let left = Ratio::from_integer(BigInt::parse_bytes(int, 10).unwrap());
+ let right = Ratio::new(BigInt::parse_bytes(frac, 10).unwrap(), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![10]).pow(frac.len() + 1)));
+ let mut vec = Vec::new();
+ vec.extend_from_slice(int);
+ vec.push(b'.');
+ vec.push(b'0');
+ vec.extend_from_slice(frac);
+ // Test a number whose integer and fraction portions are larger than u128::MAX.
+ assert!(Evaluator::new(vec.as_slice(), &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().unwrap() == left + right);
+ // Leading 0s and trailing 0s for a non-zero value are fine.
+ assert!(Evaluator::new(b"000000014.0000000000000", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![14]))));
+ // A sequence of digits followed immediately by a decimal point but no digits after is invalid.
+ assert!(match Evaluator::new(b"1.", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap_err() {
+ E::InvalidDec(i) => i == 2,
+ _ => false,
+ });
+ // A sequence of digits followed immediately by a decimal point and space but no digits after is invalid.
+ // This just shows that spaces are not ignored in number literals.
+ assert!(match Evaluator::new(b"1. 2", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap_err() {
+ E::InvalidDec(i) => i == 2,
+ _ => false,
+ });
+ // A non-space starting the input is valid but produces no value.
+ // This also shows that an invalid byte sequence does not produce an error here.
+ assert!(Evaluator::new(b"a1", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().is_none());
+ // A space starting the input is valid but produces no value.
+ assert!(Evaluator::new(b" 1", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().is_none());
+ // Negative literals don't exist, so this should succeed but produce nothing.
+ assert!(Evaluator::new(b"-1", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().is_none());
+ // '/' is division and not part of a number literal so only the "numerator" is parsed.
+ assert!(Evaluator::new(b"1/2", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ // A sequence of digits followed by invalid bytes is valid and produces a number equal to the digits before the invalid bytes.
+ assert!(Evaluator::new(b"130alj", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![130]))));
+ }
+ #[test]
+ fn recall_expression() {
+ // If the input does not start with '@', then it's valid but produces nothing.
+ assert!(Evaluator::new(b"1", &mut Cache::new(), &mut Vec::new()).get_prev().unwrap().is_none());
+ assert!(Evaluator::new(b"a", &mut Cache::new(), &mut Vec::new()).get_prev().unwrap().is_none());
+ assert!(Evaluator::new(b" @", &mut Cache::new(), &mut Vec::new()).get_prev().unwrap().is_none());
+ // Invalid recall expression since there are no previous results.
+ assert!(match Evaluator::new(b"@", &mut Cache::new(), &mut Vec::new()).get_prev().unwrap_err() {
+ E::NotEnoughPrevResults(count) => count == 0,
+ _ => false,
+ });
+ // Invalid recall expression since there are no previous results.
+ assert!(match Evaluator::new(b"@4", &mut Cache::new(), &mut Vec::new()).get_prev().unwrap_err() {
+ E::NotEnoughPrevResults(count) => count == 0,
+ _ => false,
+ });
+ // Invalid recall expression since there are no previous results.
+ // The input violates our grammar, but this error happens before that.
+ assert!(match Evaluator::new(b"@0", &mut Cache::new(), &mut Vec::new()).get_prev().unwrap_err() {
+ E::NotEnoughPrevResults(count) => count == 0,
+ _ => false,
+ });
+ // Successfully extract previous expression.
+ let mut cache = Cache::new();
+ Evaluator::new(b"1\n", &mut cache, &mut Vec::new()).evaluate().unwrap();
+ assert!(Evaluator::new(b"@", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ // Invalid characters are ignored at this stage.
+ assert!(Evaluator::new(b"@&", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ // 0 is not a valid previous value only 1-8 are.
+ assert!(Evaluator::new(b"@0", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ // 9 is not a valid previous value only 1-8 are.
+ assert!(Evaluator::new(b"@9", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &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 previous value.
+ assert!(Evaluator::new(b"@ 2", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ // One digits are looked at so this is not @<ten>.
+ assert!(Evaluator::new(b"@10", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ // Invalid recall expression since there are no previous results.
+ assert!(match Evaluator::new(b"@2", &mut cache, &mut Vec::new()).get_prev().unwrap_err() {
+ E::NotEnoughPrevResults(count) => count == 1,
+ _ => false,
+ });
+ Evaluator::new(b"2\n", &mut cache, &mut Vec::new()).evaluate().unwrap();
+ // Previous value is correct.
+ assert!(Evaluator::new(b"@", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))));
+ // Second previous value is correct.
+ assert!(Evaluator::new(b"@2", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ // Invalid recall expression since there are no previous results.
+ assert!(match Evaluator::new(b"@3", &mut cache, &mut Vec::new()).get_prev().unwrap_err() {
+ E::NotEnoughPrevResults(count) => count == 2,
+ _ => false,
+ });
+ let mut v = vec![0, b'\n'];
+ for i in b'3'..=b'8' {
+ v[0] = i;
+ Evaluator::new(v.as_slice(), &mut cache, &mut Vec::new()).evaluate().unwrap();
+ }
+ v[0] = b'@';
+ for i in b'1'..=b'8' {
+ v[1] = i;
+ // Cache is filled up correctly storing all previous values.
+ assert!(Evaluator::new(v.as_slice(), &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![(b'9' - i) as u32]))));
+ }
+ // Only parses the first @ since the second @ is not a digit between 1 and 8.
+ assert!(Evaluator::new(b"@@", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![8]))));
+ Evaluator::new(b"9\n", &mut cache, &mut Vec::new()).evaluate().unwrap();
+ // Oldest value is overwritten; all others remain.
+ for i in b'1'..=b'8' {
+ v[1] = i;
+ assert!(Evaluator::new(v.as_slice(), &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![((b'9' + 1) - i) as u32]))));
+ }
+ }
+ #[test]
+ fn abs() {
+ // Missing closing '|'
+ assert!(match Evaluator::new(b"|1", &mut Cache::new(), &mut Vec::new()).get_abs().unwrap_err() {
+ E::InvalidAbs(i) => i == 2,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"||1 + 2|", &mut Cache::new(), &mut Vec::new()).get_abs().unwrap_err() {
+ E::InvalidAbs(i) => i == 8,
+ _ => false,
+ });
+ assert!(Evaluator::new(b"| 0 |", &mut Cache::new(), &mut Vec::new()).get_abs().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::NoSign, BigUint::new(Vec::new()))));
+ assert!(Evaluator::new(b"| - 5 |", &mut Cache::new(), &mut Vec::new()).get_abs().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![5]))));
+ assert!(Evaluator::new(b"| | 2 - 5| * 9 |", &mut Cache::new(), &mut Vec::new()).get_abs().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![27]))));
+ // If the input does not start with '|', then it's valid but produces nothing.
+ assert!(Evaluator::new(b" |9|", &mut Cache::new(), &mut Vec::new()).get_abs().unwrap().is_none());
+ }
+ #[test]
+ fn par() {
+ // Missing closing ')'
+ assert!(match Evaluator::new(b"(1", &mut Cache::new(), &mut Vec::new()).get_par().unwrap_err() {
+ E::InvalidPar(i) => i == 2,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"((1 + 2)", &mut Cache::new(), &mut Vec::new()).get_par().unwrap_err() {
+ E::InvalidPar(i) => i == 8,
+ _ => false,
+ });
+ assert!(Evaluator::new(b"( 0 )", &mut Cache::new(), &mut Vec::new()).get_par().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::NoSign, BigUint::new(Vec::new()))));
+ assert!(Evaluator::new(b"( - 5 )", &mut Cache::new(), &mut Vec::new()).get_par().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![5]))));
+ assert!(Evaluator::new(b"( ( 2 - 5) * 9 )", &mut Cache::new(), &mut Vec::new()).get_par().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![27]))));
+ // If the input does not start with '(', then it's invalid since get_par must only be called as the last terminal expression which means whitespace must be consumed already.
+ assert!(match Evaluator::new(b" (2)", &mut Cache::new(), &mut Vec::new()).get_par().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ }
+ #[test]
+ fn term() {
+ assert!(Evaluator::new(b"0000.00000", &mut Cache::new(), &mut Vec::new()).get_term().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::NoSign, BigUint::new(Vec::new()))));
+ assert!(Evaluator::new(b"|4|", &mut Cache::new(), &mut Vec::new()).get_term().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4]))));
+ assert!(Evaluator::new(b"(4)", &mut Cache::new(), &mut Vec::new()).get_term().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4]))));
+ // Terminal expressions do no clean up before or after.
+ assert!(match Evaluator::new(b" 2", &mut Cache::new(), &mut Vec::new()).get_term().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ let mut cache = Cache::new();
+ Evaluator::new(b"1\n", &mut cache, &mut Vec::new()).evaluate().unwrap();
+ assert!(Evaluator::new(b"@", &mut cache, &mut Vec::new()).get_term().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ }
+ #[test]
+ fn factorial() {
+ // Negative integer is not allowed.
+ assert!(match Evaluator::new(b"(-1)!", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap_err() {
+ E::NotNonNegIntFact(i) => i == 5,
+ _ => false,
+ });
+ // Non-integer is not allowed.
+ assert!(match Evaluator::new(b"2.5!", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap_err() {
+ E::NotNonNegIntFact(i) => i == 4,
+ _ => false,
+ });
+ // factorials always become terminal expressions eventually.
+ assert!(Evaluator::new(b"7", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![7]))));
+ assert!(Evaluator::new(b"(7)", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![7]))));
+ assert!(Evaluator::new(b"|7|", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![7]))));
+ let mut cache = Cache::new();
+ Evaluator::new(b"3\n", &mut cache, &mut Vec::new()).evaluate().unwrap();
+ assert!(Evaluator::new(b"@!", &mut cache, &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![6]))));
+ assert!(Evaluator::new(b"@", &mut cache, &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3]))));
+ // 0! = 1.
+ assert!(Evaluator::new(b"0.0!", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ // 1! = 1.
+ assert!(Evaluator::new(b"1!", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ // 4! = 24, and spaces are not consumed.
+ assert!(Evaluator::new(b"4! ", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![24]))));
+ // Factorials can be chained.
+ assert!(Evaluator::new(b"3!! ", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![720]))));
+ // only factorial is consumed.
+ assert!(Evaluator::new(b"2!+3", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))));
+ // Error since leading/trailing whitespace is not consumed by factorial or higher precedence expressions.
+ assert!(match Evaluator::new(b" 2!", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ // Error since leading/trailing whitespace is not consumed by factorial or higher precedence expressions.
+ assert!(match Evaluator::new(b"-2!", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ }
+ #[test]
+ fn exp() {
+ // 1 can be raised to anything and return 1.
+ // Also white space is ignored between operator.
+ assert!(Evaluator::new(b"1 ^ 0", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ assert!(Evaluator::new(b"1^0.5", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ assert!(Evaluator::new(b"1^(-1/2)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ assert!(Evaluator::new(b"1.0^(-2)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ // 0 can be raised to any non-negative value and will always return 0 unless raised to 0 which will return 1.
+ assert!(Evaluator::new(b"0^0", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ assert!(Evaluator::new(b"0^1", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::NoSign, BigUint::new(vec![0]))));
+ assert!(Evaluator::new(b"0^0.5", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::NoSign, BigUint::new(vec![0]))));
+ // Anything else can only be raised to integers.
+ assert!(Evaluator::new(b"4^0", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ assert!(Evaluator::new(b"4^1", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4]))));
+ assert!(Evaluator::new(b"4^(-2)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![16]))));
+ assert!(Evaluator::new(b"(-4)^0", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ assert!(Evaluator::new(b"(-4)^1", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![4]))));
+ assert!(Evaluator::new(b"(-4)^2", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![16]))));
+ assert!(Evaluator::new(b"(-4)^(-2)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![16]))));
+ assert!(Evaluator::new(b"(-4)^(-3)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![1])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![64]))));
+ assert!(Evaluator::new(b"(2/3)^0", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ assert!(Evaluator::new(b"(2/3)^(2)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![9]))));
+ assert!(Evaluator::new(b"(2/3)^(-3)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![27])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![8]))));
+ assert!(Evaluator::new(b"(-2/3)^0", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ assert!(Evaluator::new(b"(-2/3)^(2)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![9]))));
+ assert!(Evaluator::new(b"(-2/3)^(3)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![8])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![27]))));
+ assert!(Evaluator::new(b"(-2/3)^(-2)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![9])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4]))));
+ assert!(Evaluator::new(b"(-2/3)^(-3)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![27])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![8]))));
+ // Error since 0 cannot be raised to a negative power.
+ assert!(match Evaluator::new(b"0^(-1)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap_err() {
+ E::ExpDivByZero(i) => i == 6,
+ _ => false,
+ });
+ // Error anything other than 0 or 1 cannot be raised to a non-integer power.
+ assert!(match Evaluator::new(b"2^(1/2)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap_err() {
+ E::ExpIsNotInt(i) => i == 7,
+ _ => false,
+ });
+ // exps always become factorials eventually.
+ assert!(Evaluator::new(b"3!", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![6]))));
+ // exponentiation has lower precedence than factorials.
+ assert!(Evaluator::new(b"2^3!", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![64]))));
+ assert!(Evaluator::new(b"3!^2", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![36]))));
+ // Error since leading/trailing whitespace is not consumed by exponentiation or higher precedence expressions.
+ assert!(match Evaluator::new(b" 2^2", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ }
+ #[test]
+ fn neg() {
+ assert!(Evaluator::new(b"-1", &mut Cache::new(), &mut Vec::new()).get_neg().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![1]))));
+ assert!(Evaluator::new(b"- - 1", &mut Cache::new(), &mut Vec::new()).get_neg().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ assert!(Evaluator::new(b"-0", &mut Cache::new(), &mut Vec::new()).get_neg().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::NoSign, BigUint::new(Vec::new()))));
+ // negation has lower precedence than exponentiation.
+ assert!(Evaluator::new(b"-2^2", &mut Cache::new(), &mut Vec::new()).get_neg().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![4]))));
+ // negation always becomes exponentiation eventually.
+ assert!(Evaluator::new(b"2.0^2.0", &mut Cache::new(), &mut Vec::new()).get_neg().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4]))));
+ // Error since leading/trailing whitespace is not consumed by exponentiation or higher precedence expressions.
+ assert!(match Evaluator::new(b" -2", &mut Cache::new(), &mut Vec::new()).get_neg().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ }
+ #[test]
+ fn mult() {
+ assert!(Evaluator::new(b"2 * 3", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![6]))));
+ assert!(Evaluator::new(b"-2 * 3", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![6]))));
+ assert!(Evaluator::new(b"2 * -3.0", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![6]))));
+ assert!(Evaluator::new(b"-2.5*-3.5", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![35])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4]))));
+ assert!(Evaluator::new(b"4.0 / 6", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3]))));
+ assert!(Evaluator::new(b"6/3", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))));
+ assert!(Evaluator::new(b"-6/3", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![2]))));
+ assert!(Evaluator::new(b"6/-3", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![2]))));
+ assert!(Evaluator::new(b"- 6 / - 3", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))));
+ // Number literals are not strictly equivalent to "ratios" as "ratios" don't exist (i.e., 2/3 is not the ratio of 2 to 3 but is the rational number two divided by the rational number 3).
+ assert!(Evaluator::new(b"1/1.5", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() != Evaluator::new(b"1/3/2", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap());
+ assert!(Evaluator::new(b"1/1.5", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Evaluator::new(b"1/(3/2)", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap());
+ // multiplication always becomes negation eventually.
+ assert!(Evaluator::new(b"-2.0", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![2]))));
+ // Error since leading/trailing whitespace is not consumed by multiplication or higher precedence expressions.
+ assert!(match Evaluator::new(b" 2*2", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b" 2/2", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ // Cannot divide by 0.
+ assert!(match Evaluator::new(b"2/0", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap_err() {
+ E::DivByZero(i) => i == 3,
+ _ => false,
+ });
+ // multiplication has lower precedence than exponentiation.
+ assert!(Evaluator::new(b"2*2^2", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![8]))));
+ assert!(Evaluator::new(b"8/2^2", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))));
+ }
+ #[test]
+ fn add() {
+ assert!(Evaluator::new(b"2 + 3", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![5]))));
+ assert!(Evaluator::new(b"-2 + 3", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ assert!(Evaluator::new(b"2 + -3.0", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![1]))));
+ assert!(Evaluator::new(b"-2.5+-3.5", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![6]))));
+ assert!(Evaluator::new(b"4.0 - 6", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![2]))));
+ assert!(Evaluator::new(b"6-3", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3]))));
+ assert!(Evaluator::new(b"-6-3", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![9]))));
+ assert!(Evaluator::new(b"6--3", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![9]))));
+ assert!(Evaluator::new(b"- 6 - - 3", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![3]))));
+ // addition always becomes multiplication eventually.
+ assert!(Evaluator::new(b"2 * 8", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![16]))));
+ assert!(Evaluator::new(b"8 / 2", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4]))));
+ // Error since leading/trailing whitespace is not consumed by addition or higher precedence expressions.
+ assert!(match Evaluator::new(b" 2+2", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b" 2-2", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ // addition has lower precedence than multiplication.
+ assert!(Evaluator::new(b"2+2*2", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![6]))));
+ assert!(Evaluator::new(b"2+2/2", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3]))));
+ assert!(Evaluator::new(b"2-2*2", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![2]))));
+ assert!(Evaluator::new(b"2-2/2", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))));
+ }
+ #[test]
+ fn empty() {
+ assert!(match Evaluator::new(b" \n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap() {
+ O::Empty => true,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap() {
+ O::Empty => true,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"\r\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"\t\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"\n\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"\n ", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() {
+ E::MissingLF => true,
+ _ => false,
+ });
+ }
+ #[test]
+ fn exit() {
+ assert!(match Evaluator::new(b" q \n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap() {
+ O::Exit=> true,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"q\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap() {
+ O::Exit=> true,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"\rq\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"\tq\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"q\n\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() {
+ E::InvalidQuit => true,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"\nq\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"q", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() {
+ E::MissingLF => true,
+ _ => false,
+ });
+ }
+ #[test]
+ fn recall_statement() {
+ let mut cache = Cache::new();
+ Evaluator::new(b"1\n", &mut cache, &mut Vec::new()).evaluate().unwrap();
+ assert!(match Evaluator::new(b" = @ \n", &mut cache, &mut Vec::new()).evaluate().unwrap() {
+ O::Prev(v)=> v.val == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) && v.round.is_none(),
+ _ => false,
+ });
+ assert!(match Evaluator::new(b" = 2@ \n", &mut cache, &mut Vec::new()).evaluate().unwrap() {
+ O::Prev(v)=> v.val == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) && v.round.unwrap() == 2,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"=9@1\n", &mut cache, &mut Vec::new()).evaluate().unwrap() {
+ O::Prev(v)=> v.val == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) && v.round.unwrap() == 9,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"=0@1\n", &mut cache, &mut Vec::new()).evaluate().unwrap() {
+ O::Prev(v)=> v.val == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) && v.round.unwrap() == 0,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"=@2\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() {
+ E::NotEnoughPrevResults(count)=> count == 0,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"=@ 1\n", &mut cache, &mut Vec::new()).evaluate().unwrap_err() {
+ E::InvalidRecall => true,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"=0 @1\n", &mut cache, &mut Vec::new()).evaluate().unwrap_err() {
+ E::InvalidRecall => true,
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"=@", &mut cache, &mut Vec::new()).evaluate().unwrap_err() {
+ E::MissingLF=> true,
+ _ => false,
+ });
+ }
+ #[test]
+ fn eval() {
+ use core::str::FromStr;
+ let mut cache = Cache::new();
+ let mut exp = Vec::new();
+ assert!(match Evaluator::new(b"1+2\n", &mut cache, &mut exp).evaluate().unwrap() {
+ O::Val(i) => i == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3]))),
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"-1/2+2*@\n", &mut cache, &mut exp).evaluate().unwrap() {
+ O::Val(i) => i == &Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![11])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))),
+ _ => false,
+ });
+ assert!(match Evaluator::new(b"@^@2!\n", &mut cache, &mut exp).evaluate().unwrap() {
+ O::Val(i) => i == &Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1771561])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![64]))),
+ _ => false,
+ });
+ // Verified with Wolfram Alpha.
+ assert!(match Evaluator::new(b" 1 + (2 * |(7.98 - 12/7)|) / 4!^@3!^|1-3| \n", &mut cache, &mut exp).evaluate().unwrap() {
+ O::Val(i) => i == &Ratio::from_str("2841328814244153299237884950647090899374680152474331/2841328814244153299237884950647090899374680152473600").unwrap(),
+ _ => false,
+ });
+ // Invalid UTF-8 does not cause a panic!.
+ assert!(match Evaluator::new(&[255, 255, 255, b'\n'], &mut cache, &mut exp).evaluate().unwrap_err() {
+ E::MissingTerm(i) => i == 0,
+ _ => false,
+ });
+ assert!(match Evaluator::new(&[b'2', 255, b'\n'], &mut cache, &mut exp).evaluate().unwrap_err() {
+ E::TrailingSyms(i) => i == 1,
+ _ => false,
+ });
+ // Exactly one line feed is required.
+ assert!(match Evaluator::new(&[b'2', b'\n', b'\n'], &mut cache, &mut exp).evaluate().unwrap_err() {
+ E::TrailingSyms(i) => i == 1,
+ _ => false,
+ });
+ assert!(match Evaluator::new(&[b'\n'], &mut cache, &mut exp).evaluate().unwrap() {
+ O::Empty => true,
+ _ => false,
+ });
+ // Empty input does not cause a panic!.
+ assert!(match Evaluator::new(&[], &mut cache, &mut exp).evaluate().unwrap_err() {
+ E::MissingLF => true,
+ _ => false,
+ });
+ }
+}
diff --git a/src/main.rs b/src/main.rs
@@ -0,0 +1,167 @@
+//! This crate is a CLI calculator for basic rational number arithmetic using standard operator precedence and
+//! associativity. Additionally, it stores the previous eight evaluated expressions as well as provides a way to
+//! display previous results in decimal form.
+//!
+//! ```bash
+//! 2.71828^0^3.14159 + -1!
+//! > 0
+//! @^0
+//! > 1
+//! @/3 * 3
+//! > 1
+//! |@2 - 9|^(1 - 2*3)
+//! > 1/32768
+//! =3@
+//! > 0.000
+//! =6@
+//! > 0.000031
+//! ```
+//!
+//! # Operators
+//!
+//! The following are the list of operators in descending order of precedence:
+//! 1. `()`, `||`
+//! 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 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 before and after operators are ignored.
+//!
+//! # 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.
+//!
+//! # Recall expressions
+//!
+//! `@` is used to recall previously evaluated expressions. 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 result where *i ∈ {1, 2, 3, 4, 5, 6, 7, 8}*.
+//! Note that spaces 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.
+//!
+//! # Displaying results in decimal form
+//!
+//! All calculations are done using exact rational number arithmetic; however if one wants to display a previous
+//! result in decimal form, one can use `=<digit>@<1-8>`. For example `=2@` will display the previous result
+//! rounded to two decimal places following normal rounding rules. Note that this is not an expression and so does
+//! not influence previous results as can be recalled by `@`.
+//!
+//! # Character encoding
+//!
+//! All inputs must only contain the UTF-8 encoding of the following Unicode scalar values: `0`-`9`, `.`, `+`, `-`,
+//! `*`, `/`, `^`, `!`, `|`, `(`, `)`, `@`, `=`, <space>, <line feed>, 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,
+//! [`io::Error`][std::io::Error]s that are not of the [`ErrorKind::Interrupted`][std::io::ErrorKind::Interrupted]
+//! variant, and [`fmt::Error`][core::fmt::Error]s all lead to program abortion.
+//!
+//! # Exiting
+//!
+//! `q` with any number of spaces 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(
+ unsafe_code,
+ unused,
+ warnings,
+ clippy::all,
+ clippy::cargo,
+ clippy::nursery,
+ clippy::pedantic
+)]
+#![allow(clippy::implicit_return)]
+use calc_lib::cache::Cache;
+use calc_lib::Evaluator;
+use calc_lib::O::{Empty, Exit, Prev, Val};
+use core::fmt::{self, Debug, Formatter};
+use std::io::{self, BufRead, Error, Write};
+/// Generic unrecoverable error.
+enum GenErr {
+ /// An [`io::Error`][std::io::Error] that is not of the
+ /// [`io::ErrorKind::Interrupted`][std::io::ErrorKind] variant.
+ Error(Error),
+ /// A [`fmt::Error`][core::fmt::Error].
+ Fmt(fmt::Error),
+}
+impl From<Error> for GenErr {
+ #[inline]
+ fn from(value: Error) -> Self {
+ Self::Error(value)
+ }
+}
+impl From<fmt::Error> for GenErr {
+ #[inline]
+ fn from(value: fmt::Error) -> Self {
+ Self::Fmt(value)
+ }
+}
+impl Debug for GenErr {
+ #[inline]
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Error(ref e) => e.fmt(f),
+ Self::Fmt(ref e) => e.fmt(f),
+ }
+ }
+}
+/// Entry point to the calc program.
+fn main() -> Result<(), GenErr> {
+ let mut prev = Cache::new();
+ 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 {
+ in_lock.read_until(b'\n', &mut buffer)?;
+ match Evaluator::new(&buffer, &mut prev, &mut exp_cache).evaluate() {
+ Ok(o) => match o {
+ Empty => (),
+ Exit => return Ok(()),
+ Prev(_) | Val(_) => out_lock.write_all(&o.to_string().into_bytes())?,
+ },
+ Err(e) => out_lock.write_all(&e.to_string().into_bytes())?,
+ }
+ buffer.clear();
+ exp_cache.clear();
+ }
+}