decimal-scaled 0.2.2

Const-generic base-10 fixed-point decimals (D9/D18/D38/D76/D153/D307) with integer-only transcendentals correctly rounded to within 0.5 ULP — exact at the type's last representable place. Deterministic across every platform; no_std-friendly.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# decimal-scaled

A Rust library providing const-generic base-10 fixed-point decimal
types with **correctly-rounded (≤ 0.5 ULP) integer-only
transcendentals**, deterministic across every platform, and
`no_std`-friendly.

📚 In-depth guides — getting started, scale aliases, the width
family, conversions, rounding modes, strict mode, the `d38!`
macro, every Cargo feature, benchmarks — live in
[**`docs/README.md`**](docs/README.md). API reference on
[docs.rs](https://docs.rs/decimal-scaled/).

> **0.5 ULP** — the strongest accuracy guarantee a finite numeric
> type can give — is the headline feature. Every `ln` / `exp` /
> `sin` / `cos` / `sqrt` / `cbrt` / `powf` / `atan` / `atan2` /
> `sinh` / `cosh` / `tanh` / `asinh` / `acosh` / `atanh` /
> `to_degrees` / `to_radians` lands within half an [ULP][ULP] of
> the exact result and the bit pattern is identical on every
> machine. No baseline numeric crate offers this for
> transcendentals. The algorithms, citations and per-function
> implementation notes are catalogued in
> [`ALGORITHMS.md`](ALGORITHMS.md).
>
> [ULP]: https://en.wikipedia.org/wiki/Unit_in_the_last_place

---

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
decimal-scaled = "0.2.2"
```

The default build pulls in the correctly-rounded transcendentals
(the `strict` feature is on by default). To opt in to the faster
`f64`-bridge ("fast") path instead — ~700× quicker on series
functions, but only ≈ 16 decimal digits of platform-libm precision
and **not** platform-deterministic — disable default features and
pick what you actually want:

```toml
[dependencies]
decimal-scaled = { version = "0.2.2", default-features = false, features = ["std", "serde"] }
```

For `no_std` targets (`alloc` is still required):

```toml
[dependencies]
decimal-scaled = { version = "0.2.2", default-features = false, features = ["serde", "alloc", "strict"] }
```

---

## Quick start

```toml
[dependencies]
decimal-scaled = { version = "0.2.2", features = ["macros"] }
```

There are three idiomatic ways to construct a value. Use whichever
fits your call site.

```rust
use decimal_scaled::{d38, D38s12};

// 1) `d38!` macro — the ergonomic constructor. Write the literal
//    as you'd read it; scale is inferred from the fractional
//    digits, or pinned explicitly with `, scale N`. (Requires the
//    `macros` feature.) One macro per width: `d9!`, `d18!`,
//    `d38!`, plus `d76!` / `d153!` / `d307!` under the wide
//    features. Pre-baked per-scale wrappers (`d38s12!`, `d18s6!`,
//    …) skip the `, scale N` and read more tersely at the call
//    site. Full grammar including the `radix N` qualifier in
//    [`macros/README.md`](macros/README.md).
let a = d38!(1.1, scale 12);                        // D38<12> — exactly 1.1

// 2) `FromStr` — parse a decimal string. Works without the
//    `macros` feature and accepts user input directly.
let b: D38s12 = "2.2".parse().unwrap();             // D38<12> — exactly 2.2

// 3) `from_bits` — for hot paths or when you already have the
//    raw integer (value × 10^SCALE). No parsing, no allocation.
let c = D38s12::from_bits(3_300_000_000_000);       // D38<12> — exactly 3.3

// Aliases like `D38s12` are just type aliases over `D38<12>`. The
// generic form works identically and is what you'd use when SCALE
// is itself a const generic in your code:
let _generic: decimal_scaled::D38<12> = D38s12::from_int(42);

// Arithmetic is plain operator overloads — exact for + / − / %,
// rounded (half-to-even) for × / ÷.
let sum     = a + b;                                // 3.3 exactly
let product = a * b;                                // 2.42 exactly
let half    = a / d38!(2, scale 12);

assert_eq!(sum, c);
assert_eq!(sum.to_string(), "3.3");
assert_eq!(a.to_bits(), 1_100_000_000_000);         // value × 10^12

// Constants are available where you need them.
let _zero = D38s12::ZERO;
let _one  = D38s12::ONE;
```

The `macros` feature is opt-in (it pulls in a `proc_macro` build
dependency). Without it, the `FromStr` and `from_bits` paths are
always available.

---

## Type names

The number in each `D<N>` type name is **the number of base-10
digits it can safely represent**, *not* the bit-width of the
underlying integer. The crate's home is decimal arithmetic, so it
names its types in the unit users actually reason about. Mapping:

| type | constructor macro | underlying signed integer | safe decimal digits (= `MAX_SCALE`) | max value at SCALE 0 | required feature | curated per-scale macros |
|---|---|---|---|---|---|---|
| `D9<S>`   | `d9!`   | `i32` (32 bits) | 9 | ±2.1 × 10⁹ | always available | `d9s0!`, `d9s2!`, `d9s4!`, `d9s6!`, `d9s9!` |
| `D18<S>`  | `d18!`  | `i64` (64 bits) | 18 | ±9.2 × 10¹⁸ | always available | `d18s0!`, `d18s2!`, `d18s4!`, `d18s6!`, `d18s9!`, `d18s12!`, `d18s18!` |
| `D38<S>`  | `d38!`  | `i128` (128 bits) | 38 | ±1.7 × 10³⁸ | always available | `d38s0!`, `d38s2!`, `d38s4!`, `d38s6!`, `d38s8!`, `d38s9!`, `d38s12!`, `d38s15!`, `d38s18!`, `d38s24!`, `d38s35!`, `d38s38!` |
| `D76<S>`  | `d76!`  | `Int256` (256 bits, in-tree wide-int) | 76 | ±5.8 × 10⁷⁶ | `d76` / `wide` | `d76s0!`, `d76s2!`, `d76s6!`, `d76s12!`, `d76s18!`, `d76s35!`, `d76s50!`, `d76s76!` |
| `D153<S>` | `d153!` | `Int512` (512 bits) | 153 | ±6.7 × 10¹⁵³ | `d153` / `wide` | `d153s0!`, `d153s35!`, `d153s75!`, `d153s150!`, `d153s153!` |
| `D307<S>` | `d307!` | `Int1024` (1024 bits) | 307 | ±9.0 × 10³⁰⁷ | `d307` / `x-wide` | `d307s0!`, `d307s35!`, `d307s150!`, `d307s300!`, `d307s307!` |

The number in each type name (`9`, `18`, `38`, …) is the type's
`MAX_SCALE` — equivalently, the safe-decimal-digits count
`⌊(bits − 1) · log₁₀ 2⌋`. The largest scale at which every
`MAX_SCALE`-digit decimal value (`±999…9`) fits the signed
storage; also the largest `S` you can pass as the const generic
parameter. `D38<38>` therefore represents values in `[−1.7, 1.7]`
with 38 fractional digits; `D38<0>` represents integers up to
`±10³⁸`.

All constructor macros require the `macros` feature in addition
to any per-tier feature listed above. Per-scale macros pre-bake
`scale N` and forward every other qualifier (`rounded`,
`radix N`) to the underlying constructor. Scales outside the
curated subset remain reachable via the explicit `, scale N`
qualifier on the main constructor.

Pick the narrowest tier whose range covers your values at the
scale you need. Widening is free (`From` / `widen()`); narrowing
is fallible (`TryFrom` / `narrow()`).

---

## Why another numeric type?

Every numeric type makes a choice about which numbers it can represent exactly. There is no universal answer - the right choice depends on where the numbers come from.

### The binary fraction problem

Standard floating-point types (`f32`, `f64`, `f128`) store values as:

```
value = mantissa × 2^exponent
```

This means the fractional part of any stored number must be expressible as a sum of negative powers of two: ½, ¼, ⅛, 1/16, …

The number `1.1` cannot be expressed this way. In binary it is:

```
1.0001100110011001100110011001100110011001100110011...  (repeating forever)
```

A 64-bit float truncates this at 52 mantissa bits. The value actually stored is:

```
1.100000000000000088817841970012523233890533447265625
```

This is not a bug - it is an unavoidable consequence of the representation. The same applies to `0.1`, `0.2`, `0.3`, and most everyday decimal fractions. This is why `0.1 + 0.2 == 0.3` is `false` in every binary floating-point system.

### The binary fixed-point alternative

The `fixed` crate (`I64F64`, `I32F32`, etc.) uses binary fixed-point: a fixed number of bits for the integer part and a fixed number of bits for the fractional part. A value is stored as:

```
value = raw_integer × 2^(-FRAC_BITS)
```

This eliminates the rounding from exponent adjustments, but the representable fractions are still powers of two. `I64F64` cannot represent `0.1` exactly either. It excels at signal processing, physics simulations, and anywhere numbers arrive as binary data or are generated by mathematical operations.

### Base-10 fixed-point: filling the gap

`decimal-scaled` uses base-10 fixed-point:

```
value = raw_integer × 10^(-SCALE)
```

With `SCALE = 12`, the number `1.1` is stored as the integer `1_100_000_000_000`. It is exact. Every number a human can write with up to `SCALE` decimal digits is represented exactly. The tradeoff is that numbers like `1/3` or `π` still cannot be represented exactly - no finite representation can hold every number. The question is always *which* numbers you need to be exact.

### Choosing the right number space

All numeric types have a finite number space. The choice is which region of the real line to cover densely and which values to round.

| System | Decimal places | Exact for | Rounds | Best suited for |
|---|---|---|---|---|
| `f64` | dynamic (binary exponent, not decimal) | powers-of-2 fractions | decimal fractions like 0.1 | scientific computation, computer-generated values |
| `f128` | dynamic (binary exponent, not decimal) | powers-of-2 fractions (more precision) | decimal fractions | high-precision scientific work |
| `fixed::I64F64` | fixed (64 binary fractional bits, not decimal) | binary fixed fractions | decimal fractions | digital signal processing, physics, binary data |
| `rust_decimal` | variable per value (0–28, stored alongside each number) | decimal fractions up to 28 digits | repeating decimals | finance, variable scale |
| `bigdecimal` | variable per value (unbounded, heap-allocated) | any terminating decimal | repeating decimals | arbitrary-precision decimal work |
| `D38<S>` (this crate) | **fixed to `S` at compile time** | decimal fractions up to `S` digits | repeating decimals | finance, computer-aided design, human-entered values |

**Use `decimal-scaled` when:**
- Values are entered by humans as decimal strings (prices, measurements, quantities)
- You need deterministic, platform-identical results across every machine
- The scale is known at compile time and you want zero-cost const-generic specialisation
- You need `no_std` compatibility
- You want a single canonical representation per value (no normalisation step)

**Use `f64` or `f128` when:**
- Values come from sensors, physics engines, or mathematical operations
- The number space is continuous and decimal fractions are not special
- You need the dynamic range of [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) binary floating-point (from ~10⁻³⁰⁸ to ~10³⁰⁸)

**Use `fixed` when:**
- Values are in a known integer-and-fraction format from binary protocols
- You are doing digital signal processing or embedded arithmetic where binary fractions are natural
- You need the best throughput on platforms without hardware decimal support

**Use `rust_decimal` when:**
- Scale varies between values (e.g. mixing 0.1 and 0.001 in the same collection)
- You need up to 28 significant decimal digits
- You are happy to carry a per-value scale byte and pay normalisation cost on equality/hash

**Use `bigdecimal` when:**
- Precision requirements are unbounded or unknown at compile time
- Throughput is not a concern

---

## What `decimal-scaled` provides

`D38<const SCALE: u32>` is a `#[repr(transparent)]` newtype around `i128`. The const generic `SCALE` is the base-10 exponent baked into the type at compile time. There is exactly one representation per value: no normalisation, no variable scale, no heap allocation.

```
stored = logical_value × 10^SCALE
```

With `SCALE = 12`, the value `1.5` is stored as `1_500_000_000_000i128`.

### Properties

- **Deterministic** - arithmetic is pure integer; identical bit-pattern outputs on every platform.
- **Canonical** - one scale means one representation per value. `Hash`, `Eq`, and `Ord` are derived directly from `i128`. Two values that are equal always hash identically, with no normalisation step.
- **`no_std` compatible** - compiles with `no_std + alloc` when default features are disabled.
- **`num-traits` compatible** - implements `Zero`, `One`, `Num`, `Bounded`, `Signed`, `FromPrimitive`, `ToPrimitive`, and the `Checked*` family.
- **`serde` support** - canonical-string serialize/deserialize behind the `serde` feature (on by default).
- **Const-generic scale** - additional scale variants (`D38<6>`, `D38<18>`) are free type aliases, not separate implementations.

---

## Numeric comparison table

| Type                           | Storage | Base | `0.1` exact | `1.1` exact | Range | Accuracy (error bound) | `no_std` |
|--------------------------------|---|---|---|---|---|---|---|
| `f32`                          | 32-bit IEEE 754 | 2 | No | No | ~±3.4 × 10³⁸ | basic ops: ≤ 0.5 [ULP](https://en.wikipedia.org/wiki/Unit_in_the_last_place) (IEEE 754); transcendentals: libm-defined, not guaranteed | Yes |
| `f64`                          | 64-bit IEEE 754 | 2 | No | No | ~±1.8 × 10³⁰⁸ | basic ops: ≤ 0.5 ULP (IEEE 754); transcendentals: libm-defined, not guaranteed | Yes |
| `f128`                         | 128-bit IEEE 754 | 2 | No | No | ~±1.2 × 10⁴⁹³² | basic ops: ≤ 0.5 ULP (IEEE 754); transcendentals: libm-defined, not guaranteed | Partial |
| `fixed::I64F64`                | 128-bit binary fixed | 2 | No | No | ~±9.2 × 10¹⁸ | add/sub: exact; mul/div: ≤ 1 ULP; no transcendentals | Yes |
| `fixed::I32F32`                | 64-bit binary fixed | 2 | No | No | ~±2.1 × 10⁹ | add/sub: exact; mul/div: ≤ 1 ULP; no transcendentals | Yes |
| `rust_decimal`                 | 96-bit + per-value scale (0..=28) | 10 | Yes | Yes | ±7.9 × 10²⁸ | add/sub: exact at common scale; mul/div: ≤ 1 ULP; transcendentals: software, **not** correctly rounded | Yes |
| `bigdecimal`                   | heap-allocated arbitrary precision | 10 | Yes | Yes | Unbounded | exact at the configured precision; transcendentals: limited | No |
| `D38<S>` (this)               | 128-bit integer, scale fixed at compile time, S ∈ 0..=38 | 10 | Yes | Yes | ±i128::MAX / 10ˢ | add/sub: **exact**; mul/div: ≤ 1 ULP; **strict transcendentals: ≤ 0.5 ULP (correctly rounded)** | Yes |
| `D76<S>` / `D153<S>` / `D307<S>` (this, `wide`) | 256 / 512 / 1024-bit integer, S up to 76 / 153 / 307 | 10 | Yes | Yes | wider, S-dependent | same accuracy as `D38<S>` | Yes |

The accuracy column gives the error bound on computed results, in [ULPs](https://en.wikipedia.org/wiki/Unit_in_the_last_place) (units in the last place). A 0.5 ULP bound — "correctly rounded" — is the IEEE-754 round-to-nearest contract and the strongest accuracy guarantee a finite numeric type can give. The floats meet it for basic arithmetic but not for transcendentals; `decimal-scaled`'s strict transcendentals meet it for transcendentals as well, which is the capability the alternatives do not offer. The position of the ULP — the absolute size of `1 ULP` — is the type's *scale*: for `f64` it's a relative ~2⁻⁵² of the value's magnitude, for `D38<S>` it's exactly `10⁻ˢ` at every value, fixed at compile time.

### Hash and equality contracts

A well-behaved numeric type must satisfy: if `a == b` then `hash(a) == hash(b)`. The way different types handle this for values like `1.1` and `1.10` varies significantly.

| Type | `1.10 == 1.1`? | `hash(1.10) == hash(1.1)`? | `Hash` implemented? | How |
|---|---|---|---|---|
| `f32` / `f64` | Yes (same bit pattern) | N/A | No - Not-a-Number breaks the contract | - |
| `f128` | Yes (same bit pattern) | N/A | No | - |
| `fixed::I64F64` | Yes (same binary approximation) | Yes | Yes | structural (one representation) |
| `rust_decimal` | Yes | Yes | Yes | normalises trailing zeros at comparison and hash time |
| `bigdecimal` | Yes | Yes | Yes | normalises at comparison and hash time |
| `D38<S>` (this) | Yes | Yes | Yes | structural - scale is fixed, one bit pattern per value |

`f32`, `f64`, and `f128` do not implement `Hash` in the Rust standard library because Not-a-Number values are not equal to themselves (`NaN != NaN`) while a structural hash would make all such values collide - the contract cannot be satisfied without special-casing.

For `rust_decimal` and `bigdecimal`, the normalisation is correct but carries a runtime cost on every comparison and hash call, and it means the stored representation is not canonical - you cannot memcmp two values.

`D38<S>` derives `Hash`, `Eq`, and `Ord` directly from `i128`. Because the scale is fixed at compile time there is exactly one `i128` value per logical number. `1.10` and `1.1` parsed via `FromStr` both produce `D38s12(1_100_000_000_000)` - the same bit pattern - so equality and hashing are a single integer comparison with no runtime normalisation.

### Key differences from `fixed`

The `fixed` crate's `I64F64` has 64 bits of integer and 64 bits of binary fraction. Its least significant bit is 2⁻⁶⁴ ≈ 5.4 × 10⁻²⁰, and its maximum value is 2⁶³ - 1 ≈ 9.2 × 10¹⁸.

`D38<20>` has a least significant decimal digit of 10⁻²⁰ and a maximum value of i128::MAX / 10²⁰ ≈ 1.7 × 10¹⁸ model units. The two types offer comparable precision and range in this configuration, but with opposite trade-offs: `I64F64` represents its fractional part in binary (exact for powers of two, rounded for decimal fractions), while `D38<20>` represents it in decimal (exact for decimal fractions, rounded for fractions like 1/3).

For human-scale decimal values `D38` gives decimal-exact results with no rounding on input or output. For values derived from binary arithmetic or mathematical operations, `I64F64` avoids the binary-to-decimal rounding boundary entirely.

---

## Performance and accuracy

`D38` arithmetic is a thin wrapper over `i128`: add / sub are a single
instruction (~1 ns), mul / div carry a 256-bit widening step (~10 ns).
The wide `D76` tier keeps add / sub almost free but its mul / div pay
for a 256-bit hand-rolled-integer divide — roughly 20× the `D38` cost.

Transcendentals (`ln`, `exp`, `sqrt`, trig, …) come in two forms. The
**fast** `f64`-bridge form (~40 ns) inherits `f64`'s precision
ceiling and is not platform-independent. The **strict** integer-only
form — **on by default** — is **correctly rounded to within 0.5 ULP**
of the exact result (the IEEE-754 round-to-nearest contract) and is
`no_std` and platform-deterministic. Build with `default-features =
false, features = ["std", "serde"]` to switch the plain `ln` / `exp` /
… surface to the fast f64 bridge.

The full per-algorithm catalogue with citations, equations, and the
Wikipedia / Wolfram MathWorld / author-homepage links lives in
[`ALGORITHMS.md`](ALGORITHMS.md).

### Transcendental accuracy comparison

A *correctly rounded* result is the exact mathematical value rounded
to the nearest representable number — i.e. the error is at most half a
ULP. It is the strongest accuracy guarantee a finite type can give,
and the capability the alternatives do not offer:

| Type | Transcendentals | Correctly rounded to 0.5 ULP | Platform-deterministic |
|---|---|---|---|
| `f32` / `f64` (platform libm) | yes | no — `libm` is not guaranteed correctly rounded | no |
| `fixed` (`I64F64`, …) | none | — | — |
| `bigdecimal` | none | — | — |
| `rust_decimal` (`MathematicalOps`) | yes | no — accurate, but not to the last place | yes |
| `decimal-scaled` — fast (`f64` bridge, opt-in) | yes | no — inherits `f64` | no |
| `decimal-scaled` — **strict** (default, `*_strict`) | yes | **yes — within 0.5 ULP** | **yes** |

For series functions the strict form costs ~700× the fast bridge;
`sqrt_strict` is the exception — algebraic, so it ties the fast form.
Full head-to-head measurements against `bnum`, `ruint`, `rust_decimal`,
and `fixed` are in [`docs/benchmarks.md`](docs/benchmarks.md).

---

## Scale aliases

| Alias | `SCALE` | 1 least significant decimal digit | Approximate range |
|---|---|---|---|
| `D38s0` | 0 | 1 | ±1.7 × 10³⁸ |
| `D38s2` | 2 | 0.01 (cents) | ±1.7 × 10³⁶ |
| `D38s6` | 6 | 10⁻⁶ (µ) | ±1.7 × 10³² |
| `D38s12` | 12 | 10⁻¹² (p) | ±1.7 × 10²⁶ |
| `D38s18` | 18 | 10⁻¹⁸ (a) | ±1.7 × 10²⁰ |
| `D38s38` | 38 | 10⁻³⁸ | ±1.7 |

Aliases `D38s0` through `D38s38` are all provided. `SCALE = 39` would overflow `i128`.

---

## The width family

`D38` is the foundation, but it is one of six storage widths that share
an identical API and the `Decimal` trait:

| Type | Storage | `MAX_SCALE` | Feature gate |
|---|---|---|---|
| `D9`   | 32-bit  | 9   | always on |
| `D18`   | 64-bit  | 18  | always on |
| `D38`  | 128-bit | 38  | always on |
| `D76`  | 256-bit | 76  | `d76` / `wide` |
| `D153`  | 512-bit | 153 | `d153` / `wide` |
| `D307` | 1024-bit| 307 | `d307` / `wide` |

Pick the narrowest width whose range covers your values at the scale you
need. Widening between widths is lossless (`From`); narrowing is fallible
(`TryFrom`). The wide tier is backed by an in-tree hand-rolled
wide-integer type — no external big-integer dependency — and is opt-in.
See [`docs/widths.md`](docs/widths.md).

## Compile-time literals

With the `macros` feature, `d38!` writes `D38` values at compile time
with automatic scale inference:

```rust
use decimal_scaled::d38;

let price = d38!(19.99);              // D38<2>
let micro = d38!(1.234_567, scale 6); // D38<6>
let rnd   = d38!(1.235, scale 2, rounded); // 1.24 (half-to-even)
```

See [`docs/macros.md`](docs/macros.md).

---

## Features

| Feature | Default | Description |
|---|---|---|
| `std` | yes | Platform `f64`-bridge transcendentals (used when `strict` is off). Pulls in `alloc`. |
| `alloc` | yes | String formatting and parsing on `no_std`. Required. |
| `serde` | yes | `Serialize` / `Deserialize` via `serde_helpers`. |
| `strict` | **yes** | Plain transcendentals (`ln` / `sin` / …) dispatch to the integer-only, 0.5 ULP, platform-deterministic path. `no_std`-compatible. The `*_strict` and `*_fast` named entry points are both always available; this feature only chooses what plain `*` resolves to. |
| `macros` | no | The `d38!` compile-time decimal-literal macro. |
| `fast` | no | Forces plain transcendentals to dispatch to the f64 bridge instead of `*_strict`, even when `strict` is also on. The integer-only `*_strict` and f64 `*_fast` named methods stay available either way. |
| `rounding-*` | no | Five mutually-exclusive flags that change the crate-wide default `RoundingMode` at compile time. |
| `d76` / `d153` / `d307` | no | The wide decimal tiers (256 / 512 / 1024-bit storage), backed by an in-tree hand-rolled wide-integer type. |
| `wide` | no | Umbrella over `d76` + `d153` + `d307`. |
| `experimental-floats` | no | Nightly-only `f16` / `f128` entry points on the float bridge. |

See [`docs/features.md`](docs/features.md) for the full reference and
common configurations.

---

## Documentation

In-depth usage guides live in [`docs/`](docs/README.md):

- [Getting started](docs/getting-started.md) — constructing values, arithmetic, formatting, parsing.
- [The width family](docs/widths.md) — `D9` … `D307`, scale ranges, the `Decimal` trait.
- [Conversions](docs/conversions.md) — integers, floats, cross-width widening / narrowing.
- [Rounding](docs/rounding.md) — `RoundingMode`, `rescale`, the `rounding-*` features.
- [Strict mode](docs/strict-mode.md) — integer-only transcendentals.
- [The `d38!` macro](docs/macros.md) — compile-time decimal literals.
- [Cargo features](docs/features.md) — every feature flag.
- [Benchmarks](docs/benchmarks.md) — head-to-head against `bnum`, `ruint`, `rust_decimal`, and `fixed`, plus fast vs strict.

API reference: <https://docs.rs/decimal-scaled/>.

---

## License

Licensed under either of:

- MIT license ([LICENSE-MIT](LICENSE-MIT))
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))

at your option.

Copyright 2026 John Moxley.

Third-party code attributions are listed in [LICENSE-THIRD-PARTY](LICENSE-THIRD-PARTY).