Skip to main content

decimal_scaled/types/
checked_transcendentals.rs

1//! `checked_*` siblings of the strict transcendental family.
2//!
3//! One generic `impl` over `(N, SCALE)` — a single source serving every
4//! width tier (D18 .. D1232), per the overflow contract in
5//! `docs/ARCHITECTURE.md` ("Overflow & domain behaviour"): the default
6//! strict form panics on a domain error or an out-of-range result; the
7//! `checked_` form returns `None` instead. The two forms run the SAME
8//! policy-dispatched kernel, so an in-range `checked_*` result is
9//! bit-identical to the default form's result.
10//!
11//! # Shape
12//!
13//! Every strict transcendental gets the pair
14//!
15//! - `checked_<fn>_strict_with(self, .., mode) -> Option<Self>`
16//! - `checked_<fn>_strict(self, ..) -> Option<Self>` — the default-mode
17//!   sibling, delegating with [`DEFAULT_ROUNDING_MODE`].
18//!
19//! Only the **strict** forms get checked siblings: the architecture's
20//! claim covers the strict transcendentals (the f64-bridge `*_fast`
21//! forms have no panic contract to opt out of — they saturate — and the
22//! `*_approx` forms trade away the strict guarantee by construction).
23//!
24//! # What `None` covers, per method class
25//!
26//! - **Total methods** (`sqrt`, `cbrt`, `sin`, `cos`, `atan`, `atan2`,
27//!   `tanh`, `asinh`, `to_radians`): the default form cannot panic — the
28//!   result is mathematically bounded well inside every tier's range at
29//!   every valid scale (each method's doc carries the bound) — so the
30//!   checked form always returns `Some`.
31//! - **Domain-checked methods** (`asin`, `acos`, `acosh`, `ln`, `log`,
32//!   `log2`, `log10`, `atanh`): `None` exactly on the inputs the default
33//!   form rejects with a domain panic.
34//! - **Range-checked methods** (`exp`, `ln`, `hypot`, …): `None` when
35//!   the correctly-rounded result does not fit the storage range — the
36//!   same single detection point whose `unwrap` is the default form's
37//!   panic (see the per-policy `checked_dispatch` primitives).
38//!
39//! A method's doc states which of these apply. Where the out-of-range
40//! seam has not yet been threaded through a kernel family, the doc says
41//! so explicitly: those methods still panic on an out-of-range result
42//! (identically to the default form — never a silent wrong value).
43//!
44//! [`DEFAULT_ROUNDING_MODE`]: crate::support::rounding::DEFAULT_ROUNDING_MODE
45
46use crate::int::types::compute_limbs::{ComputeLimbs, Limbs};
47use crate::int::types::Int;
48use crate::support::rounding::{RoundingMode, DEFAULT_ROUNDING_MODE};
49
50// `private_bounds`: the sqrt / cbrt / hypot methods carry the same
51// `Limbs<N>: ComputeLimbs` scratch bound their policy dispatchers do.
52// The trait is crate-internal plumbing; at every concrete `Dxx<S>` the
53// bound is auto-satisfied and invisible to callers, so the unnameable
54// bound never surfaces in downstream code.
55#[allow(private_bounds)]
56impl<const N: usize, const SCALE: u32> crate::D<Int<N>, SCALE> {
57    /// Raw-storage value of `1` at this scale (`10^SCALE`), the unit the
58    /// domain walls compare against.
59    #[inline]
60    fn unit_bits() -> Int<N> {
61        const { Int::<N>::TEN.pow(SCALE) }
62    }
63
64    // ── Logarithms ────────────────────────────────────────────────
65
66    /// Checked [`ln_strict_with`](crate::types::widths::D38::ln_strict_with):
67    /// natural logarithm, `None` instead of a panic.
68    ///
69    /// Returns `None` when `self <= 0` (the domain wall) or when the
70    /// correctly-rounded result does not fit the storage range (possible
71    /// only near a tier's maximum scale). Otherwise
72    /// `Some(self.ln_strict_with(mode))`, bit-identical.
73    ///
74    /// Out-of-range detection: exact on D18/D38; on the wide tiers an
75    /// out-of-range result still panics (kernel seam not yet reached).
76    /// Domain errors return `None` at every tier.
77    ///
78    /// ```
79    /// use decimal_scaled::{D38, RoundingMode};
80    /// let two = D38::<12>::try_from(2i64).unwrap();
81    /// assert_eq!(
82    ///     two.checked_ln_strict_with(RoundingMode::HalfToEven),
83    ///     Some(two.ln_strict_with(RoundingMode::HalfToEven)),
84    /// );
85    /// assert_eq!(D38::<12>::ZERO.checked_ln_strict_with(RoundingMode::HalfToEven), None);
86    /// ```
87    #[inline]
88    #[must_use]
89    pub fn checked_ln_strict_with(self, mode: RoundingMode) -> Option<Self> {
90        if self.0 <= Int::<N>::ZERO {
91            return None;
92        }
93        crate::policy::ln::checked_dispatch::<N, SCALE>(self.0, mode).map(Self)
94    }
95
96    /// Default-mode sibling of [`Self::checked_ln_strict_with`].
97    ///
98    /// ```
99    /// use decimal_scaled::D38;
100    /// assert!(D38::<12>::try_from(10i64).unwrap().checked_ln_strict().is_some());
101    /// assert_eq!(D38::<12>::try_from(-1i64).unwrap().checked_ln_strict(), None);
102    /// ```
103    #[inline]
104    #[must_use]
105    pub fn checked_ln_strict(self) -> Option<Self> {
106        self.checked_ln_strict_with(DEFAULT_ROUNDING_MODE)
107    }
108
109    /// Checked `log_strict_with`: logarithm in an arbitrary `base`,
110    /// `None` instead of a panic.
111    ///
112    /// Returns `None` when `self <= 0`, `base <= 0`, or `base == 1`
113    /// (the domain walls the default form panics on), or when the
114    /// result does not fit the storage range. Otherwise
115    /// `Some(self.log_strict_with(base, mode))`, bit-identical.
116    ///
117    /// Out-of-range detection: exact on D18/D38; on the wide tiers an
118    /// out-of-range result still panics (wide kernel-shell seam not yet
119    /// reached). Domain errors return `None` at every tier.
120    ///
121    /// ```
122    /// use decimal_scaled::{D38, RoundingMode};
123    /// let eight = D38::<10>::try_from(8i64).unwrap();
124    /// let two = D38::<10>::try_from(2i64).unwrap();
125    /// assert!(eight.checked_log_strict_with(two, RoundingMode::HalfToEven).is_some());
126    /// assert_eq!(eight.checked_log_strict_with(D38::<10>::ONE, RoundingMode::HalfToEven), None);
127    /// ```
128    #[inline]
129    #[must_use]
130    pub fn checked_log_strict_with(self, base: Self, mode: RoundingMode) -> Option<Self> {
131        if self.0 <= Int::<N>::ZERO
132            || base.0 <= Int::<N>::ZERO
133            || base.0 == Self::unit_bits()
134        {
135            return None;
136        }
137        crate::policy::log::checked_dispatch::<N, SCALE>(self.0, base.0, mode).map(Self)
138    }
139
140    /// Default-mode sibling of [`Self::checked_log_strict_with`].
141    ///
142    /// ```
143    /// use decimal_scaled::D38;
144    /// let x = D38::<10>::try_from(100i64).unwrap();
145    /// assert!(x.checked_log_strict(D38::<10>::try_from(10i64).unwrap()).is_some());
146    /// assert_eq!(x.checked_log_strict(D38::<10>::ZERO), None);
147    /// ```
148    #[inline]
149    #[must_use]
150    pub fn checked_log_strict(self, base: Self) -> Option<Self> {
151        self.checked_log_strict_with(base, DEFAULT_ROUNDING_MODE)
152    }
153
154    /// Checked `log2_strict_with`: base-2 logarithm, `None` instead of
155    /// a panic.
156    ///
157    /// Returns `None` when `self <= 0`, or when the result does not fit
158    /// the storage range. Otherwise bit-identical `Some`.
159    ///
160    /// Out-of-range detection: exact on D18/D38; on the wide tiers an
161    /// out-of-range result still panics (wide kernel-shell seam not yet reached).
162    ///
163    /// ```
164    /// use decimal_scaled::{D38, RoundingMode};
165    /// let eight = D38::<10>::try_from(8i64).unwrap();
166    /// assert_eq!(
167    ///     eight.checked_log2_strict_with(RoundingMode::HalfToEven),
168    ///     Some(eight.log2_strict_with(RoundingMode::HalfToEven)),
169    /// );
170    /// assert_eq!(D38::<10>::ZERO.checked_log2_strict_with(RoundingMode::HalfToEven), None);
171    /// ```
172    #[inline]
173    #[must_use]
174    pub fn checked_log2_strict_with(self, mode: RoundingMode) -> Option<Self> {
175        if self.0 <= Int::<N>::ZERO {
176            return None;
177        }
178        crate::policy::ln::checked_log2_dispatch::<N, SCALE>(self.0, mode).map(Self)
179    }
180
181    /// Default-mode sibling of [`Self::checked_log2_strict_with`].
182    ///
183    /// ```
184    /// use decimal_scaled::D38;
185    /// assert!(D38::<10>::try_from(4i64).unwrap().checked_log2_strict().is_some());
186    /// assert_eq!(D38::<10>::try_from(-4i64).unwrap().checked_log2_strict(), None);
187    /// ```
188    #[inline]
189    #[must_use]
190    pub fn checked_log2_strict(self) -> Option<Self> {
191        self.checked_log2_strict_with(DEFAULT_ROUNDING_MODE)
192    }
193
194    /// Checked `log10_strict_with`: base-10 logarithm, `None` instead
195    /// of a panic.
196    ///
197    /// Returns `None` when `self <= 0`, or when the result does not fit
198    /// the storage range. Otherwise bit-identical `Some`.
199    ///
200    /// Out-of-range detection: exact on D18/D38; on the wide tiers an
201    /// out-of-range result still panics (wide kernel-shell seam not yet reached).
202    ///
203    /// ```
204    /// use decimal_scaled::{D38, RoundingMode};
205    /// let hundred = D38::<10>::try_from(100i64).unwrap();
206    /// assert!(hundred.checked_log10_strict_with(RoundingMode::HalfToEven).is_some());
207    /// assert_eq!(D38::<10>::ZERO.checked_log10_strict_with(RoundingMode::HalfToEven), None);
208    /// ```
209    #[inline]
210    #[must_use]
211    pub fn checked_log10_strict_with(self, mode: RoundingMode) -> Option<Self> {
212        if self.0 <= Int::<N>::ZERO {
213            return None;
214        }
215        crate::policy::ln::checked_log10_dispatch::<N, SCALE>(self.0, mode).map(Self)
216    }
217
218    /// Default-mode sibling of [`Self::checked_log10_strict_with`].
219    ///
220    /// ```
221    /// use decimal_scaled::D38;
222    /// assert!(D38::<10>::try_from(1000i64).unwrap().checked_log10_strict().is_some());
223    /// assert_eq!(D38::<10>::ZERO.checked_log10_strict(), None);
224    /// ```
225    #[inline]
226    #[must_use]
227    pub fn checked_log10_strict(self) -> Option<Self> {
228        self.checked_log10_strict_with(DEFAULT_ROUNDING_MODE)
229    }
230
231    /// Checked `log1p_strict_with`: `ln(1 + self)`, `None` instead of a
232    /// domain panic.
233    ///
234    /// Returns `None` when `self <= -1` — the domain wall the default
235    /// form panics on (`log1p: argument must be greater than -1`).
236    /// Otherwise `Some(self.log1p_strict_with(mode))`, bit-identical.
237    ///
238    /// There is no out-of-range case: `ln(1 + t)` is bounded by the
239    /// storage range wherever `1 + t` is representable, so every
240    /// in-domain argument returns `Some` at every tier. Unlike the
241    /// logarithms above, this needs no wide-tier caveat.
242    ///
243    /// ```
244    /// use decimal_scaled::{D38, RoundingMode};
245    /// let one = D38::<12>::ONE;
246    /// assert_eq!(
247    ///     one.checked_log1p_strict_with(RoundingMode::HalfToEven),
248    ///     Some(one.log1p_strict_with(RoundingMode::HalfToEven)),
249    /// );
250    /// // `t = -1` is the wall — `ln(0)` is undefined.
251    /// assert_eq!(
252    ///     D38::<12>::try_from(-1i64).unwrap().checked_log1p_strict_with(RoundingMode::HalfToEven),
253    ///     None,
254    /// );
255    /// ```
256    #[inline]
257    #[must_use]
258    pub fn checked_log1p_strict_with(self, mode: RoundingMode) -> Option<Self> {
259        if self.0 <= -Self::unit_bits() {
260            return None;
261        }
262        Some(Self(crate::policy::log1p::dispatch::<N, SCALE>(
263            self.0, mode,
264        )))
265    }
266
267    /// Default-mode sibling of [`Self::checked_log1p_strict_with`].
268    ///
269    /// ```
270    /// use decimal_scaled::D38;
271    /// assert!(D38::<12>::ONE.checked_log1p_strict().is_some());
272    /// assert_eq!(D38::<12>::try_from(-1i64).unwrap().checked_log1p_strict(), None);
273    /// assert_eq!(D38::<12>::try_from(-2i64).unwrap().checked_log1p_strict(), None);
274    /// ```
275    #[inline]
276    #[must_use]
277    pub fn checked_log1p_strict(self) -> Option<Self> {
278        self.checked_log1p_strict_with(DEFAULT_ROUNDING_MODE)
279    }
280
281    // ── Exponentials ──────────────────────────────────────────────
282
283    /// Checked `exp_strict_with`: `e^self`, `None` instead of a panic.
284    ///
285    /// `exp` has no domain wall; `None` means the correctly-rounded
286    /// result does not fit the storage range — the same condition on
287    /// which the default form panics. Otherwise
288    /// `Some(self.exp_strict_with(mode))`, bit-identical.
289    ///
290    /// Out-of-range detection: exact on D18/D38; on the wide tiers an
291    /// out-of-range result still panics (kernel seam not yet reached).
292    ///
293    /// ```
294    /// use decimal_scaled::{D38, RoundingMode};
295    /// let one = D38::<12>::ONE;
296    /// assert_eq!(
297    ///     one.checked_exp_strict_with(RoundingMode::HalfToEven),
298    ///     Some(one.exp_strict_with(RoundingMode::HalfToEven)),
299    /// );
300    /// // e^120 has 53 integer digits — far outside D38's 38.
301    /// assert_eq!(D38::<12>::try_from(120i64).unwrap().checked_exp_strict_with(RoundingMode::HalfToEven), None);
302    /// ```
303    #[inline]
304    #[must_use]
305    pub fn checked_exp_strict_with(self, mode: RoundingMode) -> Option<Self> {
306        crate::policy::exp::checked_dispatch::<N, SCALE>(self.0, mode).map(Self)
307    }
308
309    /// Default-mode sibling of [`Self::checked_exp_strict_with`].
310    ///
311    /// ```
312    /// use decimal_scaled::D38;
313    /// assert!(D38::<12>::ONE.checked_exp_strict().is_some());
314    /// assert_eq!(D38::<12>::try_from(120i64).unwrap().checked_exp_strict(), None);
315    /// ```
316    #[inline]
317    #[must_use]
318    pub fn checked_exp_strict(self) -> Option<Self> {
319        self.checked_exp_strict_with(DEFAULT_ROUNDING_MODE)
320    }
321
322    /// Checked `exp2_strict_with`: `2^self`, `None` instead of a panic.
323    ///
324    /// No domain wall; `None` means the result does not fit the storage
325    /// range. Otherwise bit-identical `Some`.
326    ///
327    /// Out-of-range detection: exact on D18/D38; on the wide tiers an
328    /// out-of-range result still panics (wide kernel-shell seam not yet reached).
329    ///
330    /// ```
331    /// use decimal_scaled::{D38, RoundingMode};
332    /// let ten = D38::<12>::try_from(10i64).unwrap();
333    /// assert_eq!(
334    ///     ten.checked_exp2_strict_with(RoundingMode::HalfToEven),
335    ///     Some(ten.exp2_strict_with(RoundingMode::HalfToEven)),
336    /// );
337    /// // 2^95 has 29 integer digits — outside D38<12>'s 26.
338    /// assert_eq!(D38::<12>::try_from(95i64).unwrap().checked_exp2_strict_with(RoundingMode::HalfToEven), None);
339    /// ```
340    #[inline]
341    #[must_use]
342    pub fn checked_exp2_strict_with(self, mode: RoundingMode) -> Option<Self> {
343        crate::policy::exp::checked_exp2_dispatch::<N, SCALE>(self.0, mode).map(Self)
344    }
345
346    /// Default-mode sibling of [`Self::checked_exp2_strict_with`].
347    ///
348    /// ```
349    /// use decimal_scaled::D38;
350    /// assert!(D38::<12>::try_from(10i64).unwrap().checked_exp2_strict().is_some());
351    /// assert_eq!(D38::<12>::try_from(95i64).unwrap().checked_exp2_strict(), None);
352    /// ```
353    #[inline]
354    #[must_use]
355    pub fn checked_exp2_strict(self) -> Option<Self> {
356        self.checked_exp2_strict_with(DEFAULT_ROUNDING_MODE)
357    }
358
359    /// Checked `expm1_strict_with`: `e^self − 1`, `None` instead of a
360    /// panic.
361    ///
362    /// `expm1` is total over its argument, so there is no domain wall
363    /// and `None` is reserved for a result that does not fit the storage
364    /// range — the same condition on which the default form panics.
365    /// Otherwise `Some(self.expm1_strict_with(mode))`, bit-identical.
366    ///
367    /// Out-of-range detection: the kernel seam that reports overflow as
368    /// an `Option` has not been threaded through `expm1`, so at every
369    /// tier an out-of-range result still PANICS rather than returning
370    /// `None`, and this form is `Some` whenever the default form
371    /// returns at all. That is the same gap the wide-tier note on
372    /// [`Self::checked_exp_strict_with`] records — `exp` has the seam on
373    /// D18/D38, `expm1` does not have it yet — so this pair completes
374    /// the documented surface rather than adding a stronger guarantee.
375    ///
376    /// ```
377    /// use decimal_scaled::{D38, RoundingMode};
378    /// let one = D38::<12>::ONE;
379    /// assert_eq!(
380    ///     one.checked_expm1_strict_with(RoundingMode::HalfToEven),
381    ///     Some(one.expm1_strict_with(RoundingMode::HalfToEven)),
382    /// );
383    /// ```
384    #[inline]
385    #[must_use]
386    pub fn checked_expm1_strict_with(self, mode: RoundingMode) -> Option<Self> {
387        Some(Self(crate::policy::expm1::dispatch::<N, SCALE>(
388            self.0, mode,
389        )))
390    }
391
392    /// Default-mode sibling of [`Self::checked_expm1_strict_with`].
393    ///
394    /// ```
395    /// use decimal_scaled::D38;
396    /// assert!(D38::<12>::ONE.checked_expm1_strict().is_some());
397    /// assert!(D38::<12>::try_from(-1i64).unwrap().checked_expm1_strict().is_some());
398    /// ```
399    #[inline]
400    #[must_use]
401    pub fn checked_expm1_strict(self) -> Option<Self> {
402        self.checked_expm1_strict_with(DEFAULT_ROUNDING_MODE)
403    }
404
405    // ── Power ─────────────────────────────────────────────────────
406
407    /// Checked `powf_strict_with`: `self^exp`, `None` instead of a
408    /// panic.
409    ///
410    /// `powf` has no domain panic: a non-positive base saturates to
411    /// zero (the kernel's documented behaviour at every tier), so
412    /// `checked_powf` returns `Some(ZERO)` there, matching the default
413    /// form. `None` means the result does not fit the storage range.
414    ///
415    /// Out-of-range detection: exact on D18/D38; on the wide tiers an
416    /// out-of-range result still panics (wide kernel-shell seam not yet reached).
417    ///
418    /// ```
419    /// use decimal_scaled::{D38, RoundingMode};
420    /// let three = D38::<10>::try_from(3i64).unwrap();
421    /// let two = D38::<10>::try_from(2i64).unwrap();
422    /// assert_eq!(
423    ///     three.checked_powf_strict_with(two, RoundingMode::HalfToEven),
424    ///     Some(three.powf_strict_with(two, RoundingMode::HalfToEven)),
425    /// );
426    /// // Non-positive base saturates to zero, as the default form does.
427    /// let half = D38::<10>::ONE / two;
428    /// assert_eq!(
429    ///     (-three).checked_powf_strict_with(half, RoundingMode::HalfToEven),
430    ///     Some(D38::<10>::ZERO),
431    /// );
432    /// // 10^30 has 31 integer digits — out of D38<10>'s 28.
433    /// let ten = D38::<10>::try_from(10i64).unwrap();
434    /// let thirty = D38::<10>::try_from(30i64).unwrap();
435    /// assert_eq!(ten.checked_powf_strict_with(thirty, RoundingMode::HalfToEven), None);
436    /// ```
437    #[inline]
438    #[must_use]
439    pub fn checked_powf_strict_with(self, exp: Self, mode: RoundingMode) -> Option<Self> {
440        crate::policy::pow::checked_dispatch::<N, SCALE>(self.0, exp.0, mode).map(Self)
441    }
442
443    /// Default-mode sibling of [`Self::checked_powf_strict_with`].
444    ///
445    /// ```
446    /// use decimal_scaled::D38;
447    /// let x = D38::<10>::try_from(2i64).unwrap();
448    /// assert!(x.checked_powf_strict(D38::<10>::try_from(8i64).unwrap()).is_some());
449    /// ```
450    #[inline]
451    #[must_use]
452    pub fn checked_powf_strict(self, exp: Self) -> Option<Self> {
453        self.checked_powf_strict_with(exp, DEFAULT_ROUNDING_MODE)
454    }
455
456    // ── Roots ─────────────────────────────────────────────────────
457
458    /// Checked `sqrt_strict_with`. Always `Some`: the strict square
459    /// root is total — negative inputs saturate to zero (the policy's
460    /// documented behaviour, not a panic), and the result `√v ≤
461    /// max(v, 1)` always fits the storage range. The checked form
462    /// exists for surface uniformity.
463    ///
464    /// ```
465    /// use decimal_scaled::{D38, RoundingMode};
466    /// let nine = D38::<10>::try_from(9i64).unwrap();
467    /// assert_eq!(
468    ///     nine.checked_sqrt_strict_with(RoundingMode::HalfToEven),
469    ///     Some(nine.sqrt_strict_with(RoundingMode::HalfToEven)),
470    /// );
471    /// ```
472    #[inline]
473    #[must_use]
474    pub fn checked_sqrt_strict_with(self, mode: RoundingMode) -> Option<Self>
475    where
476        Limbs<N>: ComputeLimbs,
477    {
478        Some(Self(crate::policy::sqrt::dispatch::<N, SCALE>(self.0, mode)))
479    }
480
481    /// Default-mode sibling of [`Self::checked_sqrt_strict_with`].
482    ///
483    /// ```
484    /// use decimal_scaled::D38;
485    /// assert!(D38::<10>::try_from(2i64).unwrap().checked_sqrt_strict().is_some());
486    /// ```
487    #[inline]
488    #[must_use]
489    pub fn checked_sqrt_strict(self) -> Option<Self>
490    where
491        Limbs<N>: ComputeLimbs,
492    {
493        self.checked_sqrt_strict_with(DEFAULT_ROUNDING_MODE)
494    }
495
496    /// Checked `cbrt_strict_with`. Always `Some`: the cube root is
497    /// total over the signed domain and `∛v` never exceeds `max(|v|,
498    /// 1)`, so it always fits the storage range.
499    ///
500    /// ```
501    /// use decimal_scaled::{D38, RoundingMode};
502    /// let x = D38::<10>::try_from(-27i64).unwrap();
503    /// assert_eq!(
504    ///     x.checked_cbrt_strict_with(RoundingMode::HalfToEven),
505    ///     Some(x.cbrt_strict_with(RoundingMode::HalfToEven)),
506    /// );
507    /// ```
508    #[inline]
509    #[must_use]
510    pub fn checked_cbrt_strict_with(self, mode: RoundingMode) -> Option<Self>
511    where
512        Limbs<N>: ComputeLimbs,
513    {
514        Some(Self(crate::policy::cbrt::dispatch::<N, SCALE>(self.0, mode)))
515    }
516
517    /// Default-mode sibling of [`Self::checked_cbrt_strict_with`].
518    ///
519    /// ```
520    /// use decimal_scaled::D38;
521    /// assert!(D38::<10>::try_from(8i64).unwrap().checked_cbrt_strict().is_some());
522    /// ```
523    #[inline]
524    #[must_use]
525    pub fn checked_cbrt_strict(self) -> Option<Self>
526    where
527        Limbs<N>: ComputeLimbs,
528    {
529        self.checked_cbrt_strict_with(DEFAULT_ROUNDING_MODE)
530    }
531
532    /// Checked `hypot_strict_with`: `√(self² + other²)`, `None` instead
533    /// of a panic.
534    ///
535    /// No domain wall; `None` means the result does not fit the storage
536    /// range (possible only when both operands are near the range
537    /// limit). Otherwise bit-identical `Some`.
538    ///
539    /// ```
540    /// use decimal_scaled::{D38, RoundingMode};
541    /// let three = D38::<10>::try_from(3i64).unwrap();
542    /// let four = D38::<10>::try_from(4i64).unwrap();
543    /// assert_eq!(
544    ///     three.checked_hypot_strict_with(four, RoundingMode::HalfToEven),
545    ///     Some(three.hypot_strict_with(four, RoundingMode::HalfToEven)),
546    /// );
547    /// assert_eq!(D38::<10>::MAX.checked_hypot_strict_with(D38::<10>::MAX, RoundingMode::HalfToEven), None);
548    /// ```
549    #[inline]
550    #[must_use]
551    pub fn checked_hypot_strict_with(self, other: Self, mode: RoundingMode) -> Option<Self>
552    where
553        Limbs<N>: ComputeLimbs,
554    {
555        crate::policy::hypot::checked_dispatch::<N, SCALE>(self.0, other.0, mode).map(Self)
556    }
557
558    /// Default-mode sibling of [`Self::checked_hypot_strict_with`].
559    ///
560    /// ```
561    /// use decimal_scaled::D38;
562    /// let a = D38::<10>::try_from(5i64).unwrap();
563    /// assert!(a.checked_hypot_strict(a).is_some());
564    /// ```
565    #[inline]
566    #[must_use]
567    pub fn checked_hypot_strict(self, other: Self) -> Option<Self>
568    where
569        Limbs<N>: ComputeLimbs,
570    {
571        self.checked_hypot_strict_with(other, DEFAULT_ROUNDING_MODE)
572    }
573
574    // ── Trigonometry (forward) ────────────────────────────────────
575
576    /// Checked `sin_strict_with`. Always `Some`: `sin` is total and
577    /// `|sin x| <= 1`, which fits every tier's range at every valid
578    /// scale (each tier keeps >= ~10 of integer headroom at its
579    /// maximum scale).
580    ///
581    /// ```
582    /// use decimal_scaled::{D38, RoundingMode};
583    /// let one = D38::<12>::ONE;
584    /// assert_eq!(
585    ///     one.checked_sin_strict_with(RoundingMode::HalfToEven),
586    ///     Some(one.sin_strict_with(RoundingMode::HalfToEven)),
587    /// );
588    /// ```
589    #[inline]
590    #[must_use]
591    pub fn checked_sin_strict_with(self, mode: RoundingMode) -> Option<Self> {
592        Some(Self(crate::policy::trig::sin_dispatch::<N, SCALE>(self.0, mode)))
593    }
594
595    /// Default-mode sibling of [`Self::checked_sin_strict_with`].
596    ///
597    /// ```
598    /// use decimal_scaled::D38;
599    /// assert!(D38::<12>::ONE.checked_sin_strict().is_some());
600    /// ```
601    #[inline]
602    #[must_use]
603    pub fn checked_sin_strict(self) -> Option<Self> {
604        self.checked_sin_strict_with(DEFAULT_ROUNDING_MODE)
605    }
606
607    /// Checked `cos_strict_with`. Always `Some`: `cos` is total and
608    /// `|cos x| <= 1` fits every tier's range at every valid scale.
609    ///
610    /// ```
611    /// use decimal_scaled::{D38, RoundingMode};
612    /// let one = D38::<12>::ONE;
613    /// assert_eq!(
614    ///     one.checked_cos_strict_with(RoundingMode::HalfToEven),
615    ///     Some(one.cos_strict_with(RoundingMode::HalfToEven)),
616    /// );
617    /// ```
618    #[inline]
619    #[must_use]
620    pub fn checked_cos_strict_with(self, mode: RoundingMode) -> Option<Self> {
621        Some(Self(crate::policy::trig::cos_dispatch::<N, SCALE>(self.0, mode)))
622    }
623
624    /// Default-mode sibling of [`Self::checked_cos_strict_with`].
625    ///
626    /// ```
627    /// use decimal_scaled::D38;
628    /// assert!(D38::<12>::ONE.checked_cos_strict().is_some());
629    /// ```
630    #[inline]
631    #[must_use]
632    pub fn checked_cos_strict(self) -> Option<Self> {
633        self.checked_cos_strict_with(DEFAULT_ROUNDING_MODE)
634    }
635
636    /// Checked `tan_strict_with`: `None` instead of a panic.
637    ///
638    /// The default form panics when the argument's cosine rounds to
639    /// zero at the working precision (an odd multiple of π/2 to within
640    /// the kernel's resolution) and when the result does not fit the
641    /// storage range (near those asymptotes). Both conditions are
642    /// detected inside the kernels at every tier; this checked form
643    /// currently panics on them identically to the default form
644    /// (kernel seam not yet reached). For every other input
645    /// it returns bit-identical `Some`.
646    ///
647    /// ```
648    /// use decimal_scaled::{D38, RoundingMode};
649    /// let one = D38::<12>::ONE;
650    /// assert_eq!(
651    ///     one.checked_tan_strict_with(RoundingMode::HalfToEven),
652    ///     Some(one.tan_strict_with(RoundingMode::HalfToEven)),
653    /// );
654    /// ```
655    #[inline]
656    #[must_use]
657    pub fn checked_tan_strict_with(self, mode: RoundingMode) -> Option<Self> {
658        Some(Self(crate::policy::trig::tan_dispatch::<N, SCALE>(self.0, mode)))
659    }
660
661    /// Default-mode sibling of [`Self::checked_tan_strict_with`].
662    ///
663    /// ```
664    /// use decimal_scaled::D38;
665    /// assert!(D38::<12>::ONE.checked_tan_strict().is_some());
666    /// ```
667    #[inline]
668    #[must_use]
669    pub fn checked_tan_strict(self) -> Option<Self> {
670        self.checked_tan_strict_with(DEFAULT_ROUNDING_MODE)
671    }
672
673    // ── Trigonometry (inverse) ────────────────────────────────────
674
675    /// Checked `asin_strict_with`: `None` instead of a domain panic.
676    ///
677    /// Returns `None` when `|self| > 1` (the default form's domain
678    /// wall). The result `|asin x| <= π/2` always fits the storage
679    /// range, so there is no out-of-range case. Otherwise bit-identical
680    /// `Some`.
681    ///
682    /// ```
683    /// use decimal_scaled::{D38, RoundingMode};
684    /// let half = D38::<12>::ONE / D38::<12>::try_from(2i64).unwrap();
685    /// assert_eq!(
686    ///     half.checked_asin_strict_with(RoundingMode::HalfToEven),
687    ///     Some(half.asin_strict_with(RoundingMode::HalfToEven)),
688    /// );
689    /// assert_eq!(D38::<12>::try_from(2i64).unwrap().checked_asin_strict_with(RoundingMode::HalfToEven), None);
690    /// ```
691    #[inline]
692    #[must_use]
693    pub fn checked_asin_strict_with(self, mode: RoundingMode) -> Option<Self> {
694        let one = Self::unit_bits();
695        if self.0 > one || self.0 < -one {
696            return None;
697        }
698        crate::policy::trig::checked_asin_dispatch::<N, SCALE>(self.0, mode).map(Self)
699    }
700
701    /// Default-mode sibling of [`Self::checked_asin_strict_with`].
702    ///
703    /// ```
704    /// use decimal_scaled::D38;
705    /// assert!(D38::<12>::ONE.checked_asin_strict().is_some());
706    /// assert_eq!(D38::<12>::try_from(-2i64).unwrap().checked_asin_strict(), None);
707    /// ```
708    #[inline]
709    #[must_use]
710    pub fn checked_asin_strict(self) -> Option<Self> {
711        self.checked_asin_strict_with(DEFAULT_ROUNDING_MODE)
712    }
713
714    /// Checked `acos_strict_with`: `None` instead of a domain panic.
715    ///
716    /// Returns `None` when `|self| > 1`. The result `0 <= acos x <= π`
717    /// always fits the storage range, so there is no out-of-range case.
718    /// Otherwise bit-identical `Some`.
719    ///
720    /// ```
721    /// use decimal_scaled::{D38, RoundingMode};
722    /// let half = D38::<12>::ONE / D38::<12>::try_from(2i64).unwrap();
723    /// assert_eq!(
724    ///     half.checked_acos_strict_with(RoundingMode::HalfToEven),
725    ///     Some(half.acos_strict_with(RoundingMode::HalfToEven)),
726    /// );
727    /// assert_eq!(D38::<12>::try_from(2i64).unwrap().checked_acos_strict_with(RoundingMode::HalfToEven), None);
728    /// ```
729    #[inline]
730    #[must_use]
731    pub fn checked_acos_strict_with(self, mode: RoundingMode) -> Option<Self> {
732        let one = Self::unit_bits();
733        if self.0 > one || self.0 < -one {
734            return None;
735        }
736        crate::policy::trig::checked_acos_dispatch::<N, SCALE>(self.0, mode).map(Self)
737    }
738
739    /// Default-mode sibling of [`Self::checked_acos_strict_with`].
740    ///
741    /// ```
742    /// use decimal_scaled::D38;
743    /// assert!(D38::<12>::ONE.checked_acos_strict().is_some());
744    /// assert_eq!(D38::<12>::try_from(2i64).unwrap().checked_acos_strict(), None);
745    /// ```
746    #[inline]
747    #[must_use]
748    pub fn checked_acos_strict(self) -> Option<Self> {
749        self.checked_acos_strict_with(DEFAULT_ROUNDING_MODE)
750    }
751
752    /// Checked `atan_strict_with`. Always `Some`: `atan` is total and
753    /// `|atan x| < π/2` fits every tier's range at every valid scale.
754    ///
755    /// ```
756    /// use decimal_scaled::{D38, RoundingMode};
757    /// let x = D38::<12>::try_from(5i64).unwrap();
758    /// assert_eq!(
759    ///     x.checked_atan_strict_with(RoundingMode::HalfToEven),
760    ///     Some(x.atan_strict_with(RoundingMode::HalfToEven)),
761    /// );
762    /// ```
763    #[inline]
764    #[must_use]
765    pub fn checked_atan_strict_with(self, mode: RoundingMode) -> Option<Self> {
766        Some(Self(crate::policy::trig::atan_dispatch::<N, SCALE>(self.0, mode)))
767    }
768
769    /// Default-mode sibling of [`Self::checked_atan_strict_with`].
770    ///
771    /// ```
772    /// use decimal_scaled::D38;
773    /// assert!(D38::<12>::try_from(3i64).unwrap().checked_atan_strict().is_some());
774    /// ```
775    #[inline]
776    #[must_use]
777    pub fn checked_atan_strict(self) -> Option<Self> {
778        self.checked_atan_strict_with(DEFAULT_ROUNDING_MODE)
779    }
780
781    /// Checked `atan2_strict_with`. Always `Some`: `atan2` is total
782    /// (including the `(0, 0)` origin, which yields `0`) and `|atan2(y,
783    /// x)| <= π` fits every tier's range at every valid scale.
784    ///
785    /// ```
786    /// use decimal_scaled::{D38, RoundingMode};
787    /// let y = D38::<12>::ONE;
788    /// let x = D38::<12>::try_from(2i64).unwrap();
789    /// assert_eq!(
790    ///     y.checked_atan2_strict_with(x, RoundingMode::HalfToEven),
791    ///     Some(y.atan2_strict_with(x, RoundingMode::HalfToEven)),
792    /// );
793    /// ```
794    #[inline]
795    #[must_use]
796    pub fn checked_atan2_strict_with(self, other: Self, mode: RoundingMode) -> Option<Self> {
797        crate::policy::trig::checked_atan2_dispatch::<N, SCALE>(self.0, other.0, mode)
798            .map(Self)
799    }
800
801    /// Default-mode sibling of [`Self::checked_atan2_strict_with`].
802    ///
803    /// ```
804    /// use decimal_scaled::D38;
805    /// assert!(D38::<12>::ONE.checked_atan2_strict(D38::<12>::ONE).is_some());
806    /// ```
807    #[inline]
808    #[must_use]
809    pub fn checked_atan2_strict(self, other: Self) -> Option<Self> {
810        self.checked_atan2_strict_with(other, DEFAULT_ROUNDING_MODE)
811    }
812
813    // ── Hyperbolics ───────────────────────────────────────────────
814
815    /// Checked `sinh_strict_with`: `None` instead of a panic.
816    ///
817    /// `sinh` has no domain wall; `None` means the result does not fit
818    /// the storage range (it grows like `e^|x|/2`). Otherwise
819    /// bit-identical `Some`.
820    ///
821    /// Out-of-range detection: exact on D18 (a result that fits the
822    /// D38 work width but not D18 storage is `None`); detection deeper
823    /// in the kernels (D38 and the wide tiers) still panics (kernel seam not yet reached).
824    ///
825    /// ```
826    /// use decimal_scaled::{D18, D38, RoundingMode};
827    /// let one = D38::<12>::ONE;
828    /// assert_eq!(
829    ///     one.checked_sinh_strict_with(RoundingMode::HalfToEven),
830    ///     Some(one.sinh_strict_with(RoundingMode::HalfToEven)),
831    /// );
832    /// // sinh(40) ~ 1.2e17 exceeds D18<6>'s range but fits the D38 work width.
833    /// assert_eq!(D18::<6>::try_from(40).unwrap().checked_sinh_strict_with(RoundingMode::HalfToEven), None);
834    /// ```
835    #[inline]
836    #[must_use]
837    pub fn checked_sinh_strict_with(self, mode: RoundingMode) -> Option<Self> {
838        crate::policy::trig::checked_sinh_dispatch::<N, SCALE>(self.0, mode)
839            .map(Self)
840    }
841
842    /// Default-mode sibling of [`Self::checked_sinh_strict_with`].
843    ///
844    /// ```
845    /// use decimal_scaled::{D18, D38};
846    /// assert!(D38::<12>::ONE.checked_sinh_strict().is_some());
847    /// assert_eq!(D18::<6>::try_from(40).unwrap().checked_sinh_strict(), None);
848    /// ```
849    #[inline]
850    #[must_use]
851    pub fn checked_sinh_strict(self) -> Option<Self> {
852        self.checked_sinh_strict_with(DEFAULT_ROUNDING_MODE)
853    }
854
855    /// Checked `cosh_strict_with`: `None` instead of a panic.
856    ///
857    /// `cosh` has no domain wall; `None` means the result does not fit
858    /// the storage range. Otherwise bit-identical `Some`.
859    ///
860    /// Out-of-range detection: exact on D18 (a result that fits the
861    /// D38 work width but not D18 storage is `None`); detection deeper
862    /// in the kernels (D38 and the wide tiers) still panics (kernel seam not yet reached).
863    ///
864    /// ```
865    /// use decimal_scaled::{D18, D38, RoundingMode};
866    /// let one = D38::<12>::ONE;
867    /// assert_eq!(
868    ///     one.checked_cosh_strict_with(RoundingMode::HalfToEven),
869    ///     Some(one.cosh_strict_with(RoundingMode::HalfToEven)),
870    /// );
871    /// // cosh(40) ~ 1.2e17 exceeds D18<6>'s range but fits the D38 work width.
872    /// assert_eq!(D18::<6>::try_from(40).unwrap().checked_cosh_strict_with(RoundingMode::HalfToEven), None);
873    /// ```
874    #[inline]
875    #[must_use]
876    pub fn checked_cosh_strict_with(self, mode: RoundingMode) -> Option<Self> {
877        crate::policy::trig::checked_cosh_dispatch::<N, SCALE>(self.0, mode)
878            .map(Self)
879    }
880
881    /// Default-mode sibling of [`Self::checked_cosh_strict_with`].
882    ///
883    /// ```
884    /// use decimal_scaled::{D18, D38};
885    /// assert!(D38::<12>::ONE.checked_cosh_strict().is_some());
886    /// assert_eq!(D18::<6>::try_from(40).unwrap().checked_cosh_strict(), None);
887    /// ```
888    #[inline]
889    #[must_use]
890    pub fn checked_cosh_strict(self) -> Option<Self> {
891        self.checked_cosh_strict_with(DEFAULT_ROUNDING_MODE)
892    }
893
894    /// Checked `tanh_strict_with`. Always `Some`: `tanh` is total and
895    /// `|tanh x| <= 1` fits every tier's range at every valid scale.
896    ///
897    /// ```
898    /// use decimal_scaled::{D38, RoundingMode};
899    /// let one = D38::<12>::ONE;
900    /// assert_eq!(
901    ///     one.checked_tanh_strict_with(RoundingMode::HalfToEven),
902    ///     Some(one.tanh_strict_with(RoundingMode::HalfToEven)),
903    /// );
904    /// ```
905    #[inline]
906    #[must_use]
907    pub fn checked_tanh_strict_with(self, mode: RoundingMode) -> Option<Self> {
908        crate::policy::trig::checked_tanh_dispatch::<N, SCALE>(self.0, mode).map(Self)
909    }
910
911    /// Default-mode sibling of [`Self::checked_tanh_strict_with`].
912    ///
913    /// ```
914    /// use decimal_scaled::D38;
915    /// assert!(D38::<12>::ONE.checked_tanh_strict().is_some());
916    /// ```
917    #[inline]
918    #[must_use]
919    pub fn checked_tanh_strict(self) -> Option<Self> {
920        self.checked_tanh_strict_with(DEFAULT_ROUNDING_MODE)
921    }
922
923    /// Checked `asinh_strict_with`. Always `Some`: `asinh` is total and
924    /// `|asinh x| <= max(|x|, 1)` always fits the storage range when
925    /// `x` does.
926    ///
927    /// ```
928    /// use decimal_scaled::{D38, RoundingMode};
929    /// let x = D38::<12>::try_from(3i64).unwrap();
930    /// assert_eq!(
931    ///     x.checked_asinh_strict_with(RoundingMode::HalfToEven),
932    ///     Some(x.asinh_strict_with(RoundingMode::HalfToEven)),
933    /// );
934    /// ```
935    #[inline]
936    #[must_use]
937    pub fn checked_asinh_strict_with(self, mode: RoundingMode) -> Option<Self> {
938        crate::policy::trig::checked_asinh_dispatch::<N, SCALE>(self.0, mode).map(Self)
939    }
940
941    /// Default-mode sibling of [`Self::checked_asinh_strict_with`].
942    ///
943    /// ```
944    /// use decimal_scaled::D38;
945    /// assert!(D38::<12>::try_from(2i64).unwrap().checked_asinh_strict().is_some());
946    /// ```
947    #[inline]
948    #[must_use]
949    pub fn checked_asinh_strict(self) -> Option<Self> {
950        self.checked_asinh_strict_with(DEFAULT_ROUNDING_MODE)
951    }
952
953    /// Checked `acosh_strict_with`: `None` instead of a domain panic.
954    ///
955    /// Returns `None` when `self < 1` (the default form's domain wall).
956    /// The result `acosh x < ln(2x) <= x` always fits the storage range
957    /// when `x` does. Otherwise bit-identical `Some`.
958    ///
959    /// ```
960    /// use decimal_scaled::{D38, RoundingMode};
961    /// let two = D38::<12>::try_from(2i64).unwrap();
962    /// assert_eq!(
963    ///     two.checked_acosh_strict_with(RoundingMode::HalfToEven),
964    ///     Some(two.acosh_strict_with(RoundingMode::HalfToEven)),
965    /// );
966    /// assert_eq!(D38::<12>::ZERO.checked_acosh_strict_with(RoundingMode::HalfToEven), None);
967    /// ```
968    #[inline]
969    #[must_use]
970    pub fn checked_acosh_strict_with(self, mode: RoundingMode) -> Option<Self> {
971        if self.0 < Self::unit_bits() {
972            return None;
973        }
974        crate::policy::trig::checked_acosh_dispatch::<N, SCALE>(self.0, mode).map(Self)
975    }
976
977    /// Default-mode sibling of [`Self::checked_acosh_strict_with`].
978    ///
979    /// ```
980    /// use decimal_scaled::D38;
981    /// assert!(D38::<12>::try_from(3i64).unwrap().checked_acosh_strict().is_some());
982    /// assert_eq!(D38::<12>::ZERO.checked_acosh_strict(), None);
983    /// ```
984    #[inline]
985    #[must_use]
986    pub fn checked_acosh_strict(self) -> Option<Self> {
987        self.checked_acosh_strict_with(DEFAULT_ROUNDING_MODE)
988    }
989
990    /// Checked `atanh_strict_with`: `None` instead of a panic.
991    ///
992    /// Returns `None` when `|self| >= 1` (the default form's domain
993    /// wall — `atanh` diverges at ±1). An out-of-range result (the
994    /// logarithmic blow-up just inside ±1 at a near-maximum scale) is
995    /// `None` on D18 when it fits the D38 work width; detection deeper
996    /// in the kernels still panics, identically to the default form
997    /// (kernel seam not yet reached). Otherwise bit-identical
998    /// `Some`.
999    ///
1000    /// ```
1001    /// use decimal_scaled::{D38, RoundingMode};
1002    /// let half = D38::<12>::ONE / D38::<12>::try_from(2i64).unwrap();
1003    /// assert_eq!(
1004    ///     half.checked_atanh_strict_with(RoundingMode::HalfToEven),
1005    ///     Some(half.atanh_strict_with(RoundingMode::HalfToEven)),
1006    /// );
1007    /// assert_eq!(D38::<12>::ONE.checked_atanh_strict_with(RoundingMode::HalfToEven), None);
1008    /// ```
1009    #[inline]
1010    #[must_use]
1011    pub fn checked_atanh_strict_with(self, mode: RoundingMode) -> Option<Self> {
1012        let one = Self::unit_bits();
1013        if self.0 >= one || self.0 <= -one {
1014            return None;
1015        }
1016        crate::policy::trig::checked_atanh_dispatch::<N, SCALE>(self.0, mode)
1017            .map(Self)
1018    }
1019
1020    /// Default-mode sibling of [`Self::checked_atanh_strict_with`].
1021    ///
1022    /// ```
1023    /// use decimal_scaled::D38;
1024    /// let half = D38::<12>::ONE / D38::<12>::try_from(2i64).unwrap();
1025    /// assert!(half.checked_atanh_strict().is_some());
1026    /// assert_eq!(D38::<12>::ONE.checked_atanh_strict(), None);
1027    /// ```
1028    #[inline]
1029    #[must_use]
1030    pub fn checked_atanh_strict(self) -> Option<Self> {
1031        self.checked_atanh_strict_with(DEFAULT_ROUNDING_MODE)
1032    }
1033
1034    // ── Angle conversion ──────────────────────────────────────────
1035
1036    /// Checked `to_degrees_strict_with`: `None` instead of a panic.
1037    ///
1038    /// No domain wall; `None` means `self · (180/π)` does not fit the
1039    /// storage range (the result is ~57.3× the input). Otherwise
1040    /// bit-identical `Some`.
1041    ///
1042    /// Out-of-range detection: exact on D18 (a result that fits the
1043    /// D38 work width but not D18 storage is `None`); detection deeper
1044    /// in the kernels (D38 and the wide tiers) still panics (kernel seam not yet reached).
1045    ///
1046    /// ```
1047    /// use decimal_scaled::{D18, D38, RoundingMode};
1048    /// let one = D38::<12>::ONE;
1049    /// assert_eq!(
1050    ///     one.checked_to_degrees_strict_with(RoundingMode::HalfToEven),
1051    ///     Some(one.to_degrees_strict_with(RoundingMode::HalfToEven)),
1052    /// );
1053    /// // MAX·(180/π) leaves D18's range but fits the D38 work width.
1054    /// assert_eq!(D18::<6>::MAX.checked_to_degrees_strict_with(RoundingMode::HalfToEven), None);
1055    /// ```
1056    #[inline]
1057    #[must_use]
1058    pub fn checked_to_degrees_strict_with(self, mode: RoundingMode) -> Option<Self> {
1059        crate::policy::trig::checked_to_degrees_dispatch::<N, SCALE>(self.0, mode)
1060            .map(Self)
1061    }
1062
1063    /// Default-mode sibling of [`Self::checked_to_degrees_strict_with`].
1064    ///
1065    /// ```
1066    /// use decimal_scaled::{D18, D38};
1067    /// assert!(D38::<12>::ONE.checked_to_degrees_strict().is_some());
1068    /// assert_eq!(D18::<6>::MAX.checked_to_degrees_strict(), None);
1069    /// ```
1070    #[inline]
1071    #[must_use]
1072    pub fn checked_to_degrees_strict(self) -> Option<Self> {
1073        self.checked_to_degrees_strict_with(DEFAULT_ROUNDING_MODE)
1074    }
1075
1076    /// Checked `to_radians_strict_with`. Always `Some`: the conversion
1077    /// multiplies by `π/180 < 1`, so the result is strictly smaller in
1078    /// magnitude than the (representable) input.
1079    ///
1080    /// ```
1081    /// use decimal_scaled::{D38, RoundingMode};
1082    /// let x = D38::<12>::try_from(180i64).unwrap();
1083    /// assert_eq!(
1084    ///     x.checked_to_radians_strict_with(RoundingMode::HalfToEven),
1085    ///     Some(x.to_radians_strict_with(RoundingMode::HalfToEven)),
1086    /// );
1087    /// ```
1088    #[inline]
1089    #[must_use]
1090    pub fn checked_to_radians_strict_with(self, mode: RoundingMode) -> Option<Self> {
1091        crate::policy::trig::checked_to_radians_dispatch::<N, SCALE>(self.0, mode)
1092            .map(Self)
1093    }
1094
1095    /// Default-mode sibling of [`Self::checked_to_radians_strict_with`].
1096    ///
1097    /// ```
1098    /// use decimal_scaled::D38;
1099    /// assert!(D38::<12>::try_from(90i64).unwrap().checked_to_radians_strict().is_some());
1100    /// ```
1101    #[inline]
1102    #[must_use]
1103    pub fn checked_to_radians_strict(self) -> Option<Self> {
1104        self.checked_to_radians_strict_with(DEFAULT_ROUNDING_MODE)
1105    }
1106}