Skip to content

Getting started

Install

[dependencies]
decimal-scaled = "0.3.1"

no_std (drops std and serde, keeps alloc):

[dependencies]
decimal-scaled = { version = "0.3.1", default-features = false }

See Cargo features for the full list.

The model

Every type is a #[repr(transparent)] newtype around an integer. The const generic SCALE is the base-10 exponent baked into the type:

logical value  =  raw_integer × 10^(-SCALE)

With SCALE = 2, the integer 1999 is the logical value 19.99. There is exactly one representation per value - no normalisation, no per-value scale byte, no heap allocation.

The primary type is D38<const SCALE: u32> (128-bit storage). The scale aliases D38s0D38s38 name specific scales:

use decimal_scaled::{D38, D38s2, D38s12};

type Cents = D38s2;          // == D38<2>
type Pico  = D38<12>;        // same as D38s12

For the narrower (D9, D18) and wider tiers (D56 / D76 / D114 / D153 / D230 / D307 for the wide umbrella; D461 / D615 under x-wide; D923 / D1231 under xx-wide), see the width family.

Constructing values

use decimal_scaled::{D38s2, d38};

// 1. Compile-time literal macro (scale inferred - see the macro guide).
let a = d38!(19.99);

// 2. From an integer, scaled by 10^SCALE.
let b = D38s2::from_int(20);            // 20.00
let c = D38s2::from_i32(-5);            // -5.00

// 3. From the raw storage integer directly (raw = value × 10^SCALE).
let d = D38s2::from_bits(1999);         // 19.99

// 4. Parsing a string.
use core::str::FromStr;
let e = D38s2::from_str("19.99").unwrap();

// 5. Constants.
let zero = D38s2::ZERO;
let one  = D38s2::ONE;                  // raw = 100 at scale 2

from_bits / to_bits are the exact round-trip into and out of the storage integer:

# use decimal_scaled::D38s2;
let v = D38s2::from_bits(1999);
assert_eq!(v.to_bits(), 1999);
assert_eq!(D38s2::multiplier(), 100);   // 10^SCALE
assert_eq!(v.scale(), 2);                // the const generic, as a value

Arithmetic

Add, Sub, Mul, Div, Rem, Neg and their *Assign forms are implemented. Operands must share the same type (same width and scale) - mixing scales is deliberately a compile error; convert explicitly.

# use decimal_scaled::D38s2;
let x = D38s2::from_bits(1050);   // 10.50
let y = D38s2::from_bits(300);    //  3.00

assert_eq!((x + y).to_bits(), 1350);   // 13.50
assert_eq!((x - y).to_bits(),  750);   //  7.50
assert_eq!((x * y).to_bits(), 3150);   // 31.50  (rescaled by 10^SCALE)
assert_eq!((x / y).to_bits(),  350);   //  3.50

Overflow follows Rust's integer convention: debug builds panic, release builds wrap. For explicit control there is the full checked_* / wrapping_* / saturating_* / overflowing_* family:

# use decimal_scaled::D38s2;
assert_eq!(D38s2::MAX.checked_add(D38s2::ONE), None);
assert_eq!(D38s2::MAX.saturating_add(D38s2::ONE), D38s2::MAX);

Formatting and parsing

Display renders the canonical decimal string; Debug shows the type and value; LowerHex / UpperHex / Octal / Binary format the raw storage integer.

# use decimal_scaled::D38s2;
let v = D38s2::from_bits(-2050);
assert_eq!(format!("{v}"), "-20.50");
assert_eq!(format!("{v:?}"), "D38<2>(-20.50)");

FromStr parses the same canonical form. 1.10 and 1.1 parsed at the same scale produce the same bit pattern, so equality and hashing are a single integer comparison.

Rounding helpers

floor, ceil, round (half away from zero), trunc, and fract operate at the type's scale:

# use decimal_scaled::D38s2;
let v = D38s2::from_bits(1250);   // 12.50
assert_eq!(v.floor().to_bits(), 1200);
assert_eq!(v.ceil().to_bits(),  1300);
assert_eq!(v.round().to_bits(), 1300);
assert_eq!(v.trunc().to_bits(), 1200);
assert_eq!(v.fract().to_bits(),   50);

To round to a different scale, use rescale - see the rounding guide.

Next steps