decimal-bytes
Arbitrary precision decimals with lexicographically sortable byte encoding.
Overview
This crate provides two decimal types optimized for database storage:
Decimal: Variable-length arbitrary precision (up to 131,072 digits)Decimal64: Fixed 8-byte representation (precision ≤ 16 digits)
Both types support PostgreSQL special values (NaN, ±Infinity) with correct sort ordering.
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.
When to Use Which
| Type | Precision | Scale | Storage | Best For |
|---|---|---|---|---|
Decimal64 |
≤ 16 digits | 0-18 | 8 bytes | Financial data, fixed-size storage |
Decimal |
Unlimited | Unlimited | Variable | Scientific, very large numbers |
Features
- Dual storage options: Fixed 8-byte (
Decimal64) or variable-length (Decimal) - Lexicographic ordering: Byte comparison matches numerical comparison
- PostgreSQL NUMERIC compatibility: Full support for precision, scale (including negative), and special values
- Special values: Infinity, -Infinity, and NaN with correct PostgreSQL sort order
Decimal64 Usage
For most financial and business applications where precision ≤ 16 digits:
use Decimal64;
// Create with scale
let price = new.unwrap;
assert_eq!;
assert_eq!;
// Parse with automatic scale detection
let d: Decimal64 = "123.456".parse.unwrap;
assert_eq!;
// Access raw components
let value = price.value; // 9999 (scaled integer)
let scale = price.scale; // 2
// Special values (PostgreSQL compatible)
let inf = infinity;
let neg_inf = neg_infinity;
let nan = nan;
// Correct sort order: -Infinity < numbers < +Infinity < NaN
assert!;
assert!;
assert!;
// NaN equals NaN (PostgreSQL semantics)
assert_eq!;
Decimal64 with Precision and Scale (PostgreSQL NUMERIC)
Decimal64 fully supports PostgreSQL's NUMERIC(precision, scale) semantics:
use Decimal64;
// NUMERIC(5, 2) - up to 5 digits total, 2 after decimal
let d = with_precision_scale.unwrap;
assert_eq!; // Rounded to 2 decimal places
// Precision overflow - truncates from left (PostgreSQL behavior)
let d = with_precision_scale.unwrap;
assert_eq!; // Keeps rightmost 5 digits
// NUMERIC(2, -3) - negative scale rounds to powers of 10
let d = with_precision_scale.unwrap;
assert_eq!; // Rounded to nearest 1000
Decimal64 Storage Layout
64-bit packed representation:
┌──────────────────┬─────────────────────────────────────────────────────┐
│ Scale (8 bits) │ Value (56 bits, signed) │
│ Byte 0 │ Bytes 1-7 │
└──────────────────┴─────────────────────────────────────────────────────┘
- Scale byte: 0-18 for normal values, 253/254/255 for -Infinity/+Infinity/NaN
- Value: 56-bit signed integer (-255 to 255-1, ~16 significant digits)
Decimal64 Benefits
- Fixed 8 bytes: Predictable storage, no heap allocation, cache-friendly
- PostgreSQL compatible: Full NUMERIC(p,s) semantics including NaN, ±Infinity
- Fast operations: Single i64 comparison and serialization
Decimal Usage (Arbitrary Precision)
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
Decimal64 vs Decimal Comparison
For values that fit in Decimal64 (≤16 digits), Decimal64 is significantly faster:
| Operation | Decimal | Decimal64 | Speedup |
|---|---|---|---|
| Parse (small int) | 84 ns | 64 ns | 1.3x |
| Parse (16 digits) | 130 ns | 71 ns | 1.8x |
| to_string (small int) | 61 ns | 19 ns | 3.2x |
| to_string (16 digits) | 89 ns | 21 ns | 4.2x |
| Sort 10 values | 313 ns | 71 ns | 4.4x |
| Equality check | ~4 ns | 0.5 ns | 8x |
Memory Usage
| Type | Stack | Heap | Total |
|---|---|---|---|
| Decimal64 | 8 bytes | 0 | 8 bytes |
| Decimal | 24 bytes | ~9 bytes | ~33 bytes |
Decimal64 Operations
| Operation | Time | Notes |
|---|---|---|
Parse (new) |
64-71 ns | Scales with digit count |
to_string() |
19-88 ns | Scales with digit count |
Equality (==) |
0.5 ns | Single i64 comparison |
| Comparison (same scale) | 1.6 ns | Direct value comparison |
| Comparison (diff scale) | 2 ns | Requires normalization |
to_be_bytes() |
0.9 ns | Trivial conversion |
from_be_bytes() |
0.8 ns | Trivial conversion |
is_nan() / is_infinity() |
0.3 ns | Fast special value checks |
Decimal Operations (Arbitrary Precision)
| Operation | Time | Notes |
|---|---|---|
| Byte comparison | ~4 ns | The key use case - compare without decoding |
from_str (parse) |
84-312 ns | Scales with digit count |
to_string |
61-89 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 License - see LICENSE for details.