ancdec 0.3.0

Fast, precise fixed-point decimal with independent int/frac storage (u64: 19+19, u128: 38+38 digits)
Documentation

AncDec

Anchored Decimal

A fast, precise fixed-point decimal type for no_std environments with independent integer and fractional parts.

  • AncDec8 (u8): 2-digit integer + 2-digit fraction, 4 bytes — embedded/IoT
  • AncDec32 (u32): 9-digit integer + 9-digit fraction, 12 bytes — general purpose
  • AncDec (u64): 19-digit integer + 19-digit fraction, 24 bytes — financial
  • AncDec128 (u128): 38-digit integer + 38-digit fraction, 40 bytes — institutional

Why AncDec?

  • Independent storage: Integer and fraction stored separately (not shared)
  • Exact arithmetic: No floating-point rounding errors
  • Overflow-safe: Wide arithmetic (u256/u512) for mul/div prevents overflow
  • Fast: Competitive with rust_decimal across all operations
  • no_std: Zero heap allocation, embedded-friendly
  • Zero dependencies: No external crates required (serde, sqlx optional)
  • Safe: All public APIs return Result, internal panics are unreachable by design

Why AncDec128?

AncDec128 was introduced to handle institutional-scale financial data (e.g., BlackRock fund data) where integer parts regularly exceed u64::MAX (~1.8 x 10^19). When processing total asset values, NAV calculations, or aggregated positions, u64 integer overflow was unavoidable.

AncDec128 provides:

  • 38-digit integer part (u128::MAX ~ 3.4 x 10^38)
  • 38-digit fractional part (independent)
  • Tiered fast paths for arithmetic: u64 -> partial product -> u128 -> u256, selecting the cheapest path automatically

Core Structures

AncDec8 (u8) — 4 bytes

pub struct AncDec8 {
    // Fields are pub(crate) - use new() and getters
    int: u8,        // Integer part (up to 2 digits)
    frac: u8,       // Fractional part (up to 2 digits)
    scale: u8,      // Number of decimal places (0-2)
    neg: bool,      // Sign flag
}

AncDec32 (u32) — 12 bytes

pub struct AncDec32 {
    // Fields are pub(crate) - use new() and getters
    int: u32,       // Integer part (up to 9 digits)
    frac: u32,      // Fractional part (up to 9 digits)
    scale: u8,      // Number of decimal places (0-9)
    neg: bool,      // Sign flag
}

AncDec (u64) — 24 bytes

pub struct AncDec {
    pub int: u64,   // Integer part (up to 19 digits)
    pub frac: u64,  // Fractional part (up to 19 digits)
    pub scale: u8,  // Number of decimal places (0-19)
    pub neg: bool,  // Sign flag
}

AncDec128 (u128) — 40 bytes

pub struct AncDec128 {
    // Fields are pub(crate) - use new() and getters
    int: u128,      // Integer part (up to 38 digits)
    frac: u128,     // Fractional part (up to 38 digits)
    scale: u8,      // Number of decimal places (0-38)
    neg: bool,      // Sign flag
}

Why pub(crate) on AncDec8, AncDec32, AncDec128?

Their arithmetic relies on the invariant frac < 10^scale. Fields are pub(crate) to enforce validation through new() with debug_assert! at zero runtime cost in release builds.

AncDec keeps pub fields for backward compatibility and FFI use cases.

All structs use #[repr(C)] layout for FFI bindings.

Installation

[dependencies]

ancdec = "0.3"

Zero dependencies by default. All 4 types included. Only core is used (no std, no alloc).

Minimal embedded build (single type only):

ancdec = { version = "0.3", default-features = false, features = ["dec8"] }

With serde support:

ancdec = { version = "0.3", features = ["serde"] }

With SQLx (PostgreSQL) support:

ancdec = { version = "0.3", features = ["sqlx"] }

Usage

Construction

use ancdec::{AncDec8, AncDec32, AncDec, AncDec128};

// From string (all types)
let a: AncDec8 = "1.23".parse()?;
let b = AncDec32::parse("123456.789")?;
let c: AncDec = "456.789".parse()?;
let d = AncDec128::parse("123456789012345678901234567890.12")?;

// Validated constructor (AncDec8, AncDec32, AncDec128)
let e = AncDec8::new(1, 23, 2, false);     // 1.23
let f = AncDec32::new(123, 456, 3, false);  // 123.456
let g = AncDec128::new(123, 456, 3, false); // 123.456

// AncDec has pub fields — direct construction
let h = AncDec { int: 123, frac: 456, scale: 3, neg: false };

// From integer primitives
let i: AncDec8 = 42u8.into();            // AncDec8: i8, u8
let j: AncDec32 = 1000i32.into();        // AncDec32: i8-i32, u8-u32
let k: AncDec = 123i64.into();           // AncDec: all 12 integer types
let l: AncDec128 = u128::MAX.into();     // AncDec128: all 12 integer types

// From float (all types, fallible)
let m = AncDec::try_from(3.14f64)?;      // TryFrom<f64>
let n = AncDec8::try_from(1.5f32)?;      // TryFrom<f32>

Accessors (AncDec8, AncDec32, AncDec128)

let a = AncDec32::new(123, 456, 3, true);  // -123.456
assert_eq!(a.int(), 123);
assert_eq!(a.frac(), 456);
assert_eq!(a.scale(), 3);
assert!(a.is_neg());

Arithmetic

// All 4 types support: +, -, *, /, %, +=, -=, *=, /=, %=, unary -
let a: AncDec = "12.345".parse()?;
let b: AncDec = "1.2".parse()?;

let sum = a + b;          // 13.545
let diff = a - b;         // 11.145
let product = a * b;      // 14.814
let quotient = a / b;     // 10.2875
let remainder = a % b;    // 0.345
let negated = -a;         // -12.345

// Reference variants
let c = &a + &b;
let d = a + &b;
let e = &a + b;

// Assign operators
let mut x = a;
x += b;
x -= b;
x *= b;
x /= b;
x %= b;

Primitive Arithmetic

// Direct arithmetic with integer primitives (no conversion needed)
let a: AncDec8 = "1.5".parse()?;
let b = a + 2u8;             // AncDec8 + u8 → AncDec8
let c = 3i8 * a;             // i8 * AncDec8 → AncDec8

let d: AncDec32 = "100.5".parse()?;
let e = d + 50i32;           // AncDec32 + i32 → AncDec32
let f = 2u16 * d;            // u16 * AncDec32 → AncDec32

let g: AncDec = "100.0".parse()?;
let h = g / 3i64;            // AncDec / i64 → AncDec
let i = 1000u128 - g;        // u128 - AncDec → AncDec

// Supported primitive types per variant:
// AncDec8:   i8, u8
// AncDec32:  i8, i16, i32, u8, u16, u32
// AncDec:    i8-i128, isize, u8-u128, usize (all 12 types)
// AncDec128: i8-i128, isize, u8-u128, usize (all 12 types)

Cross-Type Arithmetic

use ancdec::{AncDec8, AncDec32, AncDec, AncDec128};

// Smaller + larger → larger type (all 5 ops: +, -, *, /, %)
let a = AncDec8::parse("1.5")?;
let b = AncDec32::parse("2.5")?;
let c: AncDec32 = a + b;             // AncDec8 + AncDec32 → AncDec32
let d: AncDec32 = b * a;             // AncDec32 * AncDec8 → AncDec32

let e: AncDec = AncDec::parse("100.0")? + a;     // AncDec + AncDec8 → AncDec
let f: AncDec128 = AncDec128::parse("1.0")? - b;  // AncDec128 - AncDec32 → AncDec128

// 6 pairs: (8↔32), (8↔64), (8↔128), (32↔64), (32↔128), (64↔128)
// Each pair: 5 ops × 2 directions = 10 impls

// Explicit widening via From (lossless)
let g: AncDec32 = AncDec32::from(a);     // AncDec8 → AncDec32
let h: AncDec = AncDec::from(a);         // AncDec8 → AncDec
let i: AncDec128 = AncDec128::from(b);   // AncDec32 → AncDec128

Math

let a: AncDec = "123.456".parse()?;

// Square root (all 4 types)
// AncDec8: 1-digit fractional, AncDec32: 8-digit, AncDec: 18-digit, AncDec128: 37-digit
let root = a.sqrt();              // 11.1111075555498660

// Power (all 4 types, supports negative exponents)
let squared = a.pow(2);           // 15241.383936
let cubed = a.pow(3);             // 1881640.295202816
let inverse = a.pow(-1);          // 1 / 123.456
let one = a.pow(0);               // 1

// Sign and query (all 4 types)
let abs_val = (-a).abs();         // 123.456
let sign = a.signum();            // 1
assert!(a.is_positive());
assert!(!a.is_negative());
assert!(!a.is_zero());

// Range (all 4 types)
let b: AncDec = "200.0".parse()?;
let min_val = a.min(b);           // 123.456
let max_val = a.max(b);           // 200.0
let clamped = a.clamp(AncDec::ZERO, AncDec::from(100i64));  // 100

Rounding

use ancdec::RoundMode;

// All 4 types support all 7 rounding modes
let a: AncDec = "123.456789".parse()?;

a.round(2, RoundMode::HalfUp);     // 123.46
a.round(2, RoundMode::HalfDown);   // 123.45
a.round(2, RoundMode::HalfEven);   // 123.46
a.round(2, RoundMode::Ceil);       // 123.46
a.round(2, RoundMode::Floor);      // 123.45
a.round(2, RoundMode::Truncate);   // 123.45

// Convenience methods
a.floor();                          // 123
a.ceil();                           // 124
a.trunc();                          // 123
a.fract();                          // 0.456789

Conversion

// Output conversions (all 4 types)
let a: AncDec = "123.456".parse()?;
let f: f64 = a.to_f64();           // 123.456
let i: i64 = a.to_i64();           // 123
let i128: i128 = a.to_i128();      // 123

// Display with precision (all 4 types)
let s = format!("{}", a);           // "123.456"
let s2 = format!("{:.2}", a);       // "123.45"
let s0 = format!("{:.0}", a);       // "123"

Iterator Support

// Sum and Product (all 4 types, owned and reference)
let values: Vec<AncDec> = vec!["1.1", "2.2", "3.3"]
    .into_iter()
    .map(|s| s.parse().unwrap())
    .collect();

let total: AncDec = values.iter().sum();      // 6.6
let product: AncDec = values.iter().product(); // 7.986

Benchmarks

All Types vs rust_decimal

Operation AncDec8 AncDec32 AncDec AncDec128 rust_decimal
add 2.8 ns 4.0 ns 6.3 ns 14.5 ns 11.4 ns
sub 3.2 ns 3.9 ns 6.2 ns 14.9 ns 11.4 ns
mul 4.4 ns 4.3 ns 7.4 ns 13.5 ns 11.1 ns
div 3.8 ns 5.5 ns 13.4 ns 20.3 ns 20.5 ns
cmp 1.2 ns 3.0 ns 4.4 ns 8.0 ns 5.1 ns
parse 5.7 ns 10.5 ns 10.8 ns 14.6 ns 10.4 ns

Speedup vs rust_decimal

Operation AncDec8 AncDec32 AncDec AncDec128
add 4.07x 2.85x 1.81x 0.79x
sub 3.56x 2.92x 1.84x 0.77x
mul 2.52x 2.58x 1.50x 0.82x
div 5.39x 3.73x 1.53x 1.01x
cmp 4.25x 1.70x 1.16x 0.64x
parse 1.82x ~1.0x ~1.0x 0.71x

AncDec128 High Precision

Operation AncDec128 rust_decimal Ratio
mul 19.6 ns 18.3 ns 1.07x
div 54.5 ns 55.8 ns 0.98x
parse 34.7 ns 30.3 ns 1.15x

AncDec128 is comparable to rust_decimal while supporting 38+38 digit precision vs rust_decimal's 28 shared digits.

Benchmarked on Intel Core i7-10750H @ 2.60GHz, Rust 1.87.0, release mode

Performance Architecture

AncDec Multiplication Fast Path

AncDec combines int and frac into a single u128 value (int × 10^scale + frac) before multiplication. When both combined values exceed u64 range, this requires u256 wide arithmetic (mul_wide). To avoid this overhead for common cases:

combined = int × 10^scale + frac

if combined ≤ u64::MAX for both operands:
    → cast to u64, multiply natively (u64 × u64 → u128)     ~7 ns
else:
    → mul_wide (u128 × u128 → u256) + div_wide              ~21 ns

Why this is safe: The fast path guard a ≤ u64::MAX && b ≤ u64::MAX guarantees the product fits in u128, because (2⁶⁴ - 1)² = 2¹²⁸ - 2⁶⁵ + 1 < 2¹²⁸ - 1 = u128::MAX. The operands are explicitly cast down to u64 before multiplication to make the intent unambiguous: (a as u64 as u128) * (b as u64 as u128). When the combined value exceeds u64 range (e.g., int=10^18, scale=19 → combined ≈ 10^37), the condition fails and execution falls through to mul_wide which handles the full u128×u128→u256 range safely.

AncDec128 Tiered Fast Paths

AncDec128 automatically selects the fastest arithmetic path based on operand size:

Multiplication:

Tier Condition Method Cost
1 Both fit in u64 combined Native u64 x u64 ~15 ns
2 Parts fit in u64, scale <= 19 4x partial product ~25 ns
3 Both fit in u128 combined mul_wide + divmod_u256 ~45 ns
4 Everything else Full u256 x u256 -> u512 ~80 ns

Division:

Tier Condition Method Cost
1 Both fit in u64 combined Algebraic decomposition ~25 ns
2 Both fit in u128 combined mul_wide + divmod_u256 ~40 ns
3 Everything else Full u512 / u256 ~80 ns

Performance Cliffs

Performance drops when operands exceed a tier's threshold:

Trigger Effect Typical cause
scale > 19 Skips u64 and partial product tiers div() produces scale=38
int > u64::MAX Skips u64 and partial product tiers Large aggregated values
int * 10^scale + frac > u128::MAX Falls to u256 slow path High-scale large values

Common pattern: a.div(&b).mul(&c) -- division produces scale=38, forcing subsequent multiplication into the u128 or u256 path. This is inherent to the split int/frac representation, not a bug.

Precision Limits

AncDec8 (u8) AncDec32 (u32) AncDec (u64) AncDec128 (u128)
Integer part 2 digits 9 digits 19 digits 38 digits
Fractional part 2 digits 9 digits 19 digits 38 digits
Total precision 4 digits 18 digits 38 digits 76 digits
sqrt() precision 1 digit 8 digits 18 digits 37 digits
Scale range 0-2 0-9 0-19 0-38
Struct size 4 bytes 12 bytes 24 bytes 40 bytes

Fractional digits beyond the limit are truncated during parsing. Integer parts saturate at MAX.

Complete API Reference

Methods (all 4 types)

Category Methods
Construction parse(T), new(int, frac, scale, neg) (8/32/128), direct fields (AncDec)
Accessors int(), frac(), scale(), is_neg() (8/32/128)
Arithmetic add, sub, mul, div, rem, checked_add, checked_sub, checked_mul
Math sqrt(), pow(i32), abs(), signum()
Query is_zero(), is_positive(), is_negative()
Range min(), max(), clamp()
Rounding round(places, mode), floor(), ceil(), trunc(), fract()
Conversion to_f64(), to_i64(), to_i128()

Operator Traits (all 4 types)

Trait Operators Variants
Add, Sub, Mul, Div, Rem +, -, *, /, % value, &ref, cross-type, primitives
AddAssign, SubAssign, MulAssign, DivAssign, RemAssign +=, -=, *=, /=, %=
Neg -a value, &ref

Conversion Traits

Trait AncDec8 AncDec32 AncDec AncDec128
From<i8>, From<u8> Yes Yes Yes Yes
From<i16>, From<u16> Yes Yes Yes
From<i32>, From<u32> Yes Yes Yes
From<i64>, From<u64> Yes Yes
From<i128>, From<u128> Yes Yes
From<isize>, From<usize> Yes Yes
TryFrom<f32>, TryFrom<f64> Yes Yes Yes Yes
TryFrom<&str>, FromStr Yes Yes Yes Yes

Widening From (lossless, cfg-gated)

AncDec8 → AncDec32 → AncDec → AncDec128
From To AncDec32 To AncDec To AncDec128
AncDec8 Yes Yes Yes
AncDec32 Yes Yes
AncDec Yes

Primitive Arithmetic

Type Supported primitives for +, -, *, / (both directions)
AncDec8 i8, u8
AncDec32 i8, i16, i32, u8, u16, u32
AncDec i8-i128, isize, u8-u128, usize (12 types)
AncDec128 i8-i128, isize, u8-u128, usize (12 types)

Cross-Type Arithmetic (cfg-gated)

All 5 operators (+, -, *, /, %) in both directions. Output = larger type.

Pair Output Feature gate
AncDec8 ↔ AncDec32 AncDec32 dec8 + dec32
AncDec8 ↔ AncDec AncDec dec8 + dec64
AncDec8 ↔ AncDec128 AncDec128 dec8 + dec128
AncDec32 ↔ AncDec AncDec dec32 + dec64
AncDec32 ↔ AncDec128 AncDec128 dec32 + dec128
AncDec ↔ AncDec128 AncDec128 dec64 + dec128

Other Traits (all 4 types)

Trait Notes
PartialEq, Eq 0 == -0, trailing zeros normalized
PartialOrd, Ord Total ordering
Hash Normalized (trailing zeros, 0 == -0), usable in HashMap/HashSet
Clone, Copy, Debug Derived
Default Returns ZERO
Display Precision support: format!("{:.2}", a)
Sum, Product Iterator support (owned + reference)
Serialize, Deserialize String-based, with serde feature

Constants

AncDec8::ZERO      AncDec32::ZERO      AncDec::ZERO       AncDec128::ZERO
AncDec8::ONE       AncDec32::ONE       AncDec::ONE        AncDec128::ONE
AncDec8::TWO       AncDec32::TWO       AncDec::TWO        AncDec128::TWO
AncDec8::TEN       AncDec32::TEN       AncDec::TEN        AncDec128::TEN
AncDec8::MAX       AncDec32::MAX       AncDec::MAX        AncDec128::MAX

Features

Feature Dependencies Description
(default) None All 4 types, only uses core
dec8 AncDec8 only
dec32 AncDec32 only
dec64 AncDec only
dec128 AncDec128 only
serde serde Serialization for all enabled types
sqlx sqlx, std PostgreSQL NUMERIC (AncDec only)

Serde

All types serialize as decimal strings:

#[derive(Serialize, Deserialize)]
struct Position {
    sensor: AncDec8,
    price: AncDec,
    total_value: AncDec128,
}
// JSON: {"sensor": "1.23", "price": "123.456", "total_value": "12345678901234567890.123456"}

SQLx (AncDec only)

PostgreSQL NUMERIC binary wire protocol for AncDec only. Implements Type<Postgres>, Encode<Postgres>, Decode<Postgres>.

let price: AncDec = "123.456".parse()?;
sqlx::query("INSERT INTO products (price) VALUES ($1)")
    .bind(&price)
    .execute(&pool)
    .await?;

let row: AncDec = sqlx::query_scalar("SELECT price FROM products")
    .fetch_one(&pool)
    .await?;

Safety Design

Public API - Always Safe

All public APIs return Result for fallible operations. Integer conversions via From are infallible. checked_add, checked_sub, checked_mul return Option<Self> for overflow-safe arithmetic.

Invariant Enforcement

AncDec8, AncDec32, and AncDec128 enforce frac < 10^scale through:

  • pub(crate) fields -- external code must use new() or parse()
  • debug_assert! in new() -- catches violations in debug builds at zero release cost
  • All arithmetic preserves the invariant -- internal construction is trusted

Division by Zero

Division by zero panics (consistent with Rust's integer division). Use is_zero() to check before division.

Comparison with Alternatives

Feature AncDec8 AncDec32 AncDec AncDec128 rust_decimal f64
Integer precision 2 digits 9 digits 19 digits 38 digits 28 shared ~15 shared
Fractional precision 2 digits 9 digits 19 digits 38 digits 28 shared ~15 shared
Exact decimal Yes Yes Yes Yes Yes No
no_std Yes Yes Yes Yes Feature flag Yes
Zero dependencies Yes Yes Yes Yes No Yes
FFI-friendly Yes Yes Yes Yes No Yes
Struct size 4 bytes 12 bytes 24 bytes 40 bytes 16 bytes 8 bytes

Changelog

v0.3.0

New Types:

  • AncDec8 (u8): 4-byte decimal for embedded/IoT (2+2 digit precision)
  • AncDec32 (u32): 12-byte decimal for general purpose (9+9 digit precision)

New Features:

  • Feature flags (dec8, dec32, dec64, dec128) for selective compilation
  • Cross-type arithmetic: AncDec8 + AncDec32 → AncDec32 (automatic widening)
  • Widening conversions via From: AncDec8 → AncDec32 → AncDec → AncDec128
  • Serde support for all 4 types
  • sqrt() for AncDec (18-digit fractional precision via Newton-Raphson on u256)
  • sqrt() for AncDec128 (37-digit fractional precision via Newton-Raphson on u512)
  • checked_add, checked_sub, checked_mul for all 4 types (returns Option)

Breaking Changes:

  • Default features changed from none to ["dec8", "dec32", "dec64", "dec128"]
    • Existing code compiles unchanged (all types enabled by default)
    • default-features = false now requires explicit feature selection
  • AncDec128 fields changed from pub to pub(crate)
    • Use AncDec128::new(int, frac, scale, neg) for construction
    • Use .int(), .frac(), .scale(), .is_neg() for field access
    • Enforces frac < 10^scale invariant via debug_assert!

Bug Fixes:

  • Fixed mul_wide overflow in debug mode (hl + lhwrapping_add)

Performance:

  • AncDec mul: u64 fast path bypasses mul_wide when both operands fit in u64 (-65%)
  • AncDec128 mul: partial product fast path for u64-sized operands (-45% for high precision)
  • AncDec128 mul: u64 ultra-fast path for small operands (-10%)
  • AncDec128 div: algebraic decomposition for u64 operands (-33%)
  • AncDec128 div: u128 fast path avoiding full u256 arithmetic (-20%)
  • AncDec128 sub: branchless borrow * limit pattern (-37%)
  • AncDec128 add: branchless overflow * limit pattern (-17%)
  • AncDec128 parse: two-stage u64/u128 accumulator with stage 2 gating (-18%)

v0.2.0

  • Added serde serialization/deserialization support
  • Added SQLx PostgreSQL support
  • Fixed mul/div overflow with u256 wide arithmetic

v0.1.0

  • Initial release with AncDec (u64-based decimal)

License

MIT License

Contributing

Contributions welcome! Please ensure:

  • All tests pass (cargo test)
  • Individual type tests pass (cargo test --no-default-features --features dec8)
  • Serde tests pass (cargo test --features serde)
  • Benchmarks don't regress (cargo bench)
  • Code follows existing style