num-cmp
A Rust trait for safely comparing numbers of different integer and floating-point types without manual casting.
Repository Health
Technical Analysis
num-cmp provides the NumCmp trait, a single abstraction for comparing values across Rust’s numeric types — i64 against f64, usize against i8, u32 against f32, and every other integer/float pairing the standard library ships with. Ordinary Rust code has to cast one operand to match the other before comparing, which silently introduces bugs: casting a large u64 to f64 can lose precision, casting a negative i32 to usize wraps around, and as casts between float and int truncate rather than round.
Instead of exposing a single blunt cast-and-compare helper, the crate implements five distinct comparison strategies chosen per type pair — direct comparison for identical types, size-based delegation for usize/isize, safe widening casts when no precision is lost, sign-aware comparison between differently-signed integers, and a bounds-checked truncate-and-compare approach for integer-vs-float pairs where naive casting would be lossy. Each strategy is documented inline with the mathematical reasoning behind it, including a short proof for the float/int truncation approach.
The result is a num_cmp, num_eq, num_lt, num_gt, num_le, and num_ge method set that behaves correctly at every boundary case: NaN handling, values outside the representable range of the other type, and exact-equality checks between floats and large integers.
What You Get
- The
NumCmptrait implemented for all 144 pairings of Rust’s 12 built-in numeric types (12 more with the optionali128feature) - Six comparison methods per type pair:
num_cmp,num_eq,num_ne,num_lt,num_gt,num_le,num_ge - Correct handling of edge cases: NaN propagation to
None/false results, sign-aware unsigned-vs-signed comparisons, and out-of-range float-vs-integer bounds checks - An optional
i128Cargo feature (nightly-only at the time of writing) extending coverage to 128-bit integer types - Zero runtime dependencies — pure
std-only Rust with#[inline]hints on every comparison method
Common Use Cases
- Comparing a
u64byte count or file size against anf64threshold without manually bounds-checking the cast - Validating that a signed
i32offset or index is non-negative and within range of ausizebuffer length - Cross-type comparisons in generic numeric code that accepts mixed integer/float inputs from configuration or user input
- Sorting or ordering collections of heterogeneous numeric wrapper types where a single canonical type isn’t available
- Replacing ad-hoc
ascasts in comparison-heavy code (parsers, validators, numeric bounds checks) with a correctness-checked alternative
Under The Hood
Architecture
The entire crate lives in a single src/lib.rs: one public trait, NumCmp<Other>, and five macro_rules! blocks (impl_for_equal_types, impl_for_size_types, impl_for_nonequal_types_with_casting, impl_for_nonequal_types_with_different_signedness, impl_for_int_and_float_types_with_bounds_check) that each generate a distinct comparison strategy. The bottom of the file invokes these macros against explicit lists of type pairs, expanding into concrete trait implementations for every combination of Rust’s numeric types the crate supports. There’s no runtime dispatch: the correct comparison strategy is selected entirely at compile time based on which macro generated the impl for a given (Self, Other) pair, so calling num_cmp compiles down to inlined, monomorphized code with no indirection.
Tech Stack
Pure std-only Rust with zero external dependencies — the Cargo.toml declares no [dependencies] at all. The only configurable surface is the i128 Cargo feature, which extends every macro-generated implementation to 128-bit integer types (gated behind a nightly-only feature(i128_type) attribute at the time this crate was last updated). The crate targets docs.rs for documentation hosting and has no build tooling beyond cargo build/cargo test.
Code Quality
src/tests.rs (486 lines) exercises every comparison method against a hand-built N enum wrapping each numeric type, asserting the full six-method result set (num_cmp, num_eq, num_ne, num_lt, num_gt, num_le, num_ge) against an expected Ordering for representative and boundary values, including NaN. A #[cfg(test)]-only num_cmp_strategy method on the trait lets tests assert which of the five macro strategies was actually selected for a given type pair, catching macro-dispatch regressions directly. The crate uses #![deny(missing_docs)] to force documentation on every public item, and #[inline] is applied to every generated method. No CI configuration (GitHub Actions, Travis) is present in the repository, and there’s been no commit activity since 2020.
API Design
The public surface is deliberately minimal: one trait, six methods, implemented transparently across type pairs so callers write a.num_lt(b) regardless of whether a and b share a type. No wrapper types, no builder patterns, no configuration — you add the crate, use num_cmp::NumCmp, and call the same six methods you’d use with PartialOrd/PartialEq. The tradeoff is that error/edge-case handling (NaN, out-of-range values) is baked into the trait’s semantics rather than surfaced through a Result type, which keeps call sites simple but means edge-case behavior must be learned from the docs rather than the type signature.