positive 0.7.0

A type-safe wrapper for guaranteed positive decimal values
Documentation
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
[![Crates.io](https://img.shields.io/crates/v/positive.svg)](https://crates.io/crates/positive)
[![Downloads](https://img.shields.io/crates/d/positive.svg)](https://crates.io/crates/positive)
[![Stars](https://img.shields.io/github/stars/joaquinbejar/positive.svg)](https://github.com/joaquinbejar/positive/stargazers)
[![Issues](https://img.shields.io/github/issues/joaquinbejar/positive.svg)](https://github.com/joaquinbejar/positive/issues)
[![PRs](https://img.shields.io/github/issues-pr/joaquinbejar/positive.svg)](https://github.com/joaquinbejar/positive/pulls)
[![Quality Gate](https://img.shields.io/github/actions/workflow/status/joaquinbejar/positive/quality.yml?branch=main&label=quality)](https://github.com/joaquinbejar/positive/actions/workflows/quality.yml)
[![Security Audit](https://img.shields.io/github/actions/workflow/status/joaquinbejar/positive/audit.yml?branch=main&label=audit)](https://github.com/joaquinbejar/positive/actions/workflows/audit.yml)
[![Coverage](https://img.shields.io/codecov/c/github/joaquinbejar/positive)](https://codecov.io/gh/joaquinbejar/positive)
[![Dependencies](https://img.shields.io/librariesio/github/joaquinbejar/positive)](https://libraries.io/github/joaquinbejar/positive)
[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://docs.rs/positive)
[![Wiki](https://img.shields.io/badge/wiki-latest-blue.svg)](https://deepwiki.com/joaquinbejar/positive)


## 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`]https://crates.io/crates/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`:

```toml
[dependencies]
positive = "0.7"
```

To require strictly positive values (excluding zero):

```toml
[dependencies]
positive = { version = "0.7", features = ["non-zero"] }
```

To enable OpenAPI schema support:

```toml
[dependencies]
positive = { version = "0.7", features = ["utoipa"] }
```

### Quick Start

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

```rust
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:

```rust
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

```rust
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:

```rust
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

```rust
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

```rust
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

```rust
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

```rust
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:

```rust
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:

```rust
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:

```rust
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.




## Contribution and Contact

We welcome contributions to this project! If you would like to contribute, please follow these steps:

1. Fork the repository.
2. Create a new branch for your feature or bug fix.
3. Make your changes and ensure that the project still builds and all tests pass.
4. Commit your changes and push your branch to your forked repository.
5. 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]https://t.me/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

<!-- related-projects:start -->
## Related projects

Repositories by the same author that this project depends on, and repositories that depend on it.

### Used by

| Repository | Description |
|------------|-------------|
| [ExpirationDate]https://github.com/joaquinbejar/ExpirationDate · [crates.io]https://crates.io/crates/expiration_date | Financial instrument expiration dates: parsing, arithmetic and time-to-expiry helpers. |
| [IronCondor]https://github.com/joaquinbejar/IronCondor | Backtesting engine for options strategies with order-book-level fill simulation. *(dev-dependency)* |
| [option_type]https://github.com/joaquinbejar/option_type · [crates.io]https://crates.io/crates/option_type | Enum-based classification of vanilla and exotic option contracts. |
| [OptionChain-Simulator]https://github.com/joaquinbejar/OptionChain-Simulator | RESTful simulator for option chains that evolve over time with each request. |
| [OptionStratLib]https://github.com/joaquinbejar/OptionStratLib · [crates.io]https://crates.io/crates/optionstratlib | Options pricing, Greeks, strategies and simulation library. |

<!-- related-projects:end -->