Positive
A type-safe wrapper for guaranteed positive decimal values in Rust.
Overview
Positive is a Rust library that provides a type-safe wrapper around Decimal values,
ensuring that the contained value is always positive. By default, values are non-negative
(>= 0). With the non-zero feature enabled, values must be strictly positive (> 0).
This is particularly useful in financial applications where negative values would be
invalid or meaningless, such as prices, quantities, volatilities, and other positive metrics.
Features
- Type Safety: Compile-time and runtime guarantees that values are positive
- Non-Zero Mode: Optional
non-zerofeature flag to reject zero values (strictly > 0) - Decimal Precision: Built on
rust_decimalfor accurate financial calculations - Rich API: Comprehensive arithmetic operations, conversions, and mathematical utilities
- Predefined Constants: Common numeric values (0-10, multiples of 5/100/1000, PI, E, etc.)
- Convenient Macros:
pos!,pos_or_panic!,spos!for easy value creation - Prelude Module: Simple imports with
use positive::prelude::*; - Serde Support: Lossless serialisation as exact decimal strings, for JSON and binary formats alike
- Approx Support: Approximate equality comparisons for floating-point tolerance
- Checked Operations: Safe arithmetic operations that return
Resultinstead of panicking - Optional utoipa Integration: OpenAPI schema generation support via feature flag
Installation
Add this to your Cargo.toml:
[]
= "0.6"
To require strictly positive values (excluding zero):
[]
= { = "0.6", = ["non-zero"] }
To enable OpenAPI schema support:
[]
= { = "0.6", = ["utoipa"] }
Quick Start
The recommended pattern is fallible construction and checked arithmetic,
propagating [PositiveError] with ?. Nothing here can panic:
use *;
assert!;
The operators (+, -, *, /) are available too and read more
naturally, at the cost of panicking on overflow or on a result that would
break the invariant. Every one of them has a checked_ counterpart, listed
in its # Panics section, so the panicking form is always an opt-in:
use *;
let price = new?;
let quantity = new?;
let total = price * quantity; // panics on overflow
let total = price.checked_mul?; // returns Err instead
A note on the examples below
The remaining examples use [pos_or_panic!] for brevity, so each one fits
in a line or two. That macro panics on invalid input and is intended for
tests, examples and constant literals — not for production paths that
handle external input. There, use [Positive::new], [pos!] or [spos!]
and handle the failure.
API Overview
Creation
use ;
use Decimal;
// From f64
let p = new.unwrap;
// From Decimal
let p = new_decimal.unwrap;
// Using macros
let p = pos!; // Returns Result<Positive, PositiveError>
let p = pos_or_panic!; // Panics on invalid input
let p = spos!; // Returns Option<Positive>
Constants
The library provides many predefined constants accessible via Positive::CONSTANT
or directly from the constants module:
use Positive;
use *;
// Integer constants (1-10)
let one = ONE; // 1
let two = TWO; // 2
let ten = TEN; // 10
// Multiples of 5 (15-95)
let fifteen = FIFTEEN; // 15
let fifty = FIFTY; // 50
// Multiples of 100 (100-900)
let hundred = HUNDRED; // 100
let five_hundred = FIVE_HUNDRED; // 500
// Multiples of 1000 (1000-10000)
let thousand = THOUSAND; // 1000
let ten_thousand = TEN_THOUSAND; // 10000
// Mathematical constants
let pi = PI; // π (3.14159...)
let e = E; // e (2.71828...)
// Special values
let epsilon = EPSILON; // Small tolerance for comparisons
let max = MAX; // Largest representable value (Decimal::MAX)
Conversions
use pos_or_panic;
let p = pos_or_panic!;
let f: f64 = p.to_f64; // Infallible, lossy beyond ~15 digits
let d = p.to_dec; // To Decimal, exact
// Integer conversions are fallible and truncate toward zero
let i: = i64try_from;
let u: = u64try_from;
let n: = usizetry_from;
let maybe: = p.to_u64_checked;
Arithmetic Operations
use pos_or_panic;
let a = pos_or_panic!;
let b = pos_or_panic!;
// Standard operations
let sum = a + b; // Addition
let diff = a - b; // Subtraction (panics if result < 0)
let prod = a * b; // Multiplication
let quot = a / b; // Division
// Safe operations
let safe_diff = a.checked_sub; // Returns Result
let safe_quot = a.checked_div; // Returns Result (handles div by zero)
Mathematical Functions
use pos_or_panic;
let p = pos_or_panic!;
let sqrt = p.sqrt; // Square root
let ln = p.ln; // Natural logarithm
let log10 = p.log10; // Base-10 logarithm
let exp = p.exp; // Exponential (e^x)
let pow = p.pow; // Power with Positive exponent
let powi = p.powi; // Integer power
let floor = p.floor; // Floor
let ceil = p.ceiling; // Ceiling
let round = p.round; // Round to nearest integer
let round2 = p.round_to; // Round to 2 decimal places
Utility Methods
use pos_or_panic;
use dec;
let p = pos_or_panic!;
let is_zero = p.is_zero; // Check if zero
let is_mult = p.is_multiple_of_dec; // Check if multiple of value
let clamped = p.clamp; // Clamp between bounds
let min_val = p.min; // Minimum of two values
let max_val = p.max; // Maximum of two values
let formatted = p.format_fixed_places; // Format with fixed decimals
Error Handling
The library provides PositiveError for comprehensive error handling:
use ;
PositiveError has exactly five variants, and that set is stable across
minor versions. There is no catch-all, so callers can match exhaustively
without a wildcard arm:
InvalidValue- Input that is not a decimal at all (NaN,±inf, unparsable text)ArithmeticError- Overflow, division by zero, or a result breaking the invariantConversionError- A valid value not representable in the destination typeOutOfBounds- A well-formed decimal outside the permitted rangeInvalidPrecision- A decimal precision outside the rangeDecimalsupports
OutOfBounds carries exact Decimal values for the offending input and
both bounds, so no precision is lost in the diagnostic. Under the
non-zero feature the reported minimum is 1e-28, the smallest strictly
positive Decimal.
Parsing follows the same contract — FromStr fails with a PositiveError
that preserves the offending input:
use ;
use FromStr;
let err = from_str.unwrap_err;
assert!;
Serialization
Positive serialises as the exact decimal string, so every value the
type can hold round-trips without losing a digit:
use ;
use Decimal;
use FromStr;
let p = pos_or_panic!;
let json = to_string.unwrap; // "\"42.5\""
let parsed: Positive = from_str.unwrap;
assert_eq!;
// Full 28-digit precision survives the round trip
let exact = from_str.unwrap;
let value = new_decimal.unwrap;
let json = to_string.unwrap;
let back: Positive = from_str.unwrap;
assert_eq!;
Precision guarantees
- Representation: a JSON string holding the exact decimal, e.g.
"42.5","79228162514264337593543950335". - Lossless for every representable value, including 28-digit fractions,
integers above
i64::MAX, andPositive::MAX. - Validation on the way in: the positivity invariant is enforced on
deserialisation, so the
non-zerofeature rejects zero there too. - Non-self-describing formats (bincode, postcard) are supported: the
implementation asks for a string rather than relying on
deserialize_any, which such formats cannot provide. - Backward compatibility: plain JSON numbers written by 0.5.x still deserialise. They are lossy by construction — that precision was gone before the bytes were written — so re-serialising upgrades them to the exact form.
A JSON number cannot carry this precision: nearly every consumer parses
one into an f64, which holds about 15 significant digits. That is why the
wire format is a string, and why rust_decimal itself uses one.
Use Cases
- Financial Applications: Prices, quantities, fees, rates
- Scientific Computing: Physical quantities that cannot be negative
- Game Development: Health points, distances, timers
- Data Validation: Ensuring input values meet positivity constraints
Safety
This crate contains no unsafe code, enforced at compile time by
#![forbid(unsafe_code)]. Earlier versions exposed a public
Positive::new_unchecked, an unsafe fn that performed no unsafe
operation: it moved a logical precondition onto the caller without making
a violation Rust undefined behaviour. It has been removed. Every public
path that yields a Positive validates the invariant.
License
This project is licensed under the MIT License.
Contribution and Contact
We welcome contributions to this project! If you would like to contribute, please follow these steps:
- Fork the repository.
- Create a new branch for your feature or bug fix.
- Make your changes and ensure that the project still builds and all tests pass.
- Commit your changes and push your branch to your forked repository.
- Submit a pull request to the main repository.
If you have any questions, issues, or would like to provide feedback, please feel free to contact the project maintainer:
Contact Information
- Author: Joaquín Béjar García
- Email: jb@taunais.com
- Telegram: @joaquin_bejar
- Repository: https://github.com/joaquinbejar/positive
- Documentation: https://docs.rs/positive
We appreciate your interest and look forward to your contributions!
✍️ License
Licensed under MIT license