AncDec
Anchored Decimal
A fast, precise fixed-point decimal type for no_std environments with independent 19-digit integer and 19-digit fractional parts.
Why AncDec?
AncDec's Solution
- Independent storage: 19-digit integer + 19-digit fraction (not shared)
- Exact arithmetic: No floating-point rounding errors
- Overflow-safe: Wide arithmetic (u256) for mul/div prevents overflow
- Fast: 1.2x - 1.8x faster than rust_decimal
- 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
Core Structure
The #[repr(C)] layout enables easy FFI bindings for other languages.
Installation
[]
= "0.2"
Zero dependencies by default. Only core is used (no std, no alloc).
With serde support:
= { = "0.2", = ["serde"] }
With SQLx (PostgreSQL) support:
= { = "0.2", = ["sqlx"] }
Usage
Creating AncDec values
use AncDec;
// From Rust integer primitives via From trait (infallible)
let a: AncDec = 123i64.into;
let b: AncDec = .into;
// From string via parse (returns Result)
let c: AncDec = "456.789".parse?;
let d = parse?;
// TryFrom for &str and floats
let e = try_from?;
let f = try_from?;
Arithmetic
let a: AncDec = "123.456".parse?;
let b: AncDec = "78.9".parse?;
let sum = a + b; // 202.356
let diff = a - b; // 44.556
let product = a * b; // 9740.6784
let quotient = a / b; // 1.5647148288973384030
let remainder = a % b; // 44.556
Comparison
let a: AncDec = "100.5".parse?;
let b: AncDec = "100.50".parse?;
assert!; // true (trailing zeros normalized)
assert!;
Rounding
use RoundMode;
let a: AncDec = "123.456789".parse?;
a.round; // 123.46
a.round; // 123
a.floor; // 123
a.ceil; // 124
a.trunc; // 123
a.fract; // 0.456789
Conversion
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
Supported Types & Traits
From Trait (Infallible)
Integer conversions never fail:
| Type | Example |
|---|---|
i8, i16, i32, i64, i128, isize |
(-123i64).into() |
u8, u16, u32, u64, u128, usize |
123u64.into() |
TryFrom / Parse (Fallible)
String and float conversions return Result<AncDec, ParseError>:
// FromStr trait
let a: AncDec = "123.456".parse?;
// TryFrom<&str>
let b = try_from?;
// TryFrom<f64> - rejects NaN and Infinity
let c = try_from?;
let err = try_from; // Err(InvalidFloat)
// parse method (works with any Display type)
let d = parse?;
let e = parse?;
ParseError
| Variant | Description |
|---|---|
Empty |
Input string is empty |
NoDigits |
No valid digits found |
TrailingChars |
Invalid characters after number |
InvalidFloat |
NaN or Infinity |
Operator Traits
| Trait | Operators |
|---|---|
Add, Sub, Mul, Div, Rem |
+, -, *, /, % |
AddAssign, SubAssign, MulAssign, DivAssign, RemAssign |
+=, -=, *=, /=, %= |
Neg |
-a |
All operators work with AncDec, &AncDec, and integer primitives:
let a: AncDec = 10i64.into;
let b = a + 5; // AncDec + i32
let c = 3 * a; // i32 * AncDec
let d = &a + &a; // &AncDec + &AncDec
Comparison Traits
| Trait | Usage |
|---|---|
PartialEq, Eq |
==, != |
PartialOrd, Ord |
<, >, <=, >=, .cmp() |
Other Traits
| Trait | Usage |
|---|---|
Default |
AncDec::default() returns ZERO |
Hash |
Usable in HashMap, HashSet |
FromStr |
"123.45".parse::<AncDec>() |
TryFrom<&str> |
AncDec::try_from("123.45") |
TryFrom<f32> |
AncDec::try_from(3.14f32) |
TryFrom<f64> |
AncDec::try_from(3.14f64) |
Display |
format!("{}", a), format!("{:.2}", a) |
Sum |
iter.sum::<AncDec>() |
Product |
iter.product::<AncDec>() |
Constants
ZERO // 0
ONE // 1
TWO // 2
TEN // 10
MAX // Maximum representable value
Utility Methods
let a: AncDec = "-123.456".parse?;
a.abs; // 123.456
a.signum; // -1
a.is_positive; // false
a.is_negative; // true
a.is_zero; // false
a.min; // minimum of a and b
a.max; // maximum of a and b
a.clamp; // clamp to range
a.pow; // a³ (binary exponentiation)
Safety Design
AncDec is designed with a layered safety model:
Public API - Always Safe
All public APIs return Result for fallible operations:
| Function | Return Type | Failure Case |
|---|---|---|
"123".parse::<AncDec>() |
Result<AncDec, ParseError> |
Invalid input |
AncDec::try_from("123") |
Result<AncDec, ParseError> |
Invalid input |
AncDec::try_from(3.14f64) |
Result<AncDec, ParseError> |
NaN, Infinity |
AncDec::parse(value) |
Result<AncDec, ParseError> |
Invalid input |
Integer conversions via From trait are infallible by design.
Internal Panic - Unreachable by Design
Internal functions like pow10(exp) contain panic for out-of-range values:
// Internal only (pub(crate))
const
Why this is safe:
pow10ispub(crate)- External code cannot call it directly- Scale is bounded at parse time - Fractional digits are truncated to 19
- Arithmetic preserves bounds - All operations maintain
scale ≤ 19 - The panic exists only to catch internal bugs - If reached, it indicates a logic error in the library itself
This design provides:
- Zero runtime overhead for range checks in hot paths
- Compile-time guarantees through type system
- Defense in depth against internal bugs
Division by Zero
Division by zero panics (consistent with Rust's integer division):
let a: AncDec = 10i64.into;
let b = ZERO;
let c = a / b; // Panics
Use is_zero() to check before division if needed.
Performance Optimizations
Compile-time Power of 10
Power of 10 values are const fn with match expressions:
- Compile-time evaluation when exponent is constant
- Zero heap allocation
- LLVM optimizes to jump table or direct substitution
Aggressive Inlining
All hot-path functions are #[inline(always)]:
- Arithmetic operations
- Comparisons
- Conversions
Benchmarks
Compared against rust_decimal (lower is better):
| Operation | AncDec | rust_decimal | Speedup |
|---|---|---|---|
| add | 6.3 ns | 11.1 ns | 1.76x |
| sub | 6.3 ns | 11.3 ns | 1.79x |
| mul | 9.5 ns | 11.4 ns | 1.20x |
| div | 13.7 ns | 19.8 ns | 1.45x |
| cmp | 4.5 ns | 5.3 ns | 1.18x |
| parse | 11.3 ns | 11.4 ns | 1.01x |
Benchmarked on Intel Core i7-10750H @ 2.60GHz, Rust 1.87.0, release mode
Precision Limits
| Component | Limit | Note |
|---|---|---|
| Integer part | 19 digits | u64::MAX ≈ 1.8 × 10¹⁹ |
| Fractional part | 19 digits | Independent of integer |
| Total precision | 38 digits | Not shared like rust_decimal |
Precision Behavior
- Parsing: Fractional digits beyond 19 are truncated, integer part saturates at
u64::MAX - Multiplication: Results clamped to 19 decimal places
- Division: Results have exactly 19 decimal places
// Fractional truncation during parse
let a: AncDec = "1.1234567890123456789999".parse?;
// Stored as 1.1234567890123456789 (19 digits)
// Integer saturation during parse
let b: AncDec = "99999999999999999999".parse?;
// Stored as u64::MAX (18446744073709551615)
// Multiplication precision
let c: AncDec = "0.1234567890123456789".parse?;
let d = c * c; // Result has 19 decimal places
Features
| Feature | Dependencies | Description |
|---|---|---|
| (default) | None | Core functionality, only uses core |
serde |
serde |
Serialization/deserialization support |
sqlx |
sqlx, std |
PostgreSQL NUMERIC type support |
Serde
When enabled, AncDec serializes as decimal string:
use ;
// JSON: {"amount": "123.456"}
SQLx
When enabled, AncDec can be used directly with PostgreSQL NUMERIC columns:
use ;
use AncDec;
let pool = connect.await?;
// Insert
let price: AncDec = "123.456".parse?;
query
.bind
.execute
.await?;
// Select
let row = query
.bind
.fetch_one
.await?;
let price: AncDec = row.get;
// With query_as
let product: Product = query_as
.bind
.fetch_one
.await?;
Use Cases
- Finance: Exact monetary calculations
- Accounting: Tax, invoicing, ledger systems
- Cryptocurrency: Wei/Satoshi precision arithmetic
- Embedded: Payment terminals, IoT devices (no_std, zero dependencies)
- Games: In-game currency systems
- Scientific: When exact decimal representation matters
Comparison with Alternatives
| Feature | AncDec | rust_decimal | f64 |
|---|---|---|---|
| Precision | 19+19 digits (independent) | 28 digits (shared) | ~15 digits (shared) |
| Exact decimal | ✅ | ✅ | ❌ |
| Overflow handling | ✅ (u256 wide arithmetic) | ✅ | ❌ (silent) |
| no_std | ✅ | ⚠️ (feature flag) | ✅ |
| Zero dependencies | ✅ | ❌ | ✅ |
| Speed (vs rust_decimal) | 1.2-1.8x faster | baseline | ~10x faster |
| FFI-friendly | ✅ (repr(C)) |
❌ | ✅ |
| Size | 24 bytes | 16 bytes | 8 bytes |
License
MIT License
Contributing
Contributions welcome! Please ensure:
- All tests pass (
cargo test) - Serde tests pass (
cargo test --features serde) - SQLx tests pass (
cargo test --features sqlx) - Benchmarks don't regress (
cargo bench) - Code follows existing style