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}