calc_rational

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

commit 6cb9cce1c37157d0f9ea032c86f14e3bac44c933
parent f689ae6e6b031a1ef0048a9b18ac6afc7b5a6dcd
Author: Zack Newman <zack@philomathiclife.com>
Date:   Tue, 25 Feb 2025 08:36:47 -0700

rust 2024

Diffstat:
MCargo.toml | 21++++++++-------------
MREADME.md | 31+++++++++++++++++++++++++++----
Dbuild.rs | 13-------------
Msrc/lending_iterator.rs | 7+++++--
Msrc/lib.rs | 225++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/main.rs | 7+++----
6 files changed, 175 insertions(+), 129 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -3,14 +3,14 @@ authors = ["Zack Newman <zack@philomathiclife.com>"] categories = ["algorithms", "mathematics", "science", "command-line-utilities"] description = "CLI calculator for rational numbers." documentation = "https://crates.io/crates/calc_rational" -edition = "2021" +edition = "2024" keywords = ["calculator", "mathematics", "numerics"] license = "MIT OR Apache-2.0" name = "calc_rational" readme = "README.md" repository = "https://git.philomathiclife.com/repos/calc_rational/" -rust-version = "1.81.0" -version = "2.0.0" +rust-version = "1.85.0" +version = "2.1.0" [lib] name = "calc_lib" @@ -20,33 +20,28 @@ path = "src/lib.rs" name = "calc" path = "src/main.rs" -[badges] -maintenance = { status = "actively-developed" } - [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--cfg", "docsrs"] [dependencies] num-bigint = { version = "0.4.6", default-features = false } num-integer = { version = "0.1.46", default-features = false } num-rational = { version = "0.4.2", default-features = false, features = ["num-bigint"] } num-traits = { version = "0.2.19", default-features = false } -rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"], optional = true } +rand = { version = "0.9.0", default-features = false, features = ["thread_rng"], optional = true } [target.'cfg(target_os = "openbsd")'.dependencies] -priv_sep = { version = "2.1.0", default-features = false, features = ["openbsd"], optional = true } - -[build-dependencies] -rustc_version = { version = "0.4.1", default-features = false } +priv_sep = { version = "2.2.0", default-features = false, features = ["openbsd"], optional = true } ### FEATURES ################################################################# [features] -default = ["priv_sep", "std"] +default = ["priv_sep"] # Provide pledge and unveil for OpenBSD. -priv_sep = ["dep:priv_sep"] +priv_sep = ["dep:priv_sep", "std"] # Provide random functions. rand = ["dep:rand", "std"] diff --git a/README.md b/README.md @@ -1,5 +1,9 @@ # `calc_rational` +[<img alt="git" src="https://git.philomathiclife.com/badges/calc_rational.svg" height="20">](https://git.philomathiclife.com/calc_rational/log.html) +[<img alt="crates.io" src="https://img.shields.io/crates/v/calc_rational.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/calc_rational) +[<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-calc_rational-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs" height="20">](https://docs.rs/calc_rational/latest/calc_lib/) + `calc_rational` consists of a binary crate `calc` and a library crate [`calc_lib`](https://docs.rs/calc_rational/latest/calc_lib). `calc` is a CLI calculator for basic rational number arithmetic using standard operator precedence and associativity. Internally, it is @@ -157,19 +161,38 @@ it will error if [`pledge(2)`](https://man.openbsd.org/amd64/pledge.2) errors wi `q` with any number of spaces and tabs before and after or sending `EOF` will cause the program to terminate. +## Minimum Supported Rust Version (MSRV) + +This will frequently be updated to be the same as stable. Specifically, any time stable is updated and that +update has "useful" features or compilation no longer succeeds (e.g., due to new compiler lints), then MSRV +will be updated. + +MSRV changes will correspond to a SemVer minor version bump. + +## SemVer Policy + +* All on-by-default features of this library are covered by SemVer +* MSRV is considered exempt from SemVer as noted above + ## License Licensed under either of -* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0). -* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT). +* Apache License, Version 2.0 ([LICENSE-APACHE](https://www.apache.org/licenses/LICENSE-2.0)) +* MIT license ([LICENSE-MIT](https://opensource.org/licenses/MIT)) at your option. ## Contribution -Unless you explicitly state otherwise, any contribution intentionally submitted inclusion in the work by you, as -defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, +as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +Before any PR is sent, `cargo clippy` and `cargo t` should be run _for each possible combination of "features"_ +using stable Rust. One easy way to achieve this is by building `ci` and invoking it with no commands in the +`calc_rational` directory or sub-directories. You can fetch `ci` via `git clone https://git.philomathiclife.com/repos/ci`, +and it can be built with `cargo build --release`. Additionally, +`RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features` should be run to ensure documentation can be built. ### Status diff --git a/build.rs b/build.rs @@ -1,13 +0,0 @@ -use rustc_version::{version_meta, Channel}; -fn main() { - println!("cargo::rustc-check-cfg=cfg(channel_dev,channel_nightly,channel_beta,channel_stable)"); - println!( - "cargo::rustc-cfg=channel_{}", - match version_meta().map_or(Channel::Stable, |meta| meta.channel) { - Channel::Dev => "dev", - Channel::Nightly => "nightly", - Channel::Beta => "beta", - Channel::Stable => "stable", - } - ); -} diff --git a/src/lending_iterator.rs b/src/lending_iterator.rs @@ -24,7 +24,7 @@ pub trait LendingIterator { self.lend_fold(0, |count, _| count + 1) } /// Read [`Iterator::advance_by`]. - + /// /// # Errors /// /// Read [`Iterator::advance_by`]. @@ -77,7 +77,10 @@ impl<T> LendingIterator for T where T: Iterator, { - type Item<'a> = T::Item where Self: 'a; + type Item<'a> + = T::Item + where + Self: 'a; #[inline] fn lend_next(&mut self) -> Option<Self::Item<'_>> { self.next() diff --git a/src/lib.rs b/src/lib.rs @@ -1,5 +1,9 @@ -//! # `calc_lib` -//! +//! [![git]](https://git.philomathiclife.com/calc_rational/log.html)&ensp;[![crates-io]](https://crates.io/crates/calc_rational)&ensp;[![docs-rs]](crate) +//! +//! [git]: https://git.philomathiclife.com/git_badge.svg +//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust +//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs +//! //! `calc_lib` is a library for performing basic rational number arithmetic using standard operator precedence //! and associativity. Internally, it is based on //! [`Ratio<T>`] and [`BigInt`]. @@ -107,7 +111,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(all(doc, channel_nightly), feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] #![deny( unknown_lints, future_incompatible, @@ -121,7 +125,6 @@ rust_2024_compatibility, unsafe_code, unused, - unused_crate_dependencies, warnings, clippy::all, clippy::cargo, @@ -139,12 +142,14 @@ reason = "this is a calculator, so this is unavoidable" )] #![expect( + clippy::arbitrary_source_item_ordering, clippy::blanket_clippy_restriction_lints, clippy::indexing_slicing, clippy::exhaustive_enums, clippy::implicit_return, clippy::min_ident_chars, clippy::missing_trait_methods, + clippy::pub_use, clippy::question_mark_used, clippy::ref_patterns, clippy::single_char_lifetime_names, @@ -152,8 +157,14 @@ reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs" )] extern crate alloc; +use LangErr::{ + DivByZero, ExpDivByZero, ExpIsNotIntOrOneHalf, InvalidAbs, InvalidDec, InvalidPar, InvalidQuit, + InvalidRound, InvalidStore, MissingTerm, ModIsNotInt, ModZero, NotEnoughPrevResults, + NotNonNegIntFact, SqrtDoesNotExist, TrailingSyms, +}; +use O::{Empty, Eval, Exit, Store}; use alloc::{ - string::{String, ToString}, + string::{String, ToString as _}, vec, vec::Vec, }; @@ -163,28 +174,25 @@ use core::marker::PhantomData; use core::{ convert, fmt::{self, Display, Formatter}, - ops::Index, + ops::Index as _, }; pub use num_bigint; use num_bigint::{BigInt, BigUint, Sign}; -use num_integer::Integer; +use num_integer::Integer as _; pub use num_rational; use num_rational::Ratio; #[cfg(feature = "rand")] -use num_traits::ToPrimitive; -use num_traits::{Inv, Pow}; +use num_traits::ToPrimitive as _; +use num_traits::{Inv as _, Pow as _}; +#[cfg_attr(docsrs, doc(cfg(feature = "rand")))] #[cfg(feature = "rand")] pub use rand; #[cfg(feature = "rand")] -use rand::{rngs::ThreadRng, RngCore}; -use LangErr::{ - DivByZero, ExpDivByZero, ExpIsNotIntOrOneHalf, InvalidAbs, InvalidDec, InvalidPar, InvalidQuit, - InvalidRound, InvalidStore, MissingTerm, ModIsNotInt, ModZero, NotEnoughPrevResults, - NotNonNegIntFact, SqrtDoesNotExist, TrailingSyms, -}; -use O::{Empty, Eval, Exit, Store}; +use rand::{RngCore as _, rngs::ThreadRng}; /// Fixed-sized cache that automatically overwrites the oldest data -/// when a new item is added and the cache is full. One can think of +/// 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; @@ -247,12 +255,15 @@ pub enum LangErr { /// by symbols that could not be chained with the preceding expression. TrailingSyms(usize), /// The input contained an invalid random expression. + #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] #[cfg(feature = "rand")] InvalidRand(usize), /// Error when the second argument is less than first in the rand function. + #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] #[cfg(feature = "rand")] RandInvalidArgs(usize), /// Error when there are no 64-bit integers in the interval passed to the random function. + #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] #[cfg(feature = "rand")] RandNoInts(usize), } @@ -304,7 +315,7 @@ pub enum O<'a> { /// It is `None` iff there have been no previous `Eval` results. Store(&'a Option<Ratio<BigInt>>), } -impl<'a> Display for O<'a> { +impl Display for O<'_> { #[expect( unsafe_code, reason = "manually construct guaranteed UTF-8; thus avoid the needless check" @@ -429,6 +440,14 @@ pub struct Evaluator<'input, 'cache, 'prev, 'scratch, 'rand> { #[cfg(not(feature = "rand"))] _rng: PhantomData<fn() -> &'rand ()>, } +#[expect( + clippy::allow_attributes, + reason = "can't use expect since it isn't triggered when rand is enabled" +)] +#[allow( + clippy::needless_lifetimes, + reason = "when rand is not enable, 'rand is not needed." +)] impl<'input, 'cache, 'prev, 'scratch, 'rand> Evaluator<'input, 'cache, 'prev, 'scratch, 'rand> { /// Creates an `Evaluator<'input, 'cache, 'prev, 'scratch, 'rand>` based on the supplied arguments. #[cfg(not(feature = "rand"))] @@ -468,15 +487,16 @@ impl<'input, 'cache, 'prev, 'scratch, 'rand> Evaluator<'input, 'cache, 'prev, 's } } /// Evaluates the input consuming the `Evaluator<'input, 'cache, 'exp>`. + /// /// Requires the input to contain one expression (i.e., if there are /// multiple newlines, it will error). - + /// /// # Errors /// /// Returns a [`LangErr`] iff the input violates the calc language. #[inline] pub fn evaluate(mut self) -> Result<O<'prev>, LangErr> { - self.utf8 = if self.utf8.last().map_or(true, |b| *b != b'\n') { + self.utf8 = if self.utf8.last().is_none_or(|b| *b != b'\n') { self.utf8 } else { &self.utf8[..self.utf8.len() @@ -1151,6 +1171,7 @@ impl<'input, 'cache, 'prev, 'scratch, 'rand> Evaluator<'input, 'cache, 'prev, 's } } /// Reads data from `R` passing each line to an [`Evaluator`] to be evaluated. +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] #[cfg(feature = "std")] pub struct EvalIter<R> { /// Reader that contains input data. @@ -1169,11 +1190,13 @@ pub struct EvalIter<R> { #[cfg(feature = "rand")] rng: ThreadRng, } +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] #[cfg(feature = "std")] impl<R> EvalIter<R> { /// Creates a new `EvalIter`. - #[inline] + #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] #[cfg(feature = "rand")] + #[inline] pub fn new(reader: R) -> Self { Self { reader, @@ -1181,12 +1204,12 @@ impl<R> EvalIter<R> { cache: Cache::new(), prev: None, exp_buffer: Vec::new(), - rng: rand::thread_rng(), + rng: rand::rng(), } } /// Creates a new `EvalIter`. - #[inline] #[cfg(any(doc, not(feature = "rand")))] + #[inline] pub fn new(reader: R) -> Self { Self { reader, @@ -1201,8 +1224,9 @@ impl<R> EvalIter<R> { extern crate std; #[cfg(feature = "std")] use std::io::{BufRead, Error}; -#[cfg(feature = "std")] /// Error returned from [`EvalIter`] when an expression has an error. +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +#[cfg(feature = "std")] #[derive(Debug)] pub enum E { /// Error containing [`Error`] which is returned @@ -1225,12 +1249,16 @@ impl Display for E { } #[cfg(feature = "std")] use crate::lending_iterator::LendingIterator; +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] #[cfg(feature = "std")] impl<R> LendingIterator for EvalIter<R> where R: BufRead, { - type Item<'a> = Result<O<'a>, E> where Self: 'a; + type Item<'a> + = Result<O<'a>, E> + where + Self: 'a; #[inline] fn lend_next(&mut self) -> Option<Result<O<'_>, E>> { self.input_buffer.clear(); @@ -1947,7 +1975,7 @@ mod tests { &mut Cache::new(), &mut None, &mut Vec::new(), - &mut rand::thread_rng() + &mut rand::rng() ) .get_rand() .unwrap_err() @@ -1960,7 +1988,7 @@ mod tests { &mut Cache::new(), &mut None, &mut Vec::new(), - &mut rand::thread_rng() + &mut rand::rng() ) .get_rand() .unwrap_err() @@ -1973,7 +2001,7 @@ mod tests { &mut Cache::new(), &mut None, &mut Vec::new(), - &mut rand::thread_rng(), + &mut rand::rng(), ) .get_rand() .unwrap_err() @@ -1986,7 +2014,7 @@ mod tests { &mut Cache::new(), &mut None, &mut Vec::new(), - &mut rand::thread_rng(), + &mut rand::rng(), ) .get_rand() .unwrap_err() @@ -1999,7 +2027,7 @@ mod tests { &mut Cache::new(), &mut None, &mut Vec::new(), - &mut rand::thread_rng(), + &mut rand::rng(), ) .get_rand() .unwrap_err() @@ -2013,7 +2041,7 @@ mod tests { &mut Cache::new(), &mut None, &mut Vec::new(), - &mut rand::thread_rng(), + &mut rand::rng(), ) .get_rand() .unwrap_err() @@ -2021,44 +2049,50 @@ mod tests { MissingTerm(i) => i == 0, _ => false, }); - assert!(Evaluator::new( - b"rand(2, 7)", - &mut Cache::new(), - &mut None, - &mut Vec::new(), - &mut rand::thread_rng() - ) - .get_rand() - .map(|r| { - let int = r.numer(); - int >= &BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2])) - && *int <= BigInt::from_biguint(Sign::Plus, BigUint::new(vec![7])) - }) - .unwrap()); - assert!(Evaluator::new( - b"rand()", - &mut Cache::new(), - &mut None, - &mut Vec::new(), - &mut rand::thread_rng() - ) - .get_rand() - .map(|r| { - let int = r.numer(); - int >= &BigInt::from(i64::MIN) && *int <= BigInt::from(i64::MAX) - }) - .unwrap()); - for _ in 0..100 { - assert!(Evaluator::new( - b"rand(2, 2)", + assert!( + Evaluator::new( + b"rand(2, 7)", &mut Cache::new(), &mut None, &mut Vec::new(), - &mut rand::thread_rng() + &mut rand::rng() ) .get_rand() - .map(|r| *r.numer() == BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))) - .unwrap()); + .map(|r| { + let int = r.numer(); + int >= &BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2])) + && *int <= BigInt::from_biguint(Sign::Plus, BigUint::new(vec![7])) + }) + .unwrap() + ); + assert!( + Evaluator::new( + b"rand()", + &mut Cache::new(), + &mut None, + &mut Vec::new(), + &mut rand::rng() + ) + .get_rand() + .map(|r| { + let int = r.numer(); + int >= &BigInt::from(i64::MIN) && *int <= BigInt::from(i64::MAX) + }) + .unwrap() + ); + for _ in 0..100 { + assert!( + Evaluator::new( + b"rand(2, 2)", + &mut Cache::new(), + &mut None, + &mut Vec::new(), + &mut rand::rng() + ) + .get_rand() + .map(|r| *r.numer() == BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))) + .unwrap() + ); } } #[cfg(feature = "rand")] @@ -2078,7 +2112,7 @@ mod tests { &mut cache, &mut none, &mut vec, - &mut rand::thread_rng(), + &mut rand::rng(), ) .get_rand() .unwrap() @@ -2088,16 +2122,17 @@ mod tests { + 1) as usize] += 1; } // Test that the distribution is within 1% of what is expected. - assert!(vals - .into_iter() - .try_fold(false, |_, r| { - if r < COUNT * 33 / 100 || r > COUNT * 101 / 300 { - Err(false) - } else { - Ok(true) - } - }) - .unwrap()); + assert!( + vals.into_iter() + .try_fold(false, |_, r| { + if r < COUNT * 33 / 100 || r > COUNT * 101 / 300 { + Err(false) + } else { + Ok(true) + } + }) + .unwrap() + ); } #[test] fn term() { @@ -2134,25 +2169,29 @@ mod tests { ) ); #[cfg(feature = "rand")] - assert!(Evaluator::new( - b"rand()", - &mut Cache::new(), - &mut None, - &mut Vec::new(), - &mut rand::thread_rng() - ) - .get_term() - .is_ok()); + assert!( + Evaluator::new( + b"rand()", + &mut Cache::new(), + &mut None, + &mut Vec::new(), + &mut rand::rng() + ) + .get_term() + .is_ok() + ); #[cfg(feature = "rand")] - assert!(Evaluator::new( - b"rand(-13/93, 833)", - &mut Cache::new(), - &mut None, - &mut Vec::new(), - &mut rand::thread_rng(), - ) - .get_term() - .is_ok()); + assert!( + Evaluator::new( + b"rand(-13/93, 833)", + &mut Cache::new(), + &mut None, + &mut Vec::new(), + &mut rand::rng(), + ) + .get_term() + .is_ok() + ); #[cfg(not(feature = "rand"))] assert!( match Evaluator::new(b"rand()", &mut Cache::new(), &mut None, &mut Vec::new()) diff --git a/src/main.rs b/src/main.rs @@ -1,7 +1,6 @@ //! # `calc` //! //! Consult [`README.md`](https://crates.io/crates/calc_rational). -#![cfg_attr(all(doc, channel_nightly), feature(doc_auto_cfg))] #![deny( unknown_lints, future_incompatible, @@ -37,7 +36,7 @@ reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs" )] #[cfg(feature = "std")] -use calc_lib::{lending_iterator::LendingIterator, EvalIter}; +use calc_lib::{EvalIter, lending_iterator::LendingIterator as _}; #[cfg(feature = "std")] use core::fmt::{self, Display, Formatter}; #[cfg(all(feature = "priv_sep", target_os = "openbsd"))] @@ -45,7 +44,7 @@ use priv_sep::{Promise, Promises}; #[cfg(feature = "std")] use std::{ error, - io::{self, Error, Write}, + io::{self, Error, Write as _}, }; /// Error returned by [`main`]. #[cfg(feature = "std")] @@ -54,7 +53,7 @@ enum Err { /// Error returned from [`writeln`]. Io(Error), #[cfg(all(feature = "priv_sep", target_os = "openbsd"))] - /// Error returned from [`pledge`]. + /// Error returned from `pledge`. Pledge(Error), } #[cfg(feature = "std")]