Skip to main content

Crate positive

Crate positive 

Source
Expand description

§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-zero feature flag to reject zero values (strictly > 0)
  • Decimal Precision: Built on rust_decimal for 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 Result instead of panicking
  • Optional utoipa Integration: OpenAPI schema generation support via feature flag

§Installation

Add this to your Cargo.toml:

[dependencies]
positive = "0.6"

To require strictly positive values (excluding zero):

[dependencies]
positive = { version = "0.6", features = ["non-zero"] }

To enable OpenAPI schema support:

[dependencies]
positive = { version = "0.6", features = ["utoipa"] }

§Quick Start

The recommended pattern is fallible construction and checked arithmetic, propagating PositiveError with ?. Nothing here can panic:

use positive::prelude::*;

fn order_total() -> Result<Positive, PositiveError> {
    let price = Positive::new(100.50)?;
    let quantity = Positive::new(10.0)?;
    let discount = Positive::new(5.0)?;

    let subtotal = price.checked_mul(&quantity)?;
    let after_discount = subtotal.checked_sub(&discount)?;

    // Constants are ready-made and cannot fail
    let tax_rate = FIVE.checked_div(&HUNDRED)?;   // 5%
    let tax = after_discount.checked_mul(&tax_rate)?;

    after_discount.checked_add(&tax)
}

assert!(order_total().is_ok());

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 positive::prelude::*;

let price = Positive::new(100.50)?;
let quantity = Positive::new(10.0)?;
let total = price * quantity;          // panics on overflow
let total = price.checked_mul(&quantity)?;  // 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 positive::{Positive, pos, pos_or_panic, spos};
use rust_decimal::Decimal;

// From f64
let p = Positive::new(5.0).unwrap();

// From Decimal
let p = Positive::new_decimal(Decimal::ONE).unwrap();

// Using macros
let p = pos!(5.0);           // Returns Result<Positive, PositiveError>
let p = pos_or_panic!(5.0);  // Panics on invalid input
let p = spos!(5.0);          // Returns Option<Positive>

§Constants

The library provides many predefined constants accessible via Positive::CONSTANT or directly from the constants module:

use positive::Positive;
use positive::constants::*;

// Integer constants (1-10)
let one = Positive::ONE;         // 1
let two = Positive::TWO;         // 2
let ten = Positive::TEN;         // 10

// Multiples of 5 (15-95)
let fifteen = FIFTEEN;           // 15
let fifty = FIFTY;               // 50

// Multiples of 100 (100-900)
let hundred = Positive::HUNDRED; // 100
let five_hundred = FIVE_HUNDRED; // 500

// Multiples of 1000 (1000-10000)
let thousand = Positive::THOUSAND; // 1000
let ten_thousand = TEN_THOUSAND;   // 10000

// Mathematical constants
let pi = Positive::PI;           // π (3.14159...)
let e = Positive::E;             // e (2.71828...)

// Special values
let epsilon = EPSILON;           // Small tolerance for comparisons
let max = Positive::MAX;         // Largest representable value (Decimal::MAX)

§Conversions

use positive::pos_or_panic;

let p = pos_or_panic!(5.5);

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: Result<i64, _> = i64::try_from(p);
let u: Result<u64, _> = u64::try_from(p);
let n: Result<usize, _> = usize::try_from(p);
let maybe: Option<u64> = p.to_u64_checked();

§Arithmetic Operations

use positive::pos_or_panic;

let a = pos_or_panic!(10.0);
let b = pos_or_panic!(3.0);

// 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(&b);    // Returns Result
let safe_quot = a.checked_div(&b);    // Returns Result (handles div by zero)

§Mathematical Functions

use positive::pos_or_panic;

let p = pos_or_panic!(16.0);

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(pos_or_panic!(2.0));    // Power with Positive exponent
let powi = p.powi(2);          // 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(2);    // Round to 2 decimal places

§Utility Methods

use positive::pos_or_panic;

use rust_decimal_macros::dec;
let p = pos_or_panic!(5.0);

let is_zero = p.is_zero();                      // Check if zero
let is_mult = p.is_multiple_of_dec(dec!(2));    // Check if multiple of value
let clamped = p.clamp(pos_or_panic!(1.0), pos_or_panic!(10.0));   // Clamp between bounds
let min_val = p.min(pos_or_panic!(3.0));                 // Minimum of two values
let max_val = p.max(pos_or_panic!(3.0));                 // Maximum of two values
let formatted = p.format_fixed_places(2);       // Format with fixed decimals

§Error Handling

The library provides PositiveError for comprehensive error handling:

use positive::{Positive, PositiveError};

fn example() -> Result<Positive, PositiveError> {
    let value = Positive::new(-5.0)?;  // Returns Err(OutOfBounds)
    Ok(value)
}

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 invariant
  • ConversionError - A valid value not representable in the destination type
  • OutOfBounds - A well-formed decimal outside the permitted range
  • InvalidPrecision - A decimal precision outside the range Decimal supports

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 positive::{Positive, PositiveError};
use std::str::FromStr;

let err = Positive::from_str("not a number").unwrap_err();
assert!(matches!(err, PositiveError::InvalidValue { .. }));

§Serialization

Positive serialises as the exact decimal string, so every value the type can hold round-trips without losing a digit:

use positive::{Positive, pos_or_panic};
use rust_decimal::Decimal;
use std::str::FromStr;

let p = pos_or_panic!(42.5);
let json = serde_json::to_string(&p).unwrap();      // "\"42.5\""
let parsed: Positive = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, p);

// Full 28-digit precision survives the round trip
let exact = Decimal::from_str("0.1234567890123456789012345678").unwrap();
let value = Positive::new_decimal(exact).unwrap();
let json = serde_json::to_string(&value).unwrap();
let back: Positive = serde_json::from_str(&json).unwrap();
assert_eq!(back.to_dec(), exact);

§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, and Positive::MAX.
  • Validation on the way in: the positivity invariant is enforced on deserialisation, so the non-zero feature 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.

Re-exports§

pub use error::PositiveError;
pub use error::PositiveResult;

Modules§

constants
Predefined constant values for the Positive type.
error
Error types for the Positive decimal type.
macros
Macros for creating Positive values.
prelude
Prelude module for convenient imports.

Macros§

assert_pos_relative_eq
Asserts that two Positive values are relatively equal within a given epsilon.
pos
Macro for creating a Positive value from the given expression.
pos_or_panic
Macro for creating a new Positive value that panics on invalid input.
spos
Macro for creating an optional Positive value from the given expression.

Structs§

Decimal
Re-export rust_decimal for convenience. Decimal represents a 128 bit representation of a fixed-precision decimal number. The finite set of values of type Decimal are of the form m / 10e, where m is an integer such that -296 < m < 296, and e is an integer between 0 and 28 inclusive.
Positive
A wrapper type that represents a guaranteed positive decimal value.

Constants§

DIV_ROUNDING_STRATEGY
Default rounding strategy used by every Div operator on Positive.

Functions§

is_positive
Determines if the given type parameter T is the Positive type.
is_valid_positive_value
Returns whether the given decimal value satisfies the positivity constraint.