ancdec 0.3.3

Fast, precise anchored decimal with independent integer/fraction fields (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

8-way comparison: all ancdec types vs rust_decimal, fastnum (D256), fixed-num (Dec19x19), bigdecimal.

Arithmetic

Operation ancdec8 ancdec32 ancdec ancdec128 rust_decimal fastnum_d256 fixed_num bigdecimal
add 9.0 ns 7.9 ns 10.0 ns 16.0 ns 12.5 ns 37.0 ns 1.9 ns 🥇 166 ns
sub 8.5 ns 10.9 ns 17.2 ns 13.8 ns 34.0 ns 1.9 ns 🥇 129 ns
mul 14.6 ns 8.9 ns 🥇 12.8 ns 18.1 ns 13.4 ns 22.8 ns 22.1 ns 58 ns
div 14.0 ns 12.0 ns 🥇 19.4 ns 33.9 ns 27.6 ns 171 ns 40.0 ns 287 ns
rem 16.0 ns 13.9 ns 🥇 24.3 ns 36.9 ns 15.3 ns 63.0 ns 17.1 ns 171 ns
neg_add 3.2 ns 5.3 ns 6.1 ns 14.7 ns 10.9 ns 33.8 ns 1.3 ns 🥇 140 ns
abs 1.7 ns 2.7 ns 4.4 ns 10.3 ns 2.1 ns 2.6 ns 1.1 ns 🥇 31.9 ns

Comparison, Parse, Rounding

Operation ancdec8 ancdec32 ancdec ancdec128 rust_decimal fastnum_d256 fixed_num bigdecimal
cmp 3.8 ns 6.8 ns 8.5 ns 12.3 ns 9.7 ns 14.8 ns 0.97 ns 🥇 8.6 ns
parse 9.4 ns 🥇 14.3 ns 14.2 ns 19.7 ns 12.8 ns 21.6 ns 407 ns 202 ns
round 17.3 ns 12.6 ns 🥇 19.3 ns 36.2 ns 35.8 ns 69.0 ns 21.4 ns 241 ns
floor 14.4 ns 7.3 ns 🥇 14.9 ns 32.5 ns 35.7 ns 108.7 ns 19.5 ns 238 ns
ceil 13.8 ns 8.1 ns 🥇 15.7 ns 31.1 ns 75.3 ns 111.8 ns 18.5 ns 210 ns
sqrt 21.7 ns 🥇 79.0 ns 98.0 ns 402 ns 761 ns 131.7 ns 126.9 ns 3,600 ns

Display, Chaining, Workflow

Operation ancdec8 ancdec32 ancdec ancdec128 rust_decimal fastnum_d256 fixed_num bigdecimal
display 99 ns 93 ns 🥇 108 ns 114.6 ns 120 ns 219 ns 313 ns 220 ns
chain_ops 20.6 ns 12.9 ns 🥇 18.4 ns 41.7 ns 29.1 ns 50.8 ns 21.8 ns 175 ns
sum_10 36.5 ns 19.3 ns 28.3 ns 50.8 ns 118 ns 166.6 ns 2.8 ns 🥇 591 ns
workflow¹ 30.0 ns 21.1 ns 🥇 39.5 ns 100.6 ns 115.4 ns 305.9 ns 1,503 ns 1,048 ns

¹ workflow = parse → add → mul → div → round, all in one call.

High-Precision & Extreme

Operation ancdec128 rust_decimal fastnum_d256 fixed_num bigdecimal
mul (high-prec) 17.9 ns 🥇 29.7 ns 22.9 ns 25.2 ns 131 ns
div (high-prec) 41.6 ns 61.0 ns 407 ns 40.0 ns 🥇 10,800 ns
parse (high-prec) 28.1 ns 🥇 30.1 ns 41.9 ns 381 ns 230 ns
extreme large mul 14.4 ns 17.1 ns 17.2 ns 12.4 ns 82.2 ns
extreme precision add 13.0 ns 10.8 ns 11.4 ns 0.99 ns 🥇 78.6 ns

Accuracy (1÷3 and accumulated error)

Operation ancdec8 ancdec32 ancdec ancdec128 rust_decimal fastnum_d256 fixed_num bigdecimal
1÷3 (div) 4.4 ns 🥇 5.6 ns 10.5 ns 18.8 ns 22.8 ns 277.5 ns 18.8 ns 8,391 ns
⅓+⅓+⅓−1 1.1 ns 🥇 1.2 ns 1.7 ns 4.3 ns 74.5 ns 347.6 ns 509.3 ns 8,647 ns

bigdecimal is slowest on recurring decimals (8.4 µs) because it recomputes arbitrary-precision 1÷3 at runtime on each call. ancdec types produce a truncated result at their fixed precision limit (e.g., AncDec8: 0.33; AncDec128: 38 decimal places). fastnum (D256) is the slowest fixed-precision type at 277 ns for division.

Win Count (fastest across all 24 benchmark groups)

Library Wins
ancdec32 10 — mul, div, rem, round, floor, ceil, display, chain_ops, workflow, extreme_large
fixed_num 8 — add, sub, neg_add, abs, cmp, div_hi_prec, sum_10, extreme_precision
ancdec8 4 — parse, sqrt, accuracy_recurring, accuracy_accumulated
ancdec128 2 — parse_hi, mul_hi_prec
rust_decimal 0
fastnum_d256 0
bigdecimal 0
ancdec (u64) 0

fixed_num (Dec19x19) excels at simple scalar ops because it uses a compile-time-fixed scale with no alignment overhead. ancdec32 dominates general-purpose use because its 32-bit fields fit in a single cache line with fast scalar arithmetic. ancdec8 wins on throughput tasks (sqrt, parse) due to minimal data size.

Analysis

vs rust_decimal: ancdec32 beats it across the board — mul 1.5×, div 2.3×, floor 4.9×, ceil 9.3×, workflow 5.5×. No category where rust_decimal leads ancdec32.

vs fastnum (D256): ancdec32 is 14× faster on div (12 ns vs 171 ns). On recurring decimals (1÷3), ancdec8 is 63× faster (4.4 ns vs 277 ns). fastnum trades general speed for extreme precision width.

vs bigdecimal: ancdec wins everywhere by wide margins — workflow is 50× faster (21 ns vs 1048 ns), 1÷3 is 1900× faster (4.4 ns vs 8391 ns). bigdecimal's heap allocation cost is unavoidable.

vs fixed_num: the only library that beats individual ancdec types in isolated scalar ops (add, sub, cmp). However, fixed_num's parse is catastrophically slow (407 ns vs ancdec8's 9.4 ns — 43× slower), which causes it to collapse in any real-world workflow:

ancdec32 fixed_num
parse 14 ns 407 ns
display 93 ns 313 ns
workflow (parse→add→mul→div→round) 21 ns 1,503 ns71× slower

fixed_num is only faster when values are pre-constructed in memory and you are doing nothing but arithmetic on them in a tight loop. In any workload that includes parsing input or formatting output, ancdec32 wins decisively.

Overall: ancdec32 is the best general-purpose decimal type across this benchmark set, winning 10 of 24 groups and dominating real-world workflow scenarios. All ancdec types combined win 16 of 24 groups. The only library that takes categories from ancdec is fixed_num, and only in workloads that never touch strings.

Running the Benchmarks Yourself

All benchmark code is in benches/ancdec_bench.rs — 24 groups, 8 libraries side by side. Clone the repo and run:

cargo bench

To run a specific group:

cargo bench -- workflow

cargo bench -- "add|sub|mul|div"

The benchmark compares ancdec against rust_decimal, fastnum (D256), fixed-num (Dec19x19), and bigdecimal on identical inputs. Results are written to target/criterion/ as HTML reports.

Benchmarked on Intel Core i7-10750H @ 2.60GHz, Rust 1.87.0 nightly, 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.3

Bug Fixes:

  • Fixed to_i64() panic in debug mode when value equals i64::MIN (AncDec, AncDec128): unary negation of i64::MIN overflows; replaced with wrapping_neg().
  • Fixed to_i128() panic in debug mode when value equals i128::MIN (AncDec128): same root cause, same fix.
  • Fixed Neg operator producing negative zero (neg=true on a zero value) for all 4 types: -AncDec::ZERO now always returns a value with neg=false.
  • Fixed is_neg() returning true for negative zero on AncDec8, AncDec32, AncDec128: method now correctly returns false for any zero value.

Tests:

  • Added regression tests: test_to_i64_min, test_to_i64_min_128, test_to_i128_min, test_neg_zero, test_neg_zero_128.

v0.3.2

  • Updated crate description.

v0.3.1

  • Minor maintenance release.

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

AI Usage

The core AncDec (u64) type — including its architecture, algorithms, and all implementation details — was designed and written entirely by the author from scratch without AI assistance.

AI tools (Claude) were used to accelerate the development of derivative types (AncDec8, AncDec32, AncDec128) whose implementations follow the same patterns as the original AncDec with adjusted integer sizes and scale ranges. The author reviewed and verified all AI-generated code.