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    // ── Exponentials ──────────────────────────────────────────────
232
233    /// Checked `exp_strict_with`: `e^self`, `None` instead of a panic.
234    ///
235    /// `exp` has no domain wall; `None` means the correctly-rounded
236    /// result does not fit the storage range — the same condition on
237    /// which the default form panics. Otherwise
238    /// `Some(self.exp_strict_with(mode))`, bit-identical.
239    ///
240    /// Out-of-range detection: exact on D18/D38; on the wide tiers an
241    /// out-of-range result still panics (kernel seam not yet reached).
242    ///
243    /// ```
244    /// use decimal_scaled::{D38, RoundingMode};
245    /// let one = D38::<12>::ONE;
246    /// assert_eq!(
247    ///     one.checked_exp_strict_with(RoundingMode::HalfToEven),
248    ///     Some(one.exp_strict_with(RoundingMode::HalfToEven)),
249    /// );
250    /// // e^120 has 53 integer digits — far outside D38's 38.
251    /// assert_eq!(D38::<12>::try_from(120i64).unwrap().checked_exp_strict_with(RoundingMode::HalfToEven), None);
252    /// ```
253    #[inline]
254    #[must_use]
255    pub fn checked_exp_strict_with(self, mode: RoundingMode) -> Option<Self> {
256        crate::policy::exp::checked_dispatch::<N, SCALE>(self.0, mode).map(Self)
257    }
258
259    /// Default-mode sibling of [`Self::checked_exp_strict_with`].
260    ///
261    /// ```
262    /// use decimal_scaled::D38;
263    /// assert!(D38::<12>::ONE.checked_exp_strict().is_some());
264    /// assert_eq!(D38::<12>::try_from(120i64).unwrap().checked_exp_strict(), None);
265    /// ```
266    #[inline]
267    #[must_use]
268    pub fn checked_exp_strict(self) -> Option<Self> {
269        self.checked_exp_strict_with(DEFAULT_ROUNDING_MODE)
270    }
271
272    /// Checked `exp2_strict_with`: `2^self`, `None` instead of a panic.
273    ///
274    /// No domain wall; `None` means the result does not fit the storage
275    /// range. Otherwise bit-identical `Some`.
276    ///
277    /// Out-of-range detection: exact on D18/D38; on the wide tiers an
278    /// out-of-range result still panics (wide kernel-shell seam not yet reached).
279    ///
280    /// ```
281    /// use decimal_scaled::{D38, RoundingMode};
282    /// let ten = D38::<12>::try_from(10i64).unwrap();
283    /// assert_eq!(
284    ///     ten.checked_exp2_strict_with(RoundingMode::HalfToEven),
285    ///     Some(ten.exp2_strict_with(RoundingMode::HalfToEven)),
286    /// );
287    /// // 2^95 has 29 integer digits — outside D38<12>'s 26.
288    /// assert_eq!(D38::<12>::try_from(95i64).unwrap().checked_exp2_strict_with(RoundingMode::HalfToEven), None);
289    /// ```
290    #[inline]
291    #[must_use]
292    pub fn checked_exp2_strict_with(self, mode: RoundingMode) -> Option<Self> {
293        crate::policy::exp::checked_exp2_dispatch::<N, SCALE>(self.0, mode).map(Self)
294    }
295
296    /// Default-mode sibling of [`Self::checked_exp2_strict_with`].
297    ///
298    /// ```
299    /// use decimal_scaled::D38;
300    /// assert!(D38::<12>::try_from(10i64).unwrap().checked_exp2_strict().is_some());
301    /// assert_eq!(D38::<12>::try_from(95i64).unwrap().checked_exp2_strict(), None);
302    /// ```
303    #[inline]
304    #[must_use]
305    pub fn checked_exp2_strict(self) -> Option<Self> {
306        self.checked_exp2_strict_with(DEFAULT_ROUNDING_MODE)
307    }
308
309    // ── Power ─────────────────────────────────────────────────────
310
311    /// Checked `powf_strict_with`: `self^exp`, `None` instead of a
312    /// panic.
313    ///
314    /// `powf` has no domain panic: a non-positive base saturates to
315    /// zero (the kernel's documented behaviour at every tier), so
316    /// `checked_powf` returns `Some(ZERO)` there, matching the default
317    /// form. `None` means the result does not fit the storage range.
318    ///
319    /// Out-of-range detection: exact on D18/D38; on the wide tiers an
320    /// out-of-range result still panics (wide kernel-shell seam not yet reached).
321    ///
322    /// ```
323    /// use decimal_scaled::{D38, RoundingMode};
324    /// let three = D38::<10>::try_from(3i64).unwrap();
325    /// let two = D38::<10>::try_from(2i64).unwrap();
326    /// assert_eq!(
327    ///     three.checked_powf_strict_with(two, RoundingMode::HalfToEven),
328    ///     Some(three.powf_strict_with(two, RoundingMode::HalfToEven)),
329    /// );
330    /// // Non-positive base saturates to zero, as the default form does.
331    /// let half = D38::<10>::ONE / two;
332    /// assert_eq!(
333    ///     (-three).checked_powf_strict_with(half, RoundingMode::HalfToEven),
334    ///     Some(D38::<10>::ZERO),
335    /// );
336    /// // 10^30 has 31 integer digits — out of D38<10>'s 28.
337    /// let ten = D38::<10>::try_from(10i64).unwrap();
338    /// let thirty = D38::<10>::try_from(30i64).unwrap();
339    /// assert_eq!(ten.checked_powf_strict_with(thirty, RoundingMode::HalfToEven), None);
340    /// ```
341    #[inline]
342    #[must_use]
343    pub fn checked_powf_strict_with(self, exp: Self, mode: RoundingMode) -> Option<Self> {
344        crate::policy::pow::checked_dispatch::<N, SCALE>(self.0, exp.0, mode).map(Self)
345    }
346
347    /// Default-mode sibling of [`Self::checked_powf_strict_with`].
348    ///
349    /// ```
350    /// use decimal_scaled::D38;
351    /// let x = D38::<10>::try_from(2i64).unwrap();
352    /// assert!(x.checked_powf_strict(D38::<10>::try_from(8i64).unwrap()).is_some());
353    /// ```
354    #[inline]
355    #[must_use]
356    pub fn checked_powf_strict(self, exp: Self) -> Option<Self> {
357        self.checked_powf_strict_with(exp, DEFAULT_ROUNDING_MODE)
358    }
359
360    // ── Roots ─────────────────────────────────────────────────────
361
362    /// Checked `sqrt_strict_with`. Always `Some`: the strict square
363    /// root is total — negative inputs saturate to zero (the policy's
364    /// documented behaviour, not a panic), and the result `√v ≤
365    /// max(v, 1)` always fits the storage range. The checked form
366    /// exists for surface uniformity.
367    ///
368    /// ```
369    /// use decimal_scaled::{D38, RoundingMode};
370    /// let nine = D38::<10>::try_from(9i64).unwrap();
371    /// assert_eq!(
372    ///     nine.checked_sqrt_strict_with(RoundingMode::HalfToEven),
373    ///     Some(nine.sqrt_strict_with(RoundingMode::HalfToEven)),
374    /// );
375    /// ```
376    #[inline]
377    #[must_use]
378    pub fn checked_sqrt_strict_with(self, mode: RoundingMode) -> Option<Self>
379    where
380        Limbs<N>: ComputeLimbs,
381    {
382        Some(Self(crate::policy::sqrt::dispatch::<N, SCALE>(self.0, mode)))
383    }
384
385    /// Default-mode sibling of [`Self::checked_sqrt_strict_with`].
386    ///
387    /// ```
388    /// use decimal_scaled::D38;
389    /// assert!(D38::<10>::try_from(2i64).unwrap().checked_sqrt_strict().is_some());
390    /// ```
391    #[inline]
392    #[must_use]
393    pub fn checked_sqrt_strict(self) -> Option<Self>
394    where
395        Limbs<N>: ComputeLimbs,
396    {
397        self.checked_sqrt_strict_with(DEFAULT_ROUNDING_MODE)
398    }
399
400    /// Checked `cbrt_strict_with`. Always `Some`: the cube root is
401    /// total over the signed domain and `∛v` never exceeds `max(|v|,
402    /// 1)`, so it always fits the storage range.
403    ///
404    /// ```
405    /// use decimal_scaled::{D38, RoundingMode};
406    /// let x = D38::<10>::try_from(-27i64).unwrap();
407    /// assert_eq!(
408    ///     x.checked_cbrt_strict_with(RoundingMode::HalfToEven),
409    ///     Some(x.cbrt_strict_with(RoundingMode::HalfToEven)),
410    /// );
411    /// ```
412    #[inline]
413    #[must_use]
414    pub fn checked_cbrt_strict_with(self, mode: RoundingMode) -> Option<Self>
415    where
416        Limbs<N>: ComputeLimbs,
417    {
418        Some(Self(crate::policy::cbrt::dispatch::<N, SCALE>(self.0, mode)))
419    }
420
421    /// Default-mode sibling of [`Self::checked_cbrt_strict_with`].
422    ///
423    /// ```
424    /// use decimal_scaled::D38;
425    /// assert!(D38::<10>::try_from(8i64).unwrap().checked_cbrt_strict().is_some());
426    /// ```
427    #[inline]
428    #[must_use]
429    pub fn checked_cbrt_strict(self) -> Option<Self>
430    where
431        Limbs<N>: ComputeLimbs,
432    {
433        self.checked_cbrt_strict_with(DEFAULT_ROUNDING_MODE)
434    }
435
436    /// Checked `hypot_strict_with`: `√(self² + other²)`, `None` instead
437    /// of a panic.
438    ///
439    /// No domain wall; `None` means the result does not fit the storage
440    /// range (possible only when both operands are near the range
441    /// limit). Otherwise bit-identical `Some`.
442    ///
443    /// ```
444    /// use decimal_scaled::{D38, RoundingMode};
445    /// let three = D38::<10>::try_from(3i64).unwrap();
446    /// let four = D38::<10>::try_from(4i64).unwrap();
447    /// assert_eq!(
448    ///     three.checked_hypot_strict_with(four, RoundingMode::HalfToEven),
449    ///     Some(three.hypot_strict_with(four, RoundingMode::HalfToEven)),
450    /// );
451    /// assert_eq!(D38::<10>::MAX.checked_hypot_strict_with(D38::<10>::MAX, RoundingMode::HalfToEven), None);
452    /// ```
453    #[inline]
454    #[must_use]
455    pub fn checked_hypot_strict_with(self, other: Self, mode: RoundingMode) -> Option<Self>
456    where
457        Limbs<N>: ComputeLimbs,
458    {
459        crate::policy::hypot::checked_dispatch::<N, SCALE>(self.0, other.0, mode).map(Self)
460    }
461
462    /// Default-mode sibling of [`Self::checked_hypot_strict_with`].
463    ///
464    /// ```
465    /// use decimal_scaled::D38;
466    /// let a = D38::<10>::try_from(5i64).unwrap();
467    /// assert!(a.checked_hypot_strict(a).is_some());
468    /// ```
469    #[inline]
470    #[must_use]
471    pub fn checked_hypot_strict(self, other: Self) -> Option<Self>
472    where
473        Limbs<N>: ComputeLimbs,
474    {
475        self.checked_hypot_strict_with(other, DEFAULT_ROUNDING_MODE)
476    }
477
478    // ── Trigonometry (forward) ────────────────────────────────────
479
480    /// Checked `sin_strict_with`. Always `Some`: `sin` is total and
481    /// `|sin x| <= 1`, which fits every tier's range at every valid
482    /// scale (each tier keeps >= ~10 of integer headroom at its
483    /// maximum scale).
484    ///
485    /// ```
486    /// use decimal_scaled::{D38, RoundingMode};
487    /// let one = D38::<12>::ONE;
488    /// assert_eq!(
489    ///     one.checked_sin_strict_with(RoundingMode::HalfToEven),
490    ///     Some(one.sin_strict_with(RoundingMode::HalfToEven)),
491    /// );
492    /// ```
493    #[inline]
494    #[must_use]
495    pub fn checked_sin_strict_with(self, mode: RoundingMode) -> Option<Self> {
496        Some(Self(crate::policy::trig::sin_dispatch::<N, SCALE>(self.0, mode)))
497    }
498
499    /// Default-mode sibling of [`Self::checked_sin_strict_with`].
500    ///
501    /// ```
502    /// use decimal_scaled::D38;
503    /// assert!(D38::<12>::ONE.checked_sin_strict().is_some());
504    /// ```
505    #[inline]
506    #[must_use]
507    pub fn checked_sin_strict(self) -> Option<Self> {
508        self.checked_sin_strict_with(DEFAULT_ROUNDING_MODE)
509    }
510
511    /// Checked `cos_strict_with`. Always `Some`: `cos` is total and
512    /// `|cos x| <= 1` fits every tier's range at every valid scale.
513    ///
514    /// ```
515    /// use decimal_scaled::{D38, RoundingMode};
516    /// let one = D38::<12>::ONE;
517    /// assert_eq!(
518    ///     one.checked_cos_strict_with(RoundingMode::HalfToEven),
519    ///     Some(one.cos_strict_with(RoundingMode::HalfToEven)),
520    /// );
521    /// ```
522    #[inline]
523    #[must_use]
524    pub fn checked_cos_strict_with(self, mode: RoundingMode) -> Option<Self> {
525        Some(Self(crate::policy::trig::cos_dispatch::<N, SCALE>(self.0, mode)))
526    }
527
528    /// Default-mode sibling of [`Self::checked_cos_strict_with`].
529    ///
530    /// ```
531    /// use decimal_scaled::D38;
532    /// assert!(D38::<12>::ONE.checked_cos_strict().is_some());
533    /// ```
534    #[inline]
535    #[must_use]
536    pub fn checked_cos_strict(self) -> Option<Self> {
537        self.checked_cos_strict_with(DEFAULT_ROUNDING_MODE)
538    }
539
540    /// Checked `tan_strict_with`: `None` instead of a panic.
541    ///
542    /// The default form panics when the argument's cosine rounds to
543    /// zero at the working precision (an odd multiple of π/2 to within
544    /// the kernel's resolution) and when the result does not fit the
545    /// storage range (near those asymptotes). Both conditions are
546    /// detected inside the kernels at every tier; this checked form
547    /// currently panics on them identically to the default form
548    /// (kernel seam not yet reached). For every other input
549    /// it returns bit-identical `Some`.
550    ///
551    /// ```
552    /// use decimal_scaled::{D38, RoundingMode};
553    /// let one = D38::<12>::ONE;
554    /// assert_eq!(
555    ///     one.checked_tan_strict_with(RoundingMode::HalfToEven),
556    ///     Some(one.tan_strict_with(RoundingMode::HalfToEven)),
557    /// );
558    /// ```
559    #[inline]
560    #[must_use]
561    pub fn checked_tan_strict_with(self, mode: RoundingMode) -> Option<Self> {
562        Some(Self(crate::policy::trig::tan_dispatch::<N, SCALE>(self.0, mode)))
563    }
564
565    /// Default-mode sibling of [`Self::checked_tan_strict_with`].
566    ///
567    /// ```
568    /// use decimal_scaled::D38;
569    /// assert!(D38::<12>::ONE.checked_tan_strict().is_some());
570    /// ```
571    #[inline]
572    #[must_use]
573    pub fn checked_tan_strict(self) -> Option<Self> {
574        self.checked_tan_strict_with(DEFAULT_ROUNDING_MODE)
575    }
576
577    // ── Trigonometry (inverse) ────────────────────────────────────
578
579    /// Checked `asin_strict_with`: `None` instead of a domain panic.
580    ///
581    /// Returns `None` when `|self| > 1` (the default form's domain
582    /// wall). The result `|asin x| <= π/2` always fits the storage
583    /// range, so there is no out-of-range case. Otherwise bit-identical
584    /// `Some`.
585    ///
586    /// ```
587    /// use decimal_scaled::{D38, RoundingMode};
588    /// let half = D38::<12>::ONE / D38::<12>::try_from(2i64).unwrap();
589    /// assert_eq!(
590    ///     half.checked_asin_strict_with(RoundingMode::HalfToEven),
591    ///     Some(half.asin_strict_with(RoundingMode::HalfToEven)),
592    /// );
593    /// assert_eq!(D38::<12>::try_from(2i64).unwrap().checked_asin_strict_with(RoundingMode::HalfToEven), None);
594    /// ```
595    #[inline]
596    #[must_use]
597    pub fn checked_asin_strict_with(self, mode: RoundingMode) -> Option<Self> {
598        let one = Self::unit_bits();
599        if self.0 > one || self.0 < -one {
600            return None;
601        }
602        crate::policy::trig::checked_asin_dispatch::<N, SCALE>(self.0, mode).map(Self)
603    }
604
605    /// Default-mode sibling of [`Self::checked_asin_strict_with`].
606    ///
607    /// ```
608    /// use decimal_scaled::D38;
609    /// assert!(D38::<12>::ONE.checked_asin_strict().is_some());
610    /// assert_eq!(D38::<12>::try_from(-2i64).unwrap().checked_asin_strict(), None);
611    /// ```
612    #[inline]
613    #[must_use]
614    pub fn checked_asin_strict(self) -> Option<Self> {
615        self.checked_asin_strict_with(DEFAULT_ROUNDING_MODE)
616    }
617
618    /// Checked `acos_strict_with`: `None` instead of a domain panic.
619    ///
620    /// Returns `None` when `|self| > 1`. The result `0 <= acos x <= π`
621    /// always fits the storage range, so there is no out-of-range case.
622    /// Otherwise bit-identical `Some`.
623    ///
624    /// ```
625    /// use decimal_scaled::{D38, RoundingMode};
626    /// let half = D38::<12>::ONE / D38::<12>::try_from(2i64).unwrap();
627    /// assert_eq!(
628    ///     half.checked_acos_strict_with(RoundingMode::HalfToEven),
629    ///     Some(half.acos_strict_with(RoundingMode::HalfToEven)),
630    /// );
631    /// assert_eq!(D38::<12>::try_from(2i64).unwrap().checked_acos_strict_with(RoundingMode::HalfToEven), None);
632    /// ```
633    #[inline]
634    #[must_use]
635    pub fn checked_acos_strict_with(self, mode: RoundingMode) -> Option<Self> {
636        let one = Self::unit_bits();
637        if self.0 > one || self.0 < -one {
638            return None;
639        }
640        crate::policy::trig::checked_acos_dispatch::<N, SCALE>(self.0, mode).map(Self)
641    }
642
643    /// Default-mode sibling of [`Self::checked_acos_strict_with`].
644    ///
645    /// ```
646    /// use decimal_scaled::D38;
647    /// assert!(D38::<12>::ONE.checked_acos_strict().is_some());
648    /// assert_eq!(D38::<12>::try_from(2i64).unwrap().checked_acos_strict(), None);
649    /// ```
650    #[inline]
651    #[must_use]
652    pub fn checked_acos_strict(self) -> Option<Self> {
653        self.checked_acos_strict_with(DEFAULT_ROUNDING_MODE)
654    }
655
656    /// Checked `atan_strict_with`. Always `Some`: `atan` is total and
657    /// `|atan x| < π/2` fits every tier's range at every valid scale.
658    ///
659    /// ```
660    /// use decimal_scaled::{D38, RoundingMode};
661    /// let x = D38::<12>::try_from(5i64).unwrap();
662    /// assert_eq!(
663    ///     x.checked_atan_strict_with(RoundingMode::HalfToEven),
664    ///     Some(x.atan_strict_with(RoundingMode::HalfToEven)),
665    /// );
666    /// ```
667    #[inline]
668    #[must_use]
669    pub fn checked_atan_strict_with(self, mode: RoundingMode) -> Option<Self> {
670        Some(Self(crate::policy::trig::atan_dispatch::<N, SCALE>(self.0, mode)))
671    }
672
673    /// Default-mode sibling of [`Self::checked_atan_strict_with`].
674    ///
675    /// ```
676    /// use decimal_scaled::D38;
677    /// assert!(D38::<12>::try_from(3i64).unwrap().checked_atan_strict().is_some());
678    /// ```
679    #[inline]
680    #[must_use]
681    pub fn checked_atan_strict(self) -> Option<Self> {
682        self.checked_atan_strict_with(DEFAULT_ROUNDING_MODE)
683    }
684
685    /// Checked `atan2_strict_with`. Always `Some`: `atan2` is total
686    /// (including the `(0, 0)` origin, which yields `0`) and `|atan2(y,
687    /// x)| <= π` fits every tier's range at every valid scale.
688    ///
689    /// ```
690    /// use decimal_scaled::{D38, RoundingMode};
691    /// let y = D38::<12>::ONE;
692    /// let x = D38::<12>::try_from(2i64).unwrap();
693    /// assert_eq!(
694    ///     y.checked_atan2_strict_with(x, RoundingMode::HalfToEven),
695    ///     Some(y.atan2_strict_with(x, RoundingMode::HalfToEven)),
696    /// );
697    /// ```
698    #[inline]
699    #[must_use]
700    pub fn checked_atan2_strict_with(self, other: Self, mode: RoundingMode) -> Option<Self> {
701        crate::policy::trig::checked_atan2_dispatch::<N, SCALE>(self.0, other.0, mode)
702            .map(Self)
703    }
704
705    /// Default-mode sibling of [`Self::checked_atan2_strict_with`].
706    ///
707    /// ```
708    /// use decimal_scaled::D38;
709    /// assert!(D38::<12>::ONE.checked_atan2_strict(D38::<12>::ONE).is_some());
710    /// ```
711    #[inline]
712    #[must_use]
713    pub fn checked_atan2_strict(self, other: Self) -> Option<Self> {
714        self.checked_atan2_strict_with(other, DEFAULT_ROUNDING_MODE)
715    }
716
717    // ── Hyperbolics ───────────────────────────────────────────────
718
719    /// Checked `sinh_strict_with`: `None` instead of a panic.
720    ///
721    /// `sinh` has no domain wall; `None` means the result does not fit
722    /// the storage range (it grows like `e^|x|/2`). Otherwise
723    /// bit-identical `Some`.
724    ///
725    /// Out-of-range detection: exact on D18 (a result that fits the
726    /// D38 work width but not D18 storage is `None`); detection deeper
727    /// in the kernels (D38 and the wide tiers) still panics (kernel seam not yet reached).
728    ///
729    /// ```
730    /// use decimal_scaled::{D18, D38, RoundingMode};
731    /// let one = D38::<12>::ONE;
732    /// assert_eq!(
733    ///     one.checked_sinh_strict_with(RoundingMode::HalfToEven),
734    ///     Some(one.sinh_strict_with(RoundingMode::HalfToEven)),
735    /// );
736    /// // sinh(40) ~ 1.2e17 exceeds D18<6>'s range but fits the D38 work width.
737    /// assert_eq!(D18::<6>::try_from(40).unwrap().checked_sinh_strict_with(RoundingMode::HalfToEven), None);
738    /// ```
739    #[inline]
740    #[must_use]
741    pub fn checked_sinh_strict_with(self, mode: RoundingMode) -> Option<Self> {
742        crate::policy::trig::checked_sinh_dispatch::<N, SCALE>(self.0, mode)
743            .map(Self)
744    }
745
746    /// Default-mode sibling of [`Self::checked_sinh_strict_with`].
747    ///
748    /// ```
749    /// use decimal_scaled::{D18, D38};
750    /// assert!(D38::<12>::ONE.checked_sinh_strict().is_some());
751    /// assert_eq!(D18::<6>::try_from(40).unwrap().checked_sinh_strict(), None);
752    /// ```
753    #[inline]
754    #[must_use]
755    pub fn checked_sinh_strict(self) -> Option<Self> {
756        self.checked_sinh_strict_with(DEFAULT_ROUNDING_MODE)
757    }
758
759    /// Checked `cosh_strict_with`: `None` instead of a panic.
760    ///
761    /// `cosh` has no domain wall; `None` means the result does not fit
762    /// the storage range. Otherwise bit-identical `Some`.
763    ///
764    /// Out-of-range detection: exact on D18 (a result that fits the
765    /// D38 work width but not D18 storage is `None`); detection deeper
766    /// in the kernels (D38 and the wide tiers) still panics (kernel seam not yet reached).
767    ///
768    /// ```
769    /// use decimal_scaled::{D18, D38, RoundingMode};
770    /// let one = D38::<12>::ONE;
771    /// assert_eq!(
772    ///     one.checked_cosh_strict_with(RoundingMode::HalfToEven),
773    ///     Some(one.cosh_strict_with(RoundingMode::HalfToEven)),
774    /// );
775    /// // cosh(40) ~ 1.2e17 exceeds D18<6>'s range but fits the D38 work width.
776    /// assert_eq!(D18::<6>::try_from(40).unwrap().checked_cosh_strict_with(RoundingMode::HalfToEven), None);
777    /// ```
778    #[inline]
779    #[must_use]
780    pub fn checked_cosh_strict_with(self, mode: RoundingMode) -> Option<Self> {
781        crate::policy::trig::checked_cosh_dispatch::<N, SCALE>(self.0, mode)
782            .map(Self)
783    }
784
785    /// Default-mode sibling of [`Self::checked_cosh_strict_with`].
786    ///
787    /// ```
788    /// use decimal_scaled::{D18, D38};
789    /// assert!(D38::<12>::ONE.checked_cosh_strict().is_some());
790    /// assert_eq!(D18::<6>::try_from(40).unwrap().checked_cosh_strict(), None);
791    /// ```
792    #[inline]
793    #[must_use]
794    pub fn checked_cosh_strict(self) -> Option<Self> {
795        self.checked_cosh_strict_with(DEFAULT_ROUNDING_MODE)
796    }
797
798    /// Checked `tanh_strict_with`. Always `Some`: `tanh` is total and
799    /// `|tanh x| <= 1` fits every tier's range at every valid scale.
800    ///
801    /// ```
802    /// use decimal_scaled::{D38, RoundingMode};
803    /// let one = D38::<12>::ONE;
804    /// assert_eq!(
805    ///     one.checked_tanh_strict_with(RoundingMode::HalfToEven),
806    ///     Some(one.tanh_strict_with(RoundingMode::HalfToEven)),
807    /// );
808    /// ```
809    #[inline]
810    #[must_use]
811    pub fn checked_tanh_strict_with(self, mode: RoundingMode) -> Option<Self> {
812        crate::policy::trig::checked_tanh_dispatch::<N, SCALE>(self.0, mode).map(Self)
813    }
814
815    /// Default-mode sibling of [`Self::checked_tanh_strict_with`].
816    ///
817    /// ```
818    /// use decimal_scaled::D38;
819    /// assert!(D38::<12>::ONE.checked_tanh_strict().is_some());
820    /// ```
821    #[inline]
822    #[must_use]
823    pub fn checked_tanh_strict(self) -> Option<Self> {
824        self.checked_tanh_strict_with(DEFAULT_ROUNDING_MODE)
825    }
826
827    /// Checked `asinh_strict_with`. Always `Some`: `asinh` is total and
828    /// `|asinh x| <= max(|x|, 1)` always fits the storage range when
829    /// `x` does.
830    ///
831    /// ```
832    /// use decimal_scaled::{D38, RoundingMode};
833    /// let x = D38::<12>::try_from(3i64).unwrap();
834    /// assert_eq!(
835    ///     x.checked_asinh_strict_with(RoundingMode::HalfToEven),
836    ///     Some(x.asinh_strict_with(RoundingMode::HalfToEven)),
837    /// );
838    /// ```
839    #[inline]
840    #[must_use]
841    pub fn checked_asinh_strict_with(self, mode: RoundingMode) -> Option<Self> {
842        crate::policy::trig::checked_asinh_dispatch::<N, SCALE>(self.0, mode).map(Self)
843    }
844
845    /// Default-mode sibling of [`Self::checked_asinh_strict_with`].
846    ///
847    /// ```
848    /// use decimal_scaled::D38;
849    /// assert!(D38::<12>::try_from(2i64).unwrap().checked_asinh_strict().is_some());
850    /// ```
851    #[inline]
852    #[must_use]
853    pub fn checked_asinh_strict(self) -> Option<Self> {
854        self.checked_asinh_strict_with(DEFAULT_ROUNDING_MODE)
855    }
856
857    /// Checked `acosh_strict_with`: `None` instead of a domain panic.
858    ///
859    /// Returns `None` when `self < 1` (the default form's domain wall).
860    /// The result `acosh x < ln(2x) <= x` always fits the storage range
861    /// when `x` does. Otherwise bit-identical `Some`.
862    ///
863    /// ```
864    /// use decimal_scaled::{D38, RoundingMode};
865    /// let two = D38::<12>::try_from(2i64).unwrap();
866    /// assert_eq!(
867    ///     two.checked_acosh_strict_with(RoundingMode::HalfToEven),
868    ///     Some(two.acosh_strict_with(RoundingMode::HalfToEven)),
869    /// );
870    /// assert_eq!(D38::<12>::ZERO.checked_acosh_strict_with(RoundingMode::HalfToEven), None);
871    /// ```
872    #[inline]
873    #[must_use]
874    pub fn checked_acosh_strict_with(self, mode: RoundingMode) -> Option<Self> {
875        if self.0 < Self::unit_bits() {
876            return None;
877        }
878        crate::policy::trig::checked_acosh_dispatch::<N, SCALE>(self.0, mode).map(Self)
879    }
880
881    /// Default-mode sibling of [`Self::checked_acosh_strict_with`].
882    ///
883    /// ```
884    /// use decimal_scaled::D38;
885    /// assert!(D38::<12>::try_from(3i64).unwrap().checked_acosh_strict().is_some());
886    /// assert_eq!(D38::<12>::ZERO.checked_acosh_strict(), None);
887    /// ```
888    #[inline]
889    #[must_use]
890    pub fn checked_acosh_strict(self) -> Option<Self> {
891        self.checked_acosh_strict_with(DEFAULT_ROUNDING_MODE)
892    }
893
894    /// Checked `atanh_strict_with`: `None` instead of a panic.
895    ///
896    /// Returns `None` when `|self| >= 1` (the default form's domain
897    /// wall — `atanh` diverges at ±1). An out-of-range result (the
898    /// logarithmic blow-up just inside ±1 at a near-maximum scale) is
899    /// `None` on D18 when it fits the D38 work width; detection deeper
900    /// in the kernels still panics, identically to the default form
901    /// (kernel seam not yet reached). Otherwise bit-identical
902    /// `Some`.
903    ///
904    /// ```
905    /// use decimal_scaled::{D38, RoundingMode};
906    /// let half = D38::<12>::ONE / D38::<12>::try_from(2i64).unwrap();
907    /// assert_eq!(
908    ///     half.checked_atanh_strict_with(RoundingMode::HalfToEven),
909    ///     Some(half.atanh_strict_with(RoundingMode::HalfToEven)),
910    /// );
911    /// assert_eq!(D38::<12>::ONE.checked_atanh_strict_with(RoundingMode::HalfToEven), None);
912    /// ```
913    #[inline]
914    #[must_use]
915    pub fn checked_atanh_strict_with(self, mode: RoundingMode) -> Option<Self> {
916        let one = Self::unit_bits();
917        if self.0 >= one || self.0 <= -one {
918            return None;
919        }
920        crate::policy::trig::checked_atanh_dispatch::<N, SCALE>(self.0, mode)
921            .map(Self)
922    }
923
924    /// Default-mode sibling of [`Self::checked_atanh_strict_with`].
925    ///
926    /// ```
927    /// use decimal_scaled::D38;
928    /// let half = D38::<12>::ONE / D38::<12>::try_from(2i64).unwrap();
929    /// assert!(half.checked_atanh_strict().is_some());
930    /// assert_eq!(D38::<12>::ONE.checked_atanh_strict(), None);
931    /// ```
932    #[inline]
933    #[must_use]
934    pub fn checked_atanh_strict(self) -> Option<Self> {
935        self.checked_atanh_strict_with(DEFAULT_ROUNDING_MODE)
936    }
937
938    // ── Angle conversion ──────────────────────────────────────────
939
940    /// Checked `to_degrees_strict_with`: `None` instead of a panic.
941    ///
942    /// No domain wall; `None` means `self · (180/π)` does not fit the
943    /// storage range (the result is ~57.3× the input). Otherwise
944    /// bit-identical `Some`.
945    ///
946    /// Out-of-range detection: exact on D18 (a result that fits the
947    /// D38 work width but not D18 storage is `None`); detection deeper
948    /// in the kernels (D38 and the wide tiers) still panics (kernel seam not yet reached).
949    ///
950    /// ```
951    /// use decimal_scaled::{D18, D38, RoundingMode};
952    /// let one = D38::<12>::ONE;
953    /// assert_eq!(
954    ///     one.checked_to_degrees_strict_with(RoundingMode::HalfToEven),
955    ///     Some(one.to_degrees_strict_with(RoundingMode::HalfToEven)),
956    /// );
957    /// // MAX·(180/π) leaves D18's range but fits the D38 work width.
958    /// assert_eq!(D18::<6>::MAX.checked_to_degrees_strict_with(RoundingMode::HalfToEven), None);
959    /// ```
960    #[inline]
961    #[must_use]
962    pub fn checked_to_degrees_strict_with(self, mode: RoundingMode) -> Option<Self> {
963        crate::policy::trig::checked_to_degrees_dispatch::<N, SCALE>(self.0, mode)
964            .map(Self)
965    }
966
967    /// Default-mode sibling of [`Self::checked_to_degrees_strict_with`].
968    ///
969    /// ```
970    /// use decimal_scaled::{D18, D38};
971    /// assert!(D38::<12>::ONE.checked_to_degrees_strict().is_some());
972    /// assert_eq!(D18::<6>::MAX.checked_to_degrees_strict(), None);
973    /// ```
974    #[inline]
975    #[must_use]
976    pub fn checked_to_degrees_strict(self) -> Option<Self> {
977        self.checked_to_degrees_strict_with(DEFAULT_ROUNDING_MODE)
978    }
979
980    /// Checked `to_radians_strict_with`. Always `Some`: the conversion
981    /// multiplies by `π/180 < 1`, so the result is strictly smaller in
982    /// magnitude than the (representable) input.
983    ///
984    /// ```
985    /// use decimal_scaled::{D38, RoundingMode};
986    /// let x = D38::<12>::try_from(180i64).unwrap();
987    /// assert_eq!(
988    ///     x.checked_to_radians_strict_with(RoundingMode::HalfToEven),
989    ///     Some(x.to_radians_strict_with(RoundingMode::HalfToEven)),
990    /// );
991    /// ```
992    #[inline]
993    #[must_use]
994    pub fn checked_to_radians_strict_with(self, mode: RoundingMode) -> Option<Self> {
995        crate::policy::trig::checked_to_radians_dispatch::<N, SCALE>(self.0, mode)
996            .map(Self)
997    }
998
999    /// Default-mode sibling of [`Self::checked_to_radians_strict_with`].
1000    ///
1001    /// ```
1002    /// use decimal_scaled::D38;
1003    /// assert!(D38::<12>::try_from(90i64).unwrap().checked_to_radians_strict().is_some());
1004    /// ```
1005    #[inline]
1006    #[must_use]
1007    pub fn checked_to_radians_strict(self) -> Option<Self> {
1008        self.checked_to_radians_strict_with(DEFAULT_ROUNDING_MODE)
1009    }
1010}