# uninum
A robust, ergonomic, and unified numeric type for Rust with automatic overflow handling, type promotion, and cross-type consistency.
`uninum` provides a single `Number` enum that safely encapsulates all standard numeric types, allowing you to perform arithmetic and comparisons without boilerplate conversions, overflow panics, or floating-point precision surprises.
Want a feature-by-feature walkthrough? Check out the [Quick Reference](QUICK_REFERENCE.md) for an in-depth tour of the API surface, conversion paths, and integration tips.
## Why use `uninum`?
Working with different numeric types in Rust can be verbose and error-prone. You have to handle conversions, worry about integer overflow, and be mindful of floating-point inaccuracies. `uninum` solves these problems by providing:
* **Seamless Interoperability**: Perform arithmetic between integers, floats, and high-precision decimals without explicit casting.
* **Absolute Safety**: Operations never panic. Integer overflow is automatically handled by promoting to a larger type. Division by zero follows IEEE 754 semantics, returning `Infinity` or `NaN`.
* **Precision by Default**: When the `decimal` feature is enabled, floating-point literals and inexact divisions are automatically converted to a high-precision decimal type, preventing common floating-point errors in financial and scientific calculations.
* **Ergonomic Design**: A clean, intuitive API that feels like a natural extension of Rust's built-in numeric types.
## Precision Philosophy
- **Exact first**: keep lossless representations whenever they exist (integers stay numeric, decimal literals stay decimal).
- **Fast when exact**: among exact choices, stick to the quickest variant—integers beat decimals until you truly need decimal scale.
- **Graceful fallback**: when exactness isn’t possible, promote to the best available floating-point type (`Decimal`, then `F64`) so work keeps flowing.
| `Decimal` | Exact (≈28 digits) | Finance, billing, human-facing decimal math |
| `F64` | ~15–17 digits | Scientific/engineering work, extremely large/small values, NaN/∞ |
| `I64` / `U64` | Exact within range | Counting, IDs, fast integer-heavy logic |
## Key Features
* **Intelligent Construction with `num!`**: A powerful macro that creates the optimal `Number` variant from literals (e.g., `num!(3.14)` becomes a high-precision `Decimal`).
* **Seamless Primitive Operations**: `&num + 5`, `2.5 * &num`, and `num == 10` work just as you'd expect.
* **Automatic & Safe Type Promotion**: `u64::MAX + 1` gracefully promotes to a `Decimal` or `F64` instead of panicking.
* **High-Precision Decimal Arithmetic**: Enable the `decimal` feature for arbitrary-precision decimal calculations, perfect for financial applications.
* **Total Ordering & Hashing for Floats**: `Float64` wrapper allows floating-point numbers (including `NaN`) to be used in `BTreeMap`s and `HashMap`s with consistent, predictable behavior.
* **Stable Serialization**: The `serde` feature emits a tagged JSON representation so variants, decimal scale, and non-finite floats (`NaN`, `Infinity`) round-trip without loss.
## Installation
Add `uninum` to your `Cargo.toml`:
```toml
[dependencies]
uninum = "0.1.0"
```
`uninum` follows a **minimum supported Rust version (MSRV) of 1.88.0**. The
crate is tested against that toolchain in CI; newer stable compilers work as
well.
For high-precision decimal, `serde` support, or bitwise operations, enable the corresponding features:
```toml
[dependencies]
uninum = { version = "0.1.0", features = ["decimal", "serde", "bitwise"] }
```
## Governance & Contribution Policy
`uninum` is developed and maintained exclusively by **Synext Solution Sdn. Bhd.**
Bug reports, feature ideas, and general feedback are welcome through the issue
tracker. However, Synext Solution retains sole discretion over the roadmap and
the official code base: external pull requests are not accepted, and any changes
must be coordinated through the maintainers. You are, of course, free to fork
the project under the dual MIT/Apache-2.0 license if you need customisations.
## Quick Start
```rust
use uninum::{num, Number};
fn main() {
// The `num!` macro creates the best representation for a literal.
let integer = num!(100); // Stored as an integer
let float = num!(3.14); // Stored as a Decimal (if `decimal` feature is on)
let large_int = num!(u64::MAX);
// --- Ergonomic Operations ---
// All operations work with references, avoiding unnecessary moves.
let result1 = &integer + 50; // Number + i32
let result2 = 2.5 * &integer; // f64 * Number
let result3 = &integer + &float; // Number + Number
println!("100 + 50 = {}", result1);
println!("2.5 * 100 = {}", result2);
println!("100 + 3.14 = {}", result3);
// --- Safety and Precision ---
// No panics! Overflow promotes to a higher-precision type.
let overflow_result = large_int + 1;
println!("u64::MAX + 1 = {}", overflow_result); // No panic!
// Inexact division is handled with precision.
let division = num!(10) / num!(3);
println!("10 / 3 = {}", division); // e.g., "3.33333..." as a Decimal
// --- Comparisons ---
assert!(&integer == 100);
assert!(&float < 4);
assert!(num!(0.1) + num!(0.2) == num!(0.3)); // True if `decimal` feature is enabled!
}
```
## Core Concepts
### 1. Ergonomic Operations: References vs. Owned Values
All arithmetic and comparison operators are implemented for both owned `Number`s and references `&Number`. To avoid consuming your variables, always use references (`&`) for operations.
```rust
let num = num!(10);
// Using a reference (&num) - `num` can be reused.
let result1 = &num + 5;
let result2 = &num * 2; // `num` is still available.
println!("Original num is still: {}", num);
// Using an owned value (num) - `num` is moved and consumed.
let result3 = num + 1;
// println!("{}", num); // ❌ This would be a compile-time error.
```
### 2. The `num!` Macro
The `num!` macro is the recommended way to create `Number` instances from literals. It intelligently parses the literal to choose the best internal representation:
* **Integers**: `num!(42)`, `num!(-100)` become `Number::from(42i64)`, etc.
* **Floats**: `num!(3.14)`, `num!(1e-5)` are parsed as strings to preserve full precision, creating a `Decimal` if the `decimal` feature is enabled, or an `F64` otherwise. This avoids the inherent imprecision of `f64` literals.
* **Variables/Expressions**: `num!(my_var)` is a convenient shorthand for `Number::from(my_var)`.
### 3. Equality and Ordering Guarantees
`uninum` deliberately deviates from the raw IEEE semantics to provide consistent
behaviour across the entire type family:
* **`NaN` Equality**: all `NaN` values compare equal to each other. This makes
equality checks across mixed numeric types deterministic (`Number::F64(NaN) ==
Number::Decimal(NaN)` evaluates to `true`).
* **Signed Zero Normalisation**: `+0` and `-0` are treated as the same value for
both equality and ordering comparisons.
* **Total Ordering**: the type implements a total ordering even for special
values—`NaN` is equal to itself and considered greater than any finite number,
while infinities follow the expected ordering.
### 4. Automatic Type Promotion and Safety
`uninum` is designed to be safe and predictable.
* **Integer Overflow**: When an integer operation overflows (e.g., `u64::MAX + 1`), the operation is re-run after promoting the numbers to a higher-precision type (`Decimal` or `F64`).
* **Division by Zero**: Follows IEEE 754 rules:
* `finite / 0` -> `+Infinity` or `-Infinity`
* `0 / 0` -> `NaN`
* `Infinity / Infinity` -> `NaN`
* **NaN & ±0 Normalisation**: For deterministic behaviour across Number variants,
all `NaN` values compare equal to each other, and `+0` / `-0` are treated as the
same value for equality and ordering. Total ordering remains well-defined even
in the presence of infinities or NaNs.
### 5. Precision with the `decimal` feature
Standard `f64` types are prone to precision errors (e.g., `0.1 + 0.2 != 0.3`). The `decimal` feature (enabled by default) solves this by using the `rust_decimal` crate for arbitrary-precision decimal arithmetic.
```rust
// With the `decimal` feature enabled:
// num!(0.1) creates a precise Decimal, not an approximate f64.
assert!(num!(0.1) + num!(0.2) == num!(0.3)); // This is true!
// Without the `decimal` feature, this would fall back to f64 and fail.
let f64_sum = Number::from(0.1_f64) + Number::from(0.2_f64);
assert!(f64_sum != Number::from(0.3_f64)); // Standard f64 imprecision.
```
## Feature Flags & Dependencies
`uninum` uses feature flags to keep the core library lightweight. With
`default-features = false` the library has **no runtime dependencies**; every
extra crate is opt-in so you can control the footprint precisely.
| `decimal` | Yes | `rust_decimal` | Enables high-precision decimal arithmetic using the `rust_decimal` crate. Highly recommended for financial or scientific applications. |
| `serde` | No | `serde`, `ryu` | Enables serialization and deserialization support via the `serde` crate. |
| `bitwise` | No | (none) | Enables bitwise operations (`&`, `^`, `!`, `<<`, `>>`) for integer variants of `Number`. |
To use `uninum` without default features:
```toml
[dependencies]
uninum = { version = "0.1.0", default-features = false }
```
> 💡 **Dependency policy:** new dependencies must be justified and introduced as
> optional features whenever possible. The default configuration should remain
> as lean as possible so downstream users can rely on a zero-dependency core.
## License
This project is 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)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work shall be dual licensed as above, without any
additional terms or conditions.
## Contributing
We welcome bug reports and feature discussions via the issue tracker, but code
changes are handled solely by the Synext maintainer team. See the
[Contributing Guidelines](CONTRIBUTING.md) for details on how to share feedback
or follow the internal release workflow.