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:
| M | Cargo.toml | | | 117 | +++++++++++++++++++++++++++++++++++++++++++------------------------------------ |
| M | src/lib.rs | | | 67 | +++++++------------------------------------------------------------ |
| D | src/rational.rs | | | 79 | ------------------------------------------------------------------------------- |
| A | src/serde.rs | | | 68 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/serde/tests.rs | | | 12 | ++++++++++++ |
| A | src/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<T>.
-#[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<T>.
+#[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)
+ );
+}