decimal-bytes
Arbitrary precision decimals with lexicographically sortable byte encoding.
Overview
This crate provides a Decimal type that stores decimal numbers as bytes in a format that preserves numerical ordering when compared lexicographically. This makes it ideal for use in databases and search engines where efficient range queries on decimal values are needed.
Why not use rust_decimal or bigdecimal? Those libraries are excellent for arithmetic, but their byte representations are not lexicographically sortable. You cannot compare their serialized bytes to determine numerical order - you must deserialize first. decimal-bytes solves this by providing a byte encoding where bytes(a) < bytes(b) if and only if a < b numerically.
Features
- Bytes-first storage: The primary representation is a compact byte array - no constant conversions
- Lexicographic ordering: Byte comparison matches numerical comparison
- Arbitrary precision: Supports up to 131,072 digits before and 16,383 digits after the decimal point
- PostgreSQL NUMERIC compatibility: Full support for precision, scale (including negative), and special values
- Special values: Infinity, -Infinity, and NaN with correct PostgreSQL sort order
Usage
use Decimal;
// Create decimals from strings
let a = from_str.unwrap;
let b = from_str.unwrap;
// Byte comparison matches numerical comparison
assert!;
assert!;
// With precision and scale constraints (SQL NUMERIC semantics)
let d = with_precision_scale.unwrap;
assert_eq!; // Rounded to 2 decimal places
// Negative scale (rounds to left of decimal point)
let d = with_precision_scale.unwrap;
assert_eq!; // Rounded to nearest 1000
// Efficient byte access (primary representation)
let bytes: & = d.as_bytes;
// Reconstruct from bytes
let restored = from_bytes.unwrap;
assert_eq!;
Special Values
PostgreSQL-compatible special values with correct sort ordering:
use Decimal;
// Create special values
let pos_inf = infinity;
let neg_inf = neg_infinity;
let nan = nan;
// Or parse from strings (case-insensitive)
let inf = from_str.unwrap;
let inf = from_str.unwrap;
let nan = from_str.unwrap;
// Check for special values
assert!;
assert!;
assert!;
assert!;
assert!;
// Sort order: -Infinity < negatives < zero < positives < Infinity < NaN
assert!;
assert!;
assert!;
PostgreSQL vs IEEE 754 Semantics
This library follows PostgreSQL semantics for special values, which differ from IEEE 754 floating-point:
| Behavior | PostgreSQL / decimal-bytes | IEEE 754 float |
|---|---|---|
NaN == NaN |
true |
false |
NaN ordering |
Greatest value (> Infinity) | Unordered |
Infinity == Infinity |
true |
true |
use Decimal;
let nan1 = nan;
let nan2 = nan;
let inf = infinity;
// NaN equals itself (PostgreSQL behavior, unlike IEEE 754)
assert_eq!;
// NaN is greater than everything, including Infinity
assert!;
This makes Decimal suitable for use in indexes, sorting, and deduplication where consistent ordering and equality semantics are required.
PostgreSQL Compatibility
This crate implements the PostgreSQL NUMERIC specification:
| Feature | Support |
|---|---|
| Max digits before decimal | 131,072 |
| Max digits after decimal | 16,383 |
| Precision constraint | ✓ |
| Scale constraint (positive) | ✓ |
| Scale constraint (negative) | ✓ |
| Infinity | ✓ |
| -Infinity | ✓ |
| NaN | ✓ |
| Rounding (ties away from zero) | ✓ |
Storage Efficiency
The encoding matches PostgreSQL's storage efficiency (2 bytes per 4 decimal digits):
- 1 byte for sign
- 2 bytes for exponent
- ~N/2 bytes for N-digit mantissa (BCD encoding: 2 digits per byte)
- Special values: 3 bytes each
Example: A 9-digit number like 123456789 requires only ~8 bytes total.
Sort Order
The lexicographic byte order matches the PostgreSQL NUMERIC sort order:
-Infinity < negative numbers < zero < positive numbers < +Infinity < NaN
This enables efficient range queries in sorted key-value stores without decoding.
Performance
Key performance characteristics (see latest benchmark results for up-to-date numbers):
| Operation | Time | Notes |
|---|---|---|
| Byte comparison | ~4 ns | The key use case - compare without decoding |
Decimal comparison |
~4-5 ns | Uses byte comparison internally |
from_str (parse) |
88-375 ns | Scales with digit count |
to_string |
71-286 ns | Scales with digit count |
from_bytes |
58-261 ns | With validation |
from_bytes_unchecked |
~15 ns | Skip validation if bytes are trusted |
is_nan() / is_infinity() |
~1.3 ns | Fast special value checks |
Run cargo bench locally to reproduce benchmarks on your hardware.
Arithmetic Operations
This library focuses on storage and comparison, not arithmetic. Existing Rust decimal libraries (rust_decimal, bigdecimal) provide arithmetic but their byte representations are not lexicographically sortable - you cannot compare their serialized bytes to determine numerical order. That's the gap decimal-bytes fills: efficient storage with byte-level ordering for databases and search engines.
For calculations, use an established decimal library and convert:
With rust_decimal (recommended for most use cases)
[]
= { = "0.1", = ["rust_decimal"] }
use Decimal as RustDecimal;
use Decimal;
// Convert from rust_decimal for storage
let rd = new; // 123.45
let stored: Decimal = rd.try_into.unwrap;
// Do arithmetic with rust_decimal
let a: RustDecimal = .try_into.unwrap;
let b = new; // 10.00
let sum = a + b; // 133.45
// Convert back for storage
let result: Decimal = sum.try_into.unwrap;
With bigdecimal (for arbitrary precision arithmetic)
[]
= { = "0.1", = ["bigdecimal"] }
use BigDecimal;
use Decimal;
use FromStr;
// Convert between types
let bd = from_str.unwrap;
let stored: Decimal = bd.try_into.unwrap;
let restored: BigDecimal = .try_into.unwrap;
License
MIT