https://github.com/dunnock/decimal64
Rust 64 bit fixed scale decimal based on underlying i64, supports parsing from float string repr
https://github.com/dunnock/decimal64
Last synced: about 2 months ago
JSON representation
Rust 64 bit fixed scale decimal based on underlying i64, supports parsing from float string repr
- Host: GitHub
- URL: https://github.com/dunnock/decimal64
- Owner: dunnock
- Created: 2026-05-26T15:25:00.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2026-05-27T15:09:15.000Z (about 2 months ago)
- Last Synced: 2026-05-27T17:08:10.658Z (about 2 months ago)
- Language: Rust
- Size: 50.8 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# decimal64
64-bit fixed-point decimal with compile-time scale, for Rust.
[](https://crates.io/crates/decimal64)
[](https://docs.rs/decimal64)
## Quick start
```rust
use decimal64::Decimal64;
fn main() -> Result<(), Box> {
let price: Decimal64<4> = "123.4567".parse()?;
let qty: Decimal64<4> = "10.0000".parse()?;
// same-scale arithmetic
let total = price * qty;
println!("{}", total); // "1234.5670" (truncation in fractional component)
// rescale before mixing scales
let qty2: Decimal64<2> = "10.00".parse()?;
let total2 = price * qty2.rescale_into::<4>().unwrap();
println!("{}", total2); // "1234.5670"
Ok(())
}
```
## Scale table
`Decimal64` stores the mathematical value as an `i64` whose unit is `10^(-S)`.
| S | Unit name | Typical use | Max absolute value |
|----|--------------|------------------------------------------|----------------------|
| 2 | centis | USD cents, stock shares (round lots) | ~92.2 quadrillion |
| 4 | basis points | FX rates, equity prices | ~922 trillion |
| 6 | micros | Crypto prices, nanosecond timing | ~9.22 trillion |
| 9 | nanos | Gas prices (Ethereum), high-freq rates | ~9.22 billion |
| 18 | attos | Smallest Ethereum unit (wei) | ~9.22 |
The scale `S` is enforced at compile time: `Decimal64 + Decimal64` where `A ≠ B`
is a **compile error**. Use `rescale_into` to align scales explicitly.
`S > 18` is rejected at compile time (would overflow `ONE = 10^S`).
## Arithmetic rules
### Addition and subtraction
Both operands must share the same scale `S` — enforced by the type system.
```rust
let a: Decimal64<4> = "1.2345".parse().unwrap();
let b: Decimal64<4> = "0.0001".parse().unwrap();
let c = a + b; // Decimal64<4>("1.2346")
```
Overflow panics. Use `checked_add` / `saturating_add` for fallible or clamping variants.
### Multiplication (same-scale)
`a * b` computes `(a.raw() as i128 * b.raw() as i128) / 10^S` and returns a `Decimal64`.
The intermediate `i128` prevents overflow for valid `i64` inputs. The fractional component
of the product is **truncated toward zero** (keeping scale `S`, not `2S`).
Overflow panics. Use `checked_mul` / `saturating_mul` for fallible or clamping variants.
### Division
`a / b` computes `(a.raw() * 10^S) / b.raw()` in `i128`, **truncating toward zero**.
Division by zero panics. Use `checked_div` or `div_round(rhs, mode)` for alternatives.
### Checked and saturating variants
All operators have checked and saturating companions:
```rust
a.checked_add(b) // → Option>
a.checked_sub(b) // → Option>
a.checked_mul(b) // → Option>
a.checked_div(b) // → Option> (None for div-by-zero)
a.saturating_add(b) // → Decimal64 (clamps to MAX/MIN)
a.saturating_sub(b) // → Decimal64
a.saturating_mul(b) // → Decimal64
a.div_round(b, Round::NearestEven) // panics on div-by-zero
a.checked_div_round(b, Round::TowardPosInf) // → Option>
```
### Rescaling
```rust
let d2: Decimal64<2> = "1.25".parse().unwrap();
let d4: Decimal64<4> = d2.rescale_into::<4>().unwrap(); // 1.2500 — exact, always succeeds
let d1: Decimal64<1> = d2.rescale_into::<1>(); // None — "1.25" has fractional digits lost
let d1r = d2.rescale_round_into::<1>(Round::Nearest).unwrap(); // 1.3
```
## Parsing
`Decimal64` implements `FromStr`. Extra fractional digits beyond `S` are silently
**truncated toward zero**. Scientific notation and underscore separators are not supported.
```rust
let d: Decimal64<4> = "123.45678".parse().unwrap(); // stores 123.4567 (truncated)
let d: Decimal64<4> = ".5".parse().unwrap(); // 0.5000
let d: Decimal64<4> = "5.".parse().unwrap(); // 5.0000
```
## f64 conversions
```rust
let d = Decimal64::<4>::from_f64(1.23456789); // NearestEven rounding
let d = Decimal64::<4>::from_f64_round(x, Round::Nearest);
let f: f64 = d.to_f64();
```
`from_f64` clamps on overflow; `NaN` maps to `ZERO`. `to_f64` is lossless for
`|raw| < 2^53`; larger values lose precision inherent to `f64`.
## Benchmark
Parse throughput vs. competitors at `Decimal64::<4>` on x86-64 Linux (stable Rust, optimised):
| Input | decimal64 (M/s) | f64 (M/s) | rust_decimal (M/s) | bigdecimal (M/s) |
|---------------------|-----------------|-----------|---------------------|------------------|
| `"0"` | 214.6 | 157.2 | 102.0 | 37.1 |
| `"1.23"` | 191.2 | 130.9 | 90.6 | 20.9 |
| `"123.4567"` | 155.8 | 115.9 | 83.5 | 18.5 |
| `"9999999999.9999"` | 118.2 | 113.5 | 73.8 | 15.9 |
| `"-0.000001"` | 166.6 | 115.2 | 79.5 | 18.3 |
Full results and arithmetic benchmarks: [`docs/bench-results.md`](docs/bench-results.md).
## System requirements
- **Stable Rust** — MSRV: 1.65 (edition 2021, `f64::round_ties_even` since 1.77;
see Cargo.toml for exact MSRV)
- No external C dependencies
- `std` required (for `Display` and `Error`; `no_std` is a future cycle goal)
## Design
See [`docs/design.md`](docs/design.md) for the full design rationale, arithmetic rules,
parse algorithm, and const-generic limitations on stable Rust.