6 releases
| 0.3.2 | Mar 6, 2026 |
|---|---|
| 0.3.1 | Mar 6, 2026 |
| 0.3.0 | Feb 25, 2026 |
| 0.2.0 | Jan 29, 2026 |
| 0.1.1 | Jan 27, 2026 |
#387 in Math
220KB
5K
SLoC
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 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
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.
Dependencies
~0–13MB
~88K SLoC