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
AncDec32 (u32) — 12 bytes
AncDec (u64) — 24 bytes
AncDec128 (u128) — 40 bytes
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
[]
= "0.3"
Zero dependencies by default. All 4 types included. Only core is used (no std, no alloc).
Minimal embedded build (single type only):
= { = "0.3", = false, = ["dec8"] }
With serde support:
= { = "0.3", = ["serde"] }
With SQLx (PostgreSQL) support:
= { = "0.3", = ["sqlx"] }
Usage
Construction
use ;
// From string (all types)
let a: AncDec8 = "1.23".parse?;
let b = parse?;
let c: AncDec = "456.789".parse?;
let d = parse?;
// Validated constructor (AncDec8, AncDec32, AncDec128)
let e = new; // 1.23
let f = new; // 123.456
let g = new; // 123.456
// AncDec has pub fields — direct construction
let h = AncDec ;
// 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 = u128MAX.into; // AncDec128: all 12 integer types
// From float (all types, fallible)
let m = try_from?; // TryFrom<f64>
let n = try_from?; // TryFrom<f32>
Accessors (AncDec8, AncDec32, AncDec128)
let a = new; // -123.456
assert_eq!;
assert_eq!;
assert_eq!;
assert!;
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 ;
// Smaller + larger → larger type (all 5 ops: +, -, *, /, %)
let a = parse?;
let b = parse?;
let c: AncDec32 = a + b; // AncDec8 + AncDec32 → AncDec32
let d: AncDec32 = b * a; // AncDec32 * AncDec8 → AncDec32
let e: AncDec = parse? + a; // AncDec + AncDec8 → AncDec
let f: AncDec128 = parse? - 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 = from; // AncDec8 → AncDec32
let h: AncDec = from; // AncDec8 → AncDec
let i: AncDec128 = from; // 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; // 15241.383936
let cubed = a.pow; // 1881640.295202816
let inverse = a.pow; // 1 / 123.456
let one = a.pow; // 1
// Sign and query (all 4 types)
let abs_val = .abs; // 123.456
let sign = a.signum; // 1
assert!;
assert!;
assert!;
// Range (all 4 types)
let b: AncDec = "200.0".parse?;
let min_val = a.min; // 123.456
let max_val = a.max; // 200.0
let clamped = a.clamp; // 100
Rounding
use RoundMode;
// All 4 types support all 7 rounding modes
let a: AncDec = "123.456789".parse?;
a.round; // 123.46
a.round; // 123.45
a.round; // 123.46
a.round; // 123.46
a.round; // 123.45
a.round; // 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!; // "123.456"
let s2 = format!; // "123.45"
let s0 = format!; // "123"
Iterator Support
// Sum and Product (all 4 types, owned and reference)
let values: = vec!
.into_iter
.map
.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
ZERO ZERO ZERO ZERO
ONE ONE ONE ONE
TWO TWO TWO TWO
TEN TEN TEN TEN
MAX MAX MAX 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:
// 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?;
query
.bind
.execute
.await?;
let row: AncDec = query_scalar
.fetch_one
.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 usenew()orparse()debug_assert!innew()-- 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_mulfor all 4 types (returnsOption)
Breaking Changes:
- Default features changed from none to
["dec8", "dec32", "dec64", "dec128"]- Existing code compiles unchanged (all types enabled by default)
default-features = falsenow requires explicit feature selection
AncDec128fields changed frompubtopub(crate)- Use
AncDec128::new(int, frac, scale, neg)for construction - Use
.int(),.frac(),.scale(),.is_neg()for field access - Enforces
frac < 10^scaleinvariant viadebug_assert!
- Use
Bug Fixes:
- Fixed
mul_wideoverflow in debug mode (hl + lh→wrapping_add)
Performance:
- AncDec mul: u64 fast path bypasses
mul_widewhen 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 * limitpattern (-37%) - AncDec128 add: branchless
overflow * limitpattern (-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