decimal_scaled/types/unified.rs
1// SPDX-FileCopyrightText: 2026 John Moxley
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Unified decimal type: `D<S, const SCALE: u32>` — a generic
5//! `#[repr(transparent)]` wrapper over the storage integer `S`.
6//!
7//! # Why
8//!
9//! Each concrete decimal width in the crate (`D38`, `D57`, `D76`, …)
10//! was originally its own `#[repr(transparent)]` newtype. That worked
11//! but meant every per-width macro invocation, every method shell,
12//! every cross-width helper had to be duplicated by name. The
13//! [`D<S, SCALE>`](crate::D) type collapses that: the per-width
14//! struct definitions become type aliases over a single generic
15//! type, and methods can be implemented once per storage (generic
16//! over `SCALE`) rather than once per `(width, scale)` pair.
17//!
18//! # Storage parameterisation
19//!
20//! `S` is the storage integer. For the narrow primitive tiers
21//! `S` is `i64` (D18), `i128` (D38). For the wide tiers
22//! `S` is one of the `crate::int::types::Int{192,256,384,…,4096}`
23//! types.
24//!
25//! Methods on `D<S, SCALE>` are added per-`S` in the macros / impl
26//! blocks scattered across the crate — see `types/widths.rs`, the
27//! `macros/` directory, and `policy/`. This file only carries the
28//! struct definition and the most foundational `impl`s
29//! (`Clone` / `Copy` / `Default` derivation patterns that need
30//! tighter bounds than the derive macro provides).
31//!
32//! `Debug` is deliberately NOT a blanket impl on `D<S, SCALE>`: it is
33//! emitted per-width by `decl_decimal_display!` so the output is the
34//! canonical decimal string rather than the raw integer. A blanket
35//! `Debug` would collide with the macro-emitted impls once per-width
36//! types alias `D<…, SCALE>`.
37//!
38//! # `SCALE` parameterisation
39//!
40//! `SCALE` is the base-10 exponent: the logical value of
41//! `D::<S, SCALE>(raw)` is `raw / 10^SCALE`. Same semantics as the
42//! original per-width types.
43//!
44//! # Compatibility
45//!
46//! Existing names (`D18`, `D38`, `D57`, …, `D1232`) become
47//! type aliases of `D<…, SCALE>`. Source-compatible. The
48//! `#[repr(transparent)]` layout is preserved per storage, so the
49//! raw-bytes representation of `D38<5>` is unchanged.
50
51/// Generic scaled fixed-point decimal: storage integer `S`, base-10
52/// scale `SCALE`. The logical value is `self.0 / 10^SCALE`.
53///
54/// See the module docs for the parameterisation contract.
55#[repr(transparent)]
56pub struct D<S, const SCALE: u32>(pub S);
57
58// `Clone` / `Copy` need explicit bounds — `#[derive]` would require
59// `S: Clone + Copy` to be inferable on the struct, which isn't always
60// what we want. Hand-rolling keeps the bounds tight per-call.
61
62impl<S: Clone, const SCALE: u32> Clone for D<S, SCALE> {
63 #[inline]
64 fn clone(&self) -> Self {
65 Self(self.0.clone())
66 }
67}
68
69impl<S: Copy, const SCALE: u32> Copy for D<S, SCALE> {}
70
71// `Debug` is intentionally NOT provided here as a blanket impl. Each
72// concrete storage's `Debug` impl is emitted by the per-width display
73// macro (`decl_decimal_display!`) so the output is the canonical
74// decimal string rather than the raw integer. A blanket impl on
75// `D<S, SCALE>` would collide with those macro-emitted impls once
76// the per-width types are aliases of `D<…, SCALE>`.
77
78// Equality / ordering. The `D` type is always `Int<N>`-backed, so these
79// impls are bound to `Int` storage and delegate to the policy dispatchers
80// in `policy::dcmp` / `policy::deq`. ONE generic `PartialEq` / `PartialOrd`
81// pair, parameterised over both widths (`N`, `M`) AND both scales (`S1`,
82// `S2`), covers every `(width, scale) × (width, scale)` combination — the
83// same-type case (`N == M`, `S1 == S2`) is just one instantiation, so no
84// separate same-type impl is needed (a derived or hand-written same-type
85// comparison would collide — E0119). This 4-param impl subsumes (and
86// replaces) the earlier 3-param same-scale impl for the same coherence
87// reason.
88//
89// The `S1 == S2` branch const-folds in the policy dispatcher, so the
90// common same-scale path monomorphises to a plain cross-width compare
91// (`Int::cmp_cross`, no multiply); only `S1 != S2` reaches the cross-scale
92// comparator (`Int::cmp_cross_scaled`), oriented so the higher-scale (more
93// decimal digits) operand is the one scaled down by `10^|S1−S2|`.
94use crate::int::types::Int;
95
96impl<const N: usize, const M: usize, const S1: u32, const S2: u32> PartialEq<D<Int<M>, S2>>
97 for D<Int<N>, S1>
98{
99 #[inline]
100 fn eq(&self, other: &D<Int<M>, S2>) -> bool {
101 crate::policy::deq::deq_dispatch::<N, M, S1, S2>(self.0, other.0)
102 }
103}
104
105// `Eq` requires only `PartialEq<Self>`, provided by the generic above
106// (the `N == M`, `S1 == S2` instantiation).
107impl<const N: usize, const S: u32> Eq for D<Int<N>, S> {}
108
109impl<const N: usize, const M: usize, const S1: u32, const S2: u32> PartialOrd<D<Int<M>, S2>>
110 for D<Int<N>, S1>
111{
112 #[inline]
113 fn partial_cmp(&self, other: &D<Int<M>, S2>) -> Option<core::cmp::Ordering> {
114 Some(crate::policy::dcmp::dcmp_dispatch::<N, M, S1, S2>(self.0, other.0))
115 }
116}
117
118// Same-type total order via the policy dispatcher (same-scale fast path).
119impl<const N: usize, const S: u32> Ord for D<Int<N>, S> {
120 #[inline]
121 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
122 crate::policy::dcmp::dcmp_dispatch::<N, N, S, S>(self.0, other.0)
123 }
124}
125
126impl<S: core::hash::Hash, const SCALE: u32> core::hash::Hash for D<S, SCALE> {
127 #[inline]
128 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
129 self.0.hash(state);
130 }
131}