commit 951200277530bc4588f36bcf49a57ccb1d19e5f7
parent 723a956cf8af18f1ebbedce384edbf79f16e60c1
Author: Zack Newman <zack@philomathiclife.com>
Date: Tue, 26 Aug 2025 08:51:55 -0600
cleanup. fix macos compile. more lints. docs
Diffstat:
| M | Cargo.toml | | | 108 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------- |
| D | build.rs | | | 12 | ------------ |
| M | src/lib.rs | | | 216 | ++++++++++++++++++++++++++++++++++++++++---------------------------------------- |
| M | src/rational.rs | | | 28 | +++++----------------------- |
4 files changed, 201 insertions(+), 163 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -3,36 +3,104 @@ authors = ["Zack Newman <zack@philomathiclife.com>"]
categories = ["algorithms", "science", "no-std"]
description = "Extensions for rational numbers."
documentation = "https://docs.rs/rational_extensions"
-edition = "2021"
+edition = "2024"
keywords = ["mathematics", "numerics"]
license = "MIT OR Apache-2.0"
name = "rational_extensions"
readme = "README.md"
repository = "https://git.philomathiclife.com/repos/rational_extensions/"
-version = "0.4.1"
+rust-version = "1.88.0"
+version = "0.4.2"
-[lib]
-name = "rational_extensions"
-path = "src/lib.rs"
+[lints.rust]
+ambiguous_negative_literals = { level = "deny", priority = -1 }
+closure_returning_async_block = { level = "deny", priority = -1 }
+deprecated_safe = { level = "deny", priority = -1 }
+deref_into_dyn_supertrait = { level = "deny", priority = -1 }
+ffi_unwind_calls = { level = "deny", priority = -1 }
+future_incompatible = { level = "deny", priority = -1 }
+#fuzzy_provenance_casts = { level = "deny", priority = -1 }
+impl_trait_redundant_captures = { level = "deny", priority = -1 }
+keyword_idents = { level = "deny", priority = -1 }
+let_underscore = { level = "deny", priority = -1 }
+#linker_messages = { level = "deny", priority = -1 }
+#lossy_provenance_casts = { level = "deny", priority = -1 }
+macro_use_extern_crate = { level = "deny", priority = -1 }
+meta_variable_misuse = { level = "deny", priority = -1 }
+missing_copy_implementations = { level = "deny", priority = -1 }
+missing_debug_implementations = { level = "deny", priority = -1 }
+missing_docs = { level = "deny", priority = -1 }
+#multiple_supertrait_upcastable = { level = "deny", priority = -1 }
+#must_not_suspend = { level = "deny", priority = -1 }
+non_ascii_idents = { level = "deny", priority = -1 }
+#non_exhaustive_omitted_patterns = { level = "deny", priority = -1 }
+nonstandard_style = { level = "deny", priority = -1 }
+redundant_imports = { level = "deny", priority = -1 }
+redundant_lifetimes = { level = "deny", priority = -1 }
+refining_impl_trait = { level = "deny", priority = -1 }
+rust_2018_compatibility = { level = "deny", priority = -1 }
+rust_2018_idioms = { level = "deny", priority = -1 }
+rust_2021_compatibility = { level = "deny", priority = -1 }
+rust_2024_compatibility = { level = "deny", priority = -1 }
+single_use_lifetimes = { level = "deny", priority = -1 }
+#supertrait_item_shadowing_definition = { level = "deny", priority = -1 }
+trivial_casts = { level = "deny", priority = -1 }
+trivial_numeric_casts = { level = "deny", priority = -1 }
+unit_bindings = { level = "deny", priority = -1 }
+unnameable_types = { level = "deny", priority = -1 }
+#unqualified_local_imports = { level = "deny", priority = -1 }
+unreachable_pub = { level = "deny", priority = -1 }
+unsafe_code = { level = "deny", priority = -1 }
+unstable_features = { level = "deny", priority = -1 }
+unused = { level = "deny", priority = -1 }
+unused_crate_dependencies = { level = "deny", priority = -1 }
+unused_import_braces = { level = "deny", priority = -1 }
+unused_lifetimes = { level = "deny", priority = -1 }
+unused_qualifications = { level = "deny", priority = -1 }
+unused_results = { level = "deny", priority = -1 }
+variant_size_differences = { level = "deny", priority = -1 }
+warnings = { level = "deny", priority = -1 }
-[dependencies]
-num-integer = { version = "0.1.45", default-features = false }
-num-rational = { version = "0.4.1", default-features = false }
-num-traits = { version = "0.2.16", default-features = false }
-serde = { version = "1.0.188", default-features = false, optional = true }
+[lints.clippy]
+all = { level = "deny", priority = -1 }
+cargo = { level = "deny", priority = -1 }
+complexity = { level = "deny", priority = -1 }
+correctness = { level = "deny", priority = -1 }
+nursery = { level = "deny", priority = -1 }
+pedantic = { level = "deny", priority = -1 }
+perf = { level = "deny", priority = -1 }
+restriction = { level = "deny", priority = -1 }
+style = { level = "deny", priority = -1 }
+suspicious = { level = "deny", priority = -1 }
+# Noisy, opinionated, and likely don't prevent bugs or improve APIs.
+arbitrary_source_item_ordering = "allow"
+blanket_clippy_restriction_lints = "allow"
+implicit_return = "allow"
+min_ident_chars = "allow"
+missing_trait_methods = "allow"
+exhaustive_enums = "allow"
+exhaustive_structs = "allow"
+question_mark_used = "allow"
+ref_patterns = "allow"
+single_call_fn = "allow"
+single_char_lifetime_names = "allow"
-[build-dependencies]
-rustc_version = "0.4.0"
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = ["--cfg", "docsrs"]
+
+[dependencies]
+num-integer = { version = "0.1.46", default-features = false }
+num-rational = { version = "0.4.2", default-features = false }
+num-traits = { version = "0.2.19", default-features = false }
+serde = { version = "1.0.219", default-features = false, optional = true }
[dev-dependencies]
-serde_json = { version = "1.0.106", default-features = false, features = ["alloc"] }
+serde_json = { version = "1.0.143", default-features = false, features = ["alloc"] }
-[features]
-rational = ["dep:serde"]
-[package.metadata.docs.rs]
-all-features = true
-rustdoc-args = ["--cfg", "docsrs"]
+### FEATURES #################################################################
-[badges]
-maintenance = { status = "passively-maintained" }
+[features]
+# Provide (de)serialization support.
+rational = ["dep:serde"]
diff --git a/build.rs b/build.rs
@@ -1,12 +0,0 @@
-use rustc_version::{version_meta, Channel};
-
-fn main() {
- // Set cfg flags depending on release channel
- let channel = match version_meta().unwrap().channel {
- Channel::Stable => "CHANNEL_STABLE",
- Channel::Beta => "CHANNEL_BETA",
- Channel::Nightly => "CHANNEL_NIGHTLY",
- Channel::Dev => "CHANNEL_DEV",
- };
- println!("cargo:rustc-cfg={}", channel)
-}
diff --git a/src/lib.rs b/src/lib.rs
@@ -2,33 +2,24 @@
//! from a string specifically by allowing decimal notation with the
//! ability to constrain the minimum and maximum number of fractional
//! digits allowed.
-#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))]
+#![cfg_attr(docsrs, feature(doc_cfg))]
#![no_std]
-#![deny(
- unsafe_code,
- unused,
- warnings,
- clippy::all,
- clippy::cargo,
- clippy::nursery,
- clippy::pedantic
-)]
-#![allow(
- clippy::implicit_return,
- clippy::question_mark_used,
- clippy::unseparated_literal_suffix
-)]
extern crate alloc;
use crate::FromDecStrErr::{IntParseErr, TooFewFractionalDigits, TooManyFractionalDigits};
-use alloc::string::{String, ToString};
-use alloc::vec::Vec;
-use core::fmt::{self, Debug, Display, Formatter};
-use core::ops::Mul;
-use core::str::FromStr;
+use alloc::{
+ string::{String, ToString as _},
+ vec::Vec,
+};
+use core::{
+ fmt::{self, Debug, Display, Formatter},
+ ops::Mul,
+ str::FromStr,
+};
use num_integer::Integer;
use num_rational::Ratio;
use num_traits::Pow;
/// An ordered pair whose first value is <= to the second.
+#[derive(Clone, Copy, Debug)]
pub struct MinMax<T> {
/// The first value which is <= the second.
min: T,
@@ -57,11 +48,14 @@ where
(min <= max).then_some(Self { min, max })
}
/// Returns `MinMax` without verifying `min` `<=` `max`.
-
+ ///
/// # Safety
///
/// `min` `<=` `max`.
- #[allow(unsafe_code)]
+ #[expect(
+ unsafe_code,
+ reason = "want to expose a function that does not uphold the invariants"
+ )]
#[inline]
pub const unsafe fn new_unchecked(min: T, max: T) -> Self {
Self { min, max }
@@ -69,7 +63,6 @@ where
}
/// The error returned when parsing a string in decimal notation into
/// a `num_rational::Ratio<T>`.
-#[allow(clippy::exhaustive_enums)]
pub enum FromDecStrErr<T> {
/// Contains the error returned when parsing a string into a `T`.
IntParseErr(T),
@@ -116,17 +109,17 @@ impl<T> From<T> for FromDecStrErr<T> {
}
/// Converts a string in decimal notation into a `Ratio<T>`.
///
+/// # Panics
+///
+/// May `panic` if `T` implements arithmetic in a way where `panic`s occur on overflow or underflow.
+///
/// # Errors
///
-/// Will return `FromDecStrErr` iff `val` is not a valid rational
-/// number in decimal notation with number of fractional digits
-/// inclusively between `frac_digit_count.min()` and
-/// `frac_digit_count.max()`.
-#[allow(
+/// Will return `FromDecStrErr` iff `val` is not a valid rational number in decimal notation with number of
+/// fractional digits inclusively between `frac_digit_count.min()` and `frac_digit_count.max()`.
+#[expect(
clippy::arithmetic_side_effects,
- clippy::indexing_slicing,
- clippy::single_char_lifetime_names,
- clippy::string_slice
+ reason = "calling code's responsibility to ensure T implements arithmetic correctly"
)]
#[inline]
pub fn try_from_dec_str<T>(
@@ -157,7 +150,10 @@ where
let addend = T::from_str(r)?;
let zero = T::from(0);
Ok(Ratio::new(
- if numer < zero || (numer == zero && &l[..1] == "-") {
+ if numer < zero
+ || (numer == zero
+ && l.as_bytes().first().is_some_and(|fst| *fst == b'-'))
+ {
numer - addend
} else {
numer + addend
@@ -175,7 +171,6 @@ where
}
/// The error returned when parsing a string in decimal or
/// rational notation into a `num_rational::Ratio<T>`.
-#[allow(clippy::exhaustive_enums)]
pub enum FromStrErr<T> {
/// Contains the error when a string fails to parse into a `T`.
IntParseErr(T),
@@ -212,15 +207,18 @@ impl<T> From<T> for FromStrErr<T> {
}
/// Converts a string in rational or decimal notation into a `Ratio<T>`.
///
+/// # Panics
+///
+/// May `panic` if `T` implements arithmetic in a way where `panic`s occur on overflow or underflow.
+///
/// # Errors
///
/// Will return `FromStrErr` iff `val` is not a rational number in
/// rational or decimal notation.
-#[allow(
- unsafe_code,
+#[expect(clippy::unreachable, reason = "want to crash when there is a bug")]
+#[expect(
clippy::arithmetic_side_effects,
- clippy::single_char_lifetime_names,
- clippy::unreachable
+ reason = "calling code's responsibility to ensure T implements arithmetic correctly"
)]
#[inline]
pub fn try_from_str<T>(val: &str) -> Result<Ratio<T>, FromStrErr<<T as FromStr>::Err>>
@@ -232,82 +230,73 @@ where
+ for<'a> Mul<&'a T, Output = T>
+ Pow<usize, Output = T>,
{
- /// Used as the closure that transforms a `FromDecStrErr` into a `FromStrErr`.
- #[inline]
- fn dec_err<S>(err: FromDecStrErr<<S as FromStr>::Err>) -> FromStrErr<<S as FromStr>::Err>
- where
- S: FromStr,
- {
- match err {
- IntParseErr(x) => FromStrErr::IntParseErr(x),
- TooFewFractionalDigits(_) | TooManyFractionalDigits(_) => unreachable!("There is a bug in rational::try_from_dec_str. 0 and usize::MAX were passed as the minimum and maximum number of fractional digits allowed respectively, but it still errored due to too few or too many fractional digits"),
- }
- }
- /// Used as the closure that attempts to transform a `String` split from a `/`
- /// into a `Ratio<S>`.
- #[inline]
- fn frac_split<S>(split: (&str, &str)) -> Result<Ratio<S>, FromStrErr<<S as FromStr>::Err>>
- where
- S: Clone + From<u8> + FromStr + Integer + for<'a> Mul<&'a S, Output = S>,
- {
- let denom = S::from_str(split.1)?;
- let zero = S::from(0);
- if denom == zero {
- Err(FromStrErr::DenominatorIsZero)
- } else {
- split.0.split_once(' ').map_or_else(
- || Ok(Ratio::new(S::from_str(split.0)?, denom.clone())),
- |(l2, r2)| {
- let numer = S::from_str(l2)? * &denom;
- let addend = S::from_str(r2)?;
- Ok(Ratio::new(
- if numer < zero {
- numer - addend
- } else {
- numer + addend
- },
- denom.clone(),
- ))
- },
- )
- }
- }
val.split_once('/').map_or_else(
|| {
- // SAFETY:
- // usize::MAX >= 0
- try_from_dec_str(val, &unsafe { MinMax::new_unchecked(0, usize::MAX) })
- .map_err(dec_err::<T>)
+ try_from_dec_str(
+ val,
+ &MinMax {
+ min: 0,
+ max: usize::MAX,
+ },
+ )
+ .map_err(|err| match err {
+ IntParseErr(x) => FromStrErr::IntParseErr(x),
+ TooFewFractionalDigits(_) | TooManyFractionalDigits(_) => unreachable!(
+ "There is a bug in rational::try_from_dec_str. 0 and usize::MAX were passed as the minimum and maximum number of fractional digits allowed respectively, but it still errored due to too few or too many fractional digits"
+ ),
+ })
},
- frac_split,
+ |split| {
+ let denom = T::from_str(split.1)?;
+ let zero = T::from(0);
+ if denom == zero {
+ Err(FromStrErr::DenominatorIsZero)
+ } else {
+ split.0.split_once(' ').map_or_else(
+ || Ok(Ratio::new(T::from_str(split.0)?, denom.clone())),
+ |(l2, r2)| {
+ let numer = T::from_str(l2)? * &denom;
+ let addend = T::from_str(r2)?;
+ Ok(Ratio::new(
+ if numer < zero {
+ numer - addend
+ } else {
+ numer + addend
+ },
+ denom.clone(),
+ ))
+ },
+ )
+ }
+ }
)
}
-/// Returns a `String` representing `val` in decimal notation with
-/// `frac_digit_count` fractional digits using normal rounding rules.
-#[allow(
- unsafe_code,
+/// Returns a `String` representing `val` in decimal notation with `frac_digit_count` fractional digits
+/// using normal rounding rules.
+///
+/// # Panics
+///
+/// May `panic` if `T` implements arithmetic in a way where `panic`s occur on overflow or underflow.
+#[expect(unsafe_code, reason = "comment justifies correctness")]
+#[expect(
clippy::arithmetic_side_effects,
- clippy::as_conversions,
- clippy::cast_lossless,
+ clippy::expect_used,
clippy::indexing_slicing,
- clippy::string_slice
+ reason = "calling code's responsibility to ensure T implements arithmetic correctly"
)]
#[inline]
pub fn to_dec_string<T>(val: &Ratio<T>, frac_digit_count: usize) -> String
where
T: Clone + Display + From<u8> + Integer + Pow<usize, Output = T>,
{
- /// Returns 1 iff `start` is `"-"`; otherwise returns 0.
- /// This function is used as the closure passed to `map_or` when checking
- /// if the first "character" of the fraction string is a negative sign.
- #[inline]
- fn neg_sign(start: &str) -> usize {
- (start == "-") as usize
- }
let mult = T::from(10).pow(frac_digit_count);
let (int, frac) = (val * &mult).round().numer().div_rem(&mult);
let int_str = int.to_string();
- let mut v = Vec::with_capacity(int_str.len() + frac_digit_count + 2);
+ let mut v = Vec::with_capacity(
+ int_str
+ .len()
+ .saturating_add(frac_digit_count.saturating_add(2)),
+ );
let zero = T::from(0);
if int >= zero && frac < zero {
v.push(b'-');
@@ -316,30 +305,41 @@ where
if frac_digit_count > 0 {
v.push(b'.');
let len = v.len();
- let frac_str = frac.to_string();
- let frac_val = &frac_str[frac_str.get(..1).map_or(0, neg_sign)..];
- while v.len() < len + (frac_digit_count - frac_val.len()) {
+ let frac_vec = frac.to_string().into_bytes();
+ // This cannot `panic` since we start at index 0 when it's empty or does not begin with `b'-'`;
+ // otherwise we start at index 1.
+ let frac_val = &frac_vec[frac_vec
+ .first()
+ .map_or(0, |start| usize::from(*start == b'-'))..];
+ // We rely on saturating add. If overflow occurs, then the code will `panic` anyway due to
+ // the below loop causing the underlying `Vec` to be too large.
+ let term = len.saturating_add(
+ frac_digit_count
+ .checked_sub(frac_val.len())
+ .expect("T::to_string returns an unexpected large string"),
+ );
+ while v.len() < term {
v.push(b'0');
}
- v.extend_from_slice(frac_val.as_bytes());
+ 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
+ // `v` contains precisely the UTF-8 code units returned from `String`s
+ // returned from `to_string` on the integer and fraction part of
// the passed value plus optionally the single byte encodings of ".", "-", and "0".
unsafe { String::from_utf8_unchecked(v) }
}
+/// Enables deserialization of strings in decimal or fractional format into [`rational::Rational<T>`].
+#[cfg_attr(docsrs, doc(cfg(feature = "rational")))]
#[cfg(feature = "rational")]
-// Enables deserialization of strings in decimal or fractional format
-// into [`rational::Rational<T>`].
pub mod rational;
#[cfg(test)]
mod tests {
use super::*;
use alloc::format;
- use core::assert_eq;
- use core::num::ParseIntError;
+ use core::{assert_eq, num::ParseIntError};
+ use serde_json as _;
#[test]
fn test_min_max() -> Result<(), String> {
@@ -357,7 +357,7 @@ mod tests {
#[test]
#[should_panic]
fn test_min_max_err() {
- MinMax::new(f64::NAN, 0.0).unwrap();
+ _ = MinMax::new(f64::NAN, 0.0).unwrap();
}
#[test]
fn test_dec_string() -> Result<(), String> {
diff --git a/src/rational.rs b/src/rational.rs
@@ -1,14 +1,4 @@
-#![deny(
- unsafe_code,
- unused,
- warnings,
- clippy::all,
- clippy::cargo,
- clippy::nursery,
- clippy::pedantic
-)]
-#![allow(clippy::implicit_return, clippy::missing_trait_methods)]
-use crate::{try_from_str, Ratio};
+use crate::{Ratio, try_from_str};
use core::fmt::{self, Formatter};
use core::marker::PhantomData;
use core::ops::Mul;
@@ -19,9 +9,8 @@ use serde::de::{self, Deserialize, Deserializer, Unexpected, Visitor};
/// Wrapper around a `num_rational::Ratio` that
/// deserializes a JSON string representing a rational number in
/// fractional or decimal notation to a Ratio<T>.
-#[allow(clippy::exhaustive_structs)]
+#[derive(Clone, Copy, Debug)]
pub struct Rational<T>(pub Ratio<T>);
-#[allow(clippy::single_char_lifetime_names)]
impl<'de, T> Deserialize<'de> for Rational<T>
where
T: Clone
@@ -41,8 +30,7 @@ where
/// Does not own nor drop a `T`.
_x: PhantomData<fn() -> T>,
}
- #[allow(clippy::single_char_lifetime_names)]
- impl<'de, T> Visitor<'de> for RationalVisitor<T>
+ impl<T> Visitor<'_> for RationalVisitor<T>
where
T: Clone
+ From<u8>
@@ -52,19 +40,13 @@ where
+ Pow<usize, Output = T>,
{
type Value = Rational<T>;
- fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str("struct Rational")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
- #[allow(clippy::unnecessary_wraps)]
- /// Used as the closure that maps a `Ratio<R>`into an `Ok(Rational<R>)`.
- #[inline]
- const fn to_rational<R, E2>(val: Ratio<R>) -> Result<Rational<R>, E2> {
- Ok(Rational(val))
- }
try_from_str(v).map_or_else(
|_| {
Err(E::invalid_value(
@@ -72,7 +54,7 @@ where
&"a rational number in fraction or decimal notation",
))
},
- to_rational,
+ |val| Ok(Rational(val)),
)
}
}