rational_extensions

Extends num_rational::Ratio<T>.
git clone https://git.philomathiclife.com/repos/rational_extensions
Log | Files | Refs | README

commit 9a5eafa559d8ddde11d46d2a7f3edb11f609b4a4
parent 7b29b1a9f7903a30b7c2418aaacead6020fd498a
Author: Zack Newman <zack@philomathiclife.com>
Date:   Mon, 13 Jul 2026 13:02:47 -0600

bump msrv. new lints. update deps. move unit tests

Diffstat:
MCargo.toml | 117+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/lib.rs | 67+++++++------------------------------------------------------------
Dsrc/rational.rs | 79-------------------------------------------------------------------------------
Asrc/serde.rs | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/serde/tests.rs | 12++++++++++++
Asrc/tests.rs | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 224 insertions(+), 192 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -9,60 +9,64 @@ license = "MIT OR Apache-2.0" name = "rational_extensions" readme = "README.md" repository = "https://git.philomathiclife.com/repos/rational_extensions/" -rust-version = "1.89.0" -version = "0.4.7" +rust-version = "1.97.0" +version = "0.5.0" [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 } +deprecated-safe = { level = "deny", priority = -1 } +future-incompatible = { level = "deny", priority = -1 } +keyword-idents = { level = "deny", priority = -1 } +let-underscore = { level = "deny", priority = -1 } +nonstandard-style = { 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 } +unknown-or-malformed-diagnostic-attributes = { 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 } +ambiguous-negative-literals = { level = "deny", priority = -1 } +closure-returning-async-block = { level = "deny", priority = -1 } +dead-code-pub-in-binary = { level = "deny", priority = -1 } +deprecated-in-future = { level = "deny", priority = -1 } +#deprecated-llvm-intrinsic = { level = "deny", priority = -1 } +deref-into-dyn-supertrait = { level = "deny", priority = -1 } +ffi-unwind-calls = { level = "deny", priority = -1 } +#fuzzy-provenance-casts = { level = "deny", priority = -1 } +impl-trait-redundant-captures = { level = "deny", priority = -1 } +linker-info = { 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 } +redundant-imports = { level = "deny", priority = -1 } +redundant-lifetimes = { level = "deny", priority = -1 } +#resolving-to-items-shadowing-supertrait-items = { level = "deny", priority = -1 } +#shadowing-supertrait-items = { level = "deny", priority = -1 } +single-use-lifetimes = { 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-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 } [lints.clippy] -all = { level = "deny", priority = -1 } cargo = { level = "deny", priority = -1 } complexity = { level = "deny", priority = -1 } correctness = { level = "deny", priority = -1 } @@ -82,35 +86,42 @@ exhaustive_enums = "allow" exhaustive_structs = "allow" question_mark_used = "allow" ref_patterns = "allow" +self_named_module_files = "allow" single_call_fn = "allow" single_char_lifetime_names = "allow" +unseparated_literal_suffix = "allow" + +[lints.rustdoc] +all = { level = "deny", priority = -1 } [package.metadata.docs.rs] all-features = true -cargo-args = ["-Zbuild-std=std"] default-target = "x86_64-unknown-linux-gnu" targets = [ "aarch64-apple-darwin", + "aarch64-pc-windows-msvc", "aarch64-unknown-linux-gnu", "i686-pc-windows-msvc", "i686-unknown-linux-gnu", "x86_64-pc-windows-gnu", "x86_64-pc-windows-msvc", - "x86_64-unknown-openbsd" + "x86_64-unknown-freebsd", + "x86_64-unknown-linux-musl", + "x86_64-unknown-netbsd" ] [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 } +serde = { version = "1.0.228", default-features = false, optional = true } [dev-dependencies] -serde_json = { version = "1.0.143", default-features = false, features = ["alloc"] } +serde_json = { version = "1.0.150", default-features = false, features = ["alloc"] } ### FEATURES ################################################################# [features] # Provide (de)serialization support. -rational = ["dep:serde"] +serde = ["dep:serde"] diff --git a/src/lib.rs b/src/lib.rs @@ -5,6 +5,12 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #![no_std] extern crate alloc; +/// Enables deserialization of strings in decimal or fractional format into [`rational::Rational<T>`]. +#[cfg(feature = "serde")] +pub mod serde; +/// Unit tests. +#[cfg(test)] +mod tests; use crate::FromDecStrErr::{IntParseErr, TooFewFractionalDigits, TooManyFractionalDigits}; use alloc::{ string::{String, ToString as _}, @@ -171,6 +177,7 @@ where } /// The error returned when parsing a string in decimal or /// rational notation into a `num_rational::Ratio<T>`. +#[derive(Eq, PartialEq)] pub enum FromStrErr<T> { /// Contains the error when a string fails to parse into a `T`. IntParseErr(T), @@ -329,63 +336,3 @@ where // 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")] -pub mod rational; - -#[cfg(test)] -mod tests { - use super::*; - use alloc::format; - use core::{assert_eq, num::ParseIntError}; - use serde_json as _; - - #[test] - fn test_min_max() -> Result<(), String> { - let mut m: MinMax<u32>; - for i in 0..1000 { - for j in 0..1000 { - m = MinMax::new(i, i + j) - .ok_or_else(|| format!("Failed for {} and {}.", i, i + j))?; - assert_eq!(*m.min(), i); - assert_eq!(*m.max(), i + j); - } - } - Ok(()) - } - #[test] - #[should_panic] - fn test_min_max_err() { - _ = MinMax::new(f64::NAN, 0.0).unwrap(); - } - #[test] - fn test_dec_string() -> Result<(), String> { - assert_eq!("0", &to_dec_string(&Ratio::<u32>::new(0, 1), 0)); - assert_eq!("-1.33", &to_dec_string(&Ratio::<i32>::new(-4, 3), 2)); - assert_eq!("-0.33", &to_dec_string(&Ratio::<i32>::new(-1, 3), 2)); - assert_eq!("5.000", &to_dec_string(&Ratio::<u32>::new(5, 1), 3)); - assert_eq!("0.66667", &to_dec_string(&Ratio::<u32>::new(2, 3), 5)); - Ok(()) - } - #[test] - fn test_from_str() -> Result<(), FromStrErr<ParseIntError>> { - assert_eq!(try_from_str::<u32>("4")?, Ratio::new(4, 1)); - assert_eq!(try_from_str::<u32>("4/8")?, Ratio::new(1, 2)); - assert_eq!(try_from_str::<u32>("5 9/8")?, Ratio::new(49, 8)); - assert_eq!(try_from_str::<i32>("-2 7/6")?, Ratio::new(-19, 6)); - assert_eq!(try_from_str::<i32>("-5/6")?, Ratio::new(-5, 6)); - assert_eq!(try_from_str::<u32>("0.1249")?, Ratio::new(1249, 10000)); - assert_eq!(try_from_str::<i32>("-1.33")?, Ratio::new(-133, 100)); - assert_eq!(try_from_str::<i32>("-0.33")?, Ratio::new(-33, 100)); - assert_eq!(try_from_str::<u32>("0.0")?, Ratio::new(0, 1)); - try_from_str::<u32>("1/0").map_or_else( - |e| match e { - FromStrErr::DenominatorIsZero => (), - _ => assert_eq!(false, true), - }, - |_| assert_eq!(false, true), - ); - Ok(()) - } -} diff --git a/src/rational.rs b/src/rational.rs @@ -1,79 +0,0 @@ -use crate::{Ratio, try_from_str}; -use core::fmt::{self, Formatter}; -use core::marker::PhantomData; -use core::ops::Mul; -use core::str::FromStr; -use num_integer::Integer; -use num_traits::Pow; -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&lt;T&gt;. -#[derive(Clone, Copy, Debug)] -pub struct Rational<T>(pub Ratio<T>); -impl<'de, T> Deserialize<'de> for Rational<T> -where - T: Clone - + From<u8> - + FromStr - + Integer - + for<'a> Mul<&'a T, Output = T> - + Pow<usize, Output = T>, -{ - #[inline] - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - /// Visitor used to deserialize a JSON string into a Rational. - struct RationalVisitor<T> { - /// Does not own nor drop a `T`. - _x: PhantomData<fn() -> T>, - } - impl<T> Visitor<'_> for RationalVisitor<T> - where - T: Clone - + From<u8> - + FromStr - + Integer - + for<'a> Mul<&'a T, Output = T> - + Pow<usize, Output = T>, - { - type Value = Rational<T>; - 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, - { - try_from_str(v).map_or_else( - |_| { - Err(E::invalid_value( - Unexpected::Str(v), - &"a rational number in fraction or decimal notation", - )) - }, - |val| Ok(Rational(val)), - ) - } - } - deserializer.deserialize_str(RationalVisitor { _x: PhantomData }) - } -} -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_serde() -> Result<(), serde_json::Error> { - assert_eq!( - Ratio::new(2u8, 3u8), - serde_json::from_str::<Rational::<u8>>(r#""2/3""#)?.0 - ); - assert_eq!( - Ratio::new(67u8, 100u8), - serde_json::from_str::<Rational::<u8>>(r#""0.67""#)?.0 - ); - Ok(()) - } -} diff --git a/src/serde.rs b/src/serde.rs @@ -0,0 +1,68 @@ +/// Unit tests. +#[cfg(test)] +mod tests; +use crate::{Ratio, try_from_str}; +use core::{ + fmt::{self, Formatter}, + marker::PhantomData, + ops::Mul, + str::FromStr, +}; +use num_integer::Integer; +use num_traits::Pow; +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&lt;T&gt;. +#[derive(Clone, Copy, Debug)] +pub struct Rational<T>(pub Ratio<T>); +impl<'de, T> Deserialize<'de> for Rational<T> +where + T: Clone + + From<u8> + + FromStr + + Integer + + for<'a> Mul<&'a T, Output = T> + + Pow<usize, Output = T>, +{ + #[inline] + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + /// Visitor used to deserialize a JSON string into a Rational. + struct RationalVisitor<T> { + /// Does not own nor drop a `T`. + _x: PhantomData<fn() -> T>, + } + impl<T> Visitor<'_> for RationalVisitor<T> + where + T: Clone + + From<u8> + + FromStr + + Integer + + for<'a> Mul<&'a T, Output = T> + + Pow<usize, Output = T>, + { + type Value = Rational<T>; + 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, + { + try_from_str(v).map_or_else( + |_| { + Err(E::invalid_value( + Unexpected::Str(v), + &"a rational number in fraction or decimal notation", + )) + }, + |val| Ok(Rational(val)), + ) + } + } + deserializer.deserialize_str(RationalVisitor { _x: PhantomData }) + } +} diff --git a/src/serde/tests.rs b/src/serde/tests.rs @@ -0,0 +1,12 @@ +use super::{Ratio, Rational}; +#[test] +fn deserialize() { + assert!( + serde_json::from_str::<Rational::<u8>>(r#""2/3""#) + .is_ok_and(|r| r.0 == Ratio::new(2u8, 3u8)) + ); + assert!( + serde_json::from_str::<Rational::<u8>>(r#""0.67""#) + .is_ok_and(|r| r.0 == Ratio::new(67u8, 100u8)) + ); +} diff --git a/src/tests.rs b/src/tests.rs @@ -0,0 +1,73 @@ +use super::{FromStrErr, MinMax, Ratio}; +use serde_json as _; +#[test] +fn min_max() { + for i in 0..1000 { + for j in 0..1000 { + assert!( + MinMax::<u32>::new(i, i + j).is_some_and(|m| *m.min() == i && *m.max() == i + j) + ); + } + } +} +#[test] +fn min_max_err() { + assert!(MinMax::new(f64::NAN, 0.0f64).is_none()); +} +#[test] +fn dec_string() { + assert_eq!("0", &super::to_dec_string(&Ratio::<u32>::new(0, 1), 0)); + assert_eq!( + "-1.33", + &super::to_dec_string(&Ratio::<i32>::new(-4i32, 3i32), 2) + ); + assert_eq!( + "-0.33", + &super::to_dec_string(&Ratio::<i32>::new(-1i32, 3i32), 2) + ); + assert_eq!( + "5.000", + &super::to_dec_string(&Ratio::<u32>::new(5u32, 1u32), 3) + ); + assert_eq!( + "0.66667", + &super::to_dec_string(&Ratio::<u32>::new(2, 3), 5) + ); +} +#[test] +fn from_str() { + assert_eq!(super::try_from_str::<u32>("4"), Ok(Ratio::new(4u32, 1u32))); + assert_eq!( + super::try_from_str::<u32>("4/8"), + Ok(Ratio::new(1u32, 2u32)) + ); + assert_eq!( + super::try_from_str::<u32>("5 9/8"), + Ok(Ratio::new(49u32, 8u32)) + ); + assert_eq!( + super::try_from_str::<i32>("-2 7/6"), + Ok(Ratio::new(-19i32, 6i32)) + ); + assert_eq!( + super::try_from_str::<i32>("-5/6"), + Ok(Ratio::new(-5i32, 6i32)) + ); + assert_eq!( + super::try_from_str::<u32>("0.1249"), + Ok(Ratio::new(1249u32, 10000u32)) + ); + assert_eq!( + super::try_from_str::<i32>("-1.33"), + Ok(Ratio::new(-133i32, 100i32)) + ); + assert_eq!( + super::try_from_str::<i32>("-0.33"), + Ok(Ratio::new(-33i32, 100i32)) + ); + assert_eq!(super::try_from_str::<u32>("0.0"), Ok(Ratio::new(0, 1))); + assert_eq!( + super::try_from_str::<u32>("1/0"), + Err(FromStrErr::DenominatorIsZero) + ); +}