Skip to main content

dashu_float/
fbig_cached.rs

1//! A cached floating-point number — [`FBig`] with a shared constant cache attached.
2
3use alloc::rc::Rc;
4use core::cell::RefCell;
5use core::cmp::Ordering;
6use core::str::FromStr;
7
8use dashu_base::{AbsOrd, ConversionError, EstimatedLog2, ParseError, Sign, Signed};
9use dashu_int::{IBig, UBig};
10
11use crate::error::panic_unlimited_precision;
12use crate::fbig::FBig;
13use crate::math::cache::ConstCache;
14use crate::repr::{Context, Repr, Word};
15use crate::round::{mode, Round, Rounded};
16use crate::utils::digit_len;
17
18/// A floating-point number that carries a shared handle to a [`ConstCache`].
19///
20/// It is functionally an [`FBig`]: same in-memory representation (`fbig`),
21/// plus an [`Rc<RefCell<ConstCache>>`] handle. The difference is that the
22/// transcendental operations (`ln`, `exp`, `sin`, `cos`, …, `pi`, base conversion)
23/// thread that handle into the underlying [`Context`] methods, so they reuse and
24/// progressively extend the cached exact binary-splitting state instead of
25/// recomputing constants from scratch on every call.
26///
27/// `Context`/`FBig` themselves stay `Copy` + `Send` + `Sync` + `no_std` (so
28/// `static_fbig!` keeps working); only this cached
29/// wrapper is `!Send + !Sync`, because it shares state through an `Rc<RefCell<..>>`.
30/// To share one cache across threads, build an analogous type over
31/// `Arc<Mutex<ConstCache>>` instead (the [`Context`] methods accept
32/// `Option<&mut ConstCache>`, independent of the container).
33///
34/// Every value-producing operation returns a `CachedFBig` that preserves the
35/// handle, so `(a + b).ln().exp()` stays cached throughout — no silent cache loss.
36/// When two `CachedFBig` values with different cache handles interact in a binary
37/// operation, the LHS (left-hand-side) cache is preserved in the result. For
38/// `FBig op CachedFBig`, the `CachedFBig` operand's cache is preserved.
39///
40/// # Examples
41///
42/// ```
43/// use core::cell::RefCell;
44/// use core::str::FromStr;
45/// use dashu_float::{CachedFBig, ConstCache, Context};
46/// use dashu_float::round::mode::HalfAway;
47/// use std::rc::Rc;
48///
49/// let cache = Rc::new(RefCell::new(ConstCache::new()));
50/// // build a cached decimal number 1.234
51/// let x = CachedFBig::<HalfAway, 10>::with_cache(
52///     dashu_float::Repr::new(1234.into(), -3),
53///     Context::new(50),
54/// );
55///
56/// // ln / exp reuse the same shared cache handle
57/// let _ = x.clone().ln().exp();
58/// ```
59pub struct CachedFBig<R: Round = mode::Zero, const B: Word = 2> {
60    pub(crate) fbig: FBig<R, B>,
61    pub(crate) cache: Rc<RefCell<ConstCache>>,
62}
63
64impl<R: Round, const B: Word> CachedFBig<R, B> {
65    /// Wrap an [`FBig`], sharing the given cache handle.
66    #[inline]
67    pub fn new(value: FBig<R, B>, cache: Rc<RefCell<ConstCache>>) -> Self {
68        Self { fbig: value, cache }
69    }
70
71    /// Build from raw parts, sharing the given cache handle.
72    #[inline]
73    pub fn from_repr(repr: Repr<B>, context: Context<R>, cache: Rc<RefCell<ConstCache>>) -> Self {
74        Self {
75            fbig: FBig::new(repr, context),
76            cache,
77        }
78    }
79
80    /// Build from raw parts with a fresh, exclusive cache.
81    #[inline]
82    pub fn with_cache(repr: Repr<B>, context: Context<R>) -> Self {
83        Self::from_repr(repr, context, Rc::new(RefCell::new(ConstCache::new())))
84    }
85
86    /// Build a `CachedFBig` from an [`FBig`] result, re-attaching this value's
87    /// shared cache handle (cloned cheaply via `Rc`).
88    #[inline]
89    pub(crate) fn from_fbig(fbig: FBig<R, B>, cache: &Rc<RefCell<ConstCache>>) -> Self {
90        Self {
91            fbig,
92            cache: Rc::clone(cache),
93        }
94    }
95
96    /// Borrow the inner [`FBig`].
97    #[inline]
98    pub fn as_fbig(&self) -> &FBig<R, B> {
99        &self.fbig
100    }
101
102    /// Drop the cache handle and return the underlying [`FBig`].
103    #[inline]
104    pub fn into_fbig(self) -> FBig<R, B> {
105        self.fbig
106    }
107
108    /// Borrow the shared constant cache immutably.
109    ///
110    /// Use this to inspect cache state, e.g. `cached.cache().total_terms()`.
111    #[inline]
112    pub fn cache(&self) -> impl core::ops::Deref<Target = ConstCache> + '_ {
113        self.cache.borrow()
114    }
115
116    /// Clear all cached constant state, freeing the underlying memory.
117    ///
118    /// The next transcendental operation will recompute constants from scratch.
119    #[inline]
120    pub fn clear_cache(&self) {
121        self.cache.borrow_mut().clear();
122    }
123
124    /// π at `precision` base-`B` digits, reusing/extending `cache`.
125    pub fn pi(precision: usize, cache: &Rc<RefCell<ConstCache>>) -> Self {
126        let fbig = {
127            let mut c = cache.borrow_mut();
128            Context::<R>::new(precision).pi::<B>(Some(&mut *c)).value()
129        };
130        Self::from_fbig(fbig, cache)
131    }
132
133    /// *e* (Euler's number) at `precision` base-`B` digits.
134    ///
135    /// Unlike [`pi`](Self::pi), *e* is not cached: it depends on no other constant
136    /// and is reused by no operation, so there is no shared state to thread. The
137    /// `cache` handle is attached only so the result is a [`CachedFBig`] whose
138    /// later transcendental ops still share a cache.
139    pub fn e(precision: usize, cache: &Rc<RefCell<ConstCache>>) -> Self {
140        let fbig = Context::<R>::new(precision).e::<B>().value();
141        Self::from_fbig(fbig, cache)
142    }
143
144    // ----- accessors -----
145
146    /// Maximum precision set for the number (see [`FBig::precision`]).
147    #[inline]
148    pub const fn precision(&self) -> usize {
149        self.fbig.context.precision
150    }
151
152    /// Number of significant digits (see [`FBig::digits`]).
153    #[inline]
154    pub fn digits(&self) -> usize {
155        self.fbig.repr.digits()
156    }
157
158    /// The associated context.
159    #[inline]
160    pub const fn context(&self) -> Context<R> {
161        self.fbig.context
162    }
163
164    /// The underlying representation.
165    #[inline]
166    pub const fn repr(&self) -> &Repr<B> {
167        &self.fbig.repr
168    }
169
170    /// Consume and return the underlying representation.
171    #[inline]
172    pub fn into_repr(self) -> Repr<B> {
173        self.fbig.repr
174    }
175
176    /// Sign of the number (see [`FBig::sign`]).
177    #[inline]
178    pub const fn sign(&self) -> Sign {
179        self.fbig.repr.sign()
180    }
181
182    /// Change precision, preserving the handle (see [`FBig::with_precision`]).
183    pub fn with_precision(&self, precision: usize) -> Rounded<Self> {
184        self.fbig
185            .clone()
186            .with_precision(precision)
187            .map(|f| Self::from_fbig(f, &self.cache))
188    }
189
190    /// Change rounding mode, preserving the handle (see [`FBig::with_rounding`]).
191    pub fn with_rounding<NewR: Round>(&self) -> CachedFBig<NewR, B> {
192        CachedFBig::from_fbig(self.fbig.clone().with_rounding::<NewR>(), &self.cache)
193    }
194}
195
196impl<R: Round, const B: Word> CachedFBig<R, B> {
197    /// ULP of the number (see [`FBig::ulp`]).
198    pub fn ulp(&self) -> Self {
199        if self.fbig.context.precision == 0 {
200            panic_unlimited_precision();
201        }
202        let repr = Repr {
203            significand: dashu_int::IBig::ONE,
204            exponent: self.fbig.repr.exponent + self.fbig.repr.digits() as isize
205                - self.fbig.context.precision as isize,
206        };
207        Self::from_repr(repr, self.fbig.context, Rc::clone(&self.cache))
208    }
209
210    /// A cheap lower bound on [`ulp`](Self::ulp) (see [`FBig::ulp_lb`]).
211    pub fn ulp_lb(&self) -> Self {
212        Self::from_fbig(self.fbig.ulp_lb(), &self.cache)
213    }
214
215    /// The signum (`+1`/`0`/`-1` as the same type) (see [`FBig::signum`]).
216    pub fn signum(&self) -> Self {
217        Self::from_fbig(self.fbig.signum(), &self.cache)
218    }
219
220    /// Split into `(integral, fractional)` parts (see [`FBig::split_at_point`]).
221    pub fn split_at_point(self) -> (Self, Self) {
222        let CachedFBig { fbig, cache } = self;
223        let (a, b) = fbig.split_at_point();
224        (Self::from_fbig(a, &cache), Self::from_fbig(b, &cache))
225    }
226
227    /// Convert to an integer (see [`FBig::to_int`]).
228    pub fn to_int(&self) -> Rounded<dashu_int::IBig> {
229        self.fbig.clone().to_int()
230    }
231
232    /// Convert to `f32` (see [`FBig::to_f32`]).
233    pub fn to_f32(&self) -> Rounded<f32> {
234        self.fbig.clone().to_f32()
235    }
236
237    /// Convert to `f64` (see [`FBig::to_f64`]).
238    pub fn to_f64(&self) -> Rounded<f64> {
239        self.fbig.clone().to_f64()
240    }
241
242    /// Construct from significand + exponent, with a fresh cache (see [`FBig::from_parts`]).
243    pub fn from_parts(significand: dashu_int::IBig, exponent: isize) -> Self {
244        let precision = digit_len::<B>(&significand).max(1);
245        let repr = Repr::new(significand, exponent);
246        Self::with_cache(repr, Context::new(precision))
247    }
248}
249
250// ---------------------------------------------------------------------------
251// From / Into
252// ---------------------------------------------------------------------------
253
254impl<R: Round, const B: Word> From<FBig<R, B>> for CachedFBig<R, B> {
255    #[inline]
256    fn from(fbig: FBig<R, B>) -> Self {
257        Self::new(fbig, Rc::new(RefCell::new(ConstCache::new())))
258    }
259}
260
261impl<R: Round, const B: Word> From<CachedFBig<R, B>> for FBig<R, B> {
262    #[inline]
263    fn from(cached: CachedFBig<R, B>) -> Self {
264        cached.into_fbig()
265    }
266}
267
268impl<R: Round, const B: Word> FBig<R, B> {
269    /// Attach a shared cache handle, turning this [`FBig`] into a [`CachedFBig`].
270    #[inline]
271    pub fn into_cached(self, cache: Rc<RefCell<ConstCache>>) -> CachedFBig<R, B> {
272        CachedFBig::new(self, cache)
273    }
274}
275
276// ---------------------------------------------------------------------------
277// FromStr / From / TryFrom
278//
279// Construction from an external value (string, integer, primitive float) attaches
280// a *fresh* cache, exactly like `From<FBig>` above — there is no existing handle
281// to share, and `FromStr`/`TryFrom` have no parameter for one.
282// ---------------------------------------------------------------------------
283
284impl<R: Round, const B: Word> FromStr for CachedFBig<R, B> {
285    type Err = ParseError;
286
287    #[inline]
288    fn from_str(s: &str) -> Result<Self, ParseError> {
289        Ok(FBig::from_str(s)?.into())
290    }
291}
292
293impl<R: Round, const B: Word> From<UBig> for CachedFBig<R, B> {
294    #[inline]
295    fn from(n: UBig) -> Self {
296        FBig::from(n).into()
297    }
298}
299
300impl<R: Round, const B: Word> From<IBig> for CachedFBig<R, B> {
301    #[inline]
302    fn from(n: IBig) -> Self {
303        FBig::from(n).into()
304    }
305}
306
307macro_rules! impl_from_int_for_cached_fbig {
308    ($($t:ty)*) => {$(
309        impl<R: Round, const B: Word> From<$t> for CachedFBig<R, B> {
310            #[inline]
311            fn from(value: $t) -> Self {
312                FBig::from(value).into()
313            }
314        }
315    )*};
316}
317impl_from_int_for_cached_fbig!(u8 u16 u32 u64 u128 usize i8 i16 i32 i64 i128 isize);
318
319impl<R: Round> TryFrom<f32> for CachedFBig<R, 2> {
320    type Error = ConversionError;
321
322    #[inline]
323    fn try_from(value: f32) -> Result<Self, Self::Error> {
324        FBig::try_from(value).map(Self::from)
325    }
326}
327
328impl<R: Round> TryFrom<f64> for CachedFBig<R, 2> {
329    type Error = ConversionError;
330
331    #[inline]
332    fn try_from(value: f64) -> Result<Self, Self::Error> {
333        FBig::try_from(value).map(Self::from)
334    }
335}
336
337macro_rules! impl_try_from_cached_fbig_for_int {
338    ($($t:ty)*) => {$(
339        impl<R: Round, const B: Word> TryFrom<CachedFBig<R, B>> for $t {
340            type Error = ConversionError;
341
342            #[inline]
343            fn try_from(value: CachedFBig<R, B>) -> Result<Self, Self::Error> {
344                value.fbig.try_into()
345            }
346        }
347    )*};
348}
349impl_try_from_cached_fbig_for_int!(
350    u8 u16 u32 u64 u128 usize i8 i16 i32 i64 i128 isize UBig IBig
351);
352
353impl<R: Round> TryFrom<CachedFBig<R, 2>> for f32 {
354    type Error = ConversionError;
355
356    #[inline]
357    fn try_from(value: CachedFBig<R, 2>) -> Result<Self, Self::Error> {
358        value.fbig.try_into()
359    }
360}
361
362impl<R: Round> TryFrom<CachedFBig<R, 2>> for f64 {
363    type Error = ConversionError;
364
365    #[inline]
366    fn try_from(value: CachedFBig<R, 2>) -> Result<Self, Self::Error> {
367        value.fbig.try_into()
368    }
369}
370
371// ---------------------------------------------------------------------------
372// Clone / Default / Debug / comparisons
373// ---------------------------------------------------------------------------
374
375impl<R: Round, const B: Word> Clone for CachedFBig<R, B> {
376    #[inline]
377    fn clone(&self) -> Self {
378        Self {
379            fbig: self.fbig.clone(),
380            cache: Rc::clone(&self.cache),
381        }
382    }
383}
384
385impl<R: Round, const B: Word> Default for CachedFBig<R, B> {
386    /// Default value: 0 with a fresh cache.
387    #[inline]
388    fn default() -> Self {
389        Self::with_cache(Repr::zero(), Context::new(0))
390    }
391}
392
393impl<R: Round, const B: Word> core::fmt::Debug for CachedFBig<R, B> {
394    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
395        f.debug_struct("CachedFBig")
396            .field("repr", &self.fbig.repr)
397            .field("precision", &self.fbig.context.precision)
398            .finish()
399    }
400}
401
402// ---------------------------------------------------------------------------
403// Display / LowerExp / UpperExp / base-specific formatting
404//
405// Each delegates to the inner FBig so the rendered string is identical to FBig.
406// (`Debug` above intentionally keeps the cached-specific struct form.)
407// ---------------------------------------------------------------------------
408
409impl<R: Round, const B: Word> core::fmt::Display for CachedFBig<R, B> {
410    #[inline]
411    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
412        core::fmt::Display::fmt(&self.fbig, f)
413    }
414}
415
416impl<R: Round, const B: Word> core::fmt::LowerExp for CachedFBig<R, B> {
417    #[inline]
418    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
419        core::fmt::LowerExp::fmt(&self.fbig, f)
420    }
421}
422
423impl<R: Round, const B: Word> core::fmt::UpperExp for CachedFBig<R, B> {
424    #[inline]
425    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
426        core::fmt::UpperExp::fmt(&self.fbig, f)
427    }
428}
429
430/// Mirror the base-specific format traits ([`core::fmt::Binary`], [`Octal`](core::fmt::Octal),
431/// [`LowerHex`](core::fmt::LowerHex)/[`UpperHex`](core::fmt::UpperHex)) onto [`CachedFBig`] for
432/// the bases where they
433/// apply, delegating each to the inner [`FBig`]'s impl so the output matches.
434macro_rules! impl_cached_fmt_with_base {
435    ($base:literal, $trait:ident) => {
436        impl<R: Round> core::fmt::$trait for CachedFBig<R, $base> {
437            #[inline]
438            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
439                core::fmt::$trait::fmt(&self.fbig, f)
440            }
441        }
442    };
443}
444impl_cached_fmt_with_base!(2, Binary);
445impl_cached_fmt_with_base!(2, LowerHex);
446impl_cached_fmt_with_base!(2, UpperHex);
447impl_cached_fmt_with_base!(8, Octal);
448impl_cached_fmt_with_base!(16, LowerHex);
449impl_cached_fmt_with_base!(16, UpperHex);
450
451impl<R1: Round, R2: Round, const B: Word> PartialEq<CachedFBig<R2, B>> for CachedFBig<R1, B> {
452    #[inline]
453    fn eq(&self, other: &CachedFBig<R2, B>) -> bool {
454        // value equality, mirroring FBig (compares the representation only).
455        self.fbig.repr == other.fbig.repr
456    }
457}
458
459impl<R: Round, const B: Word> Eq for CachedFBig<R, B> {}
460
461// ---------------------------------------------------------------------------
462// Ordering and the dashu-base ordering/log/sign traits
463// (delegate to the inner FBig — value ordering, context ignored)
464// ---------------------------------------------------------------------------
465
466impl<R1: Round, R2: Round, const B: Word> PartialOrd<CachedFBig<R2, B>> for CachedFBig<R1, B> {
467    #[inline]
468    fn partial_cmp(&self, other: &CachedFBig<R2, B>) -> Option<Ordering> {
469        self.fbig.partial_cmp(&other.fbig)
470    }
471}
472
473impl<R: Round, const B: Word> Ord for CachedFBig<R, B> {
474    #[inline]
475    fn cmp(&self, other: &Self) -> Ordering {
476        self.fbig.cmp(&other.fbig)
477    }
478}
479
480impl<R: Round, const B: Word> AbsOrd for CachedFBig<R, B> {
481    #[inline]
482    fn abs_cmp(&self, other: &Self) -> Ordering {
483        AbsOrd::abs_cmp(&self.fbig, &other.fbig)
484    }
485}
486
487impl<R: Round, const B: Word> AbsOrd<UBig> for CachedFBig<R, B> {
488    #[inline]
489    fn abs_cmp(&self, other: &UBig) -> Ordering {
490        AbsOrd::abs_cmp(&self.fbig, other)
491    }
492}
493impl<R: Round, const B: Word> AbsOrd<CachedFBig<R, B>> for UBig {
494    #[inline]
495    fn abs_cmp(&self, other: &CachedFBig<R, B>) -> Ordering {
496        AbsOrd::abs_cmp(self, &other.fbig)
497    }
498}
499impl<R: Round, const B: Word> AbsOrd<IBig> for CachedFBig<R, B> {
500    #[inline]
501    fn abs_cmp(&self, other: &IBig) -> Ordering {
502        AbsOrd::abs_cmp(&self.fbig, other)
503    }
504}
505impl<R: Round, const B: Word> AbsOrd<CachedFBig<R, B>> for IBig {
506    #[inline]
507    fn abs_cmp(&self, other: &CachedFBig<R, B>) -> Ordering {
508        AbsOrd::abs_cmp(self, &other.fbig)
509    }
510}
511
512impl<R: Round, const B: Word> Signed for CachedFBig<R, B> {
513    #[inline]
514    fn sign(&self) -> Sign {
515        self.fbig.sign()
516    }
517}
518
519impl<R: Round, const B: Word> EstimatedLog2 for CachedFBig<R, B> {
520    #[inline]
521    fn log2_bounds(&self) -> (f32, f32) {
522        EstimatedLog2::log2_bounds(&self.fbig)
523    }
524
525    #[inline]
526    fn log2_est(&self) -> f32 {
527        EstimatedLog2::log2_est(&self.fbig)
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534    use crate::round::mode;
535    use alloc::format;
536
537    fn handle() -> Rc<RefCell<ConstCache>> {
538        Rc::new(RefCell::new(ConstCache::new()))
539    }
540
541    /// An `FBig` with value `n` at the given precision (so inexact results match the
542    /// `CachedFBig` operands built at the same precision).
543    fn fbig(n: i32, prec: usize) -> FBig<mode::HalfAway, 10> {
544        FBig::from_repr(Repr::new(n.into(), 0), Context::new(prec))
545    }
546
547    #[test]
548    fn test_pi_matches_fbig() {
549        for &precision in &[10usize, 50, 100] {
550            let h = handle();
551            let cached = CachedFBig::<mode::HalfAway, 10>::pi(precision, &h).into_fbig();
552            let direct = FBig::<mode::HalfAway, 10>::pi(precision);
553            assert_eq!(cached, direct, "pi mismatch at precision {precision}");
554        }
555    }
556
557    #[test]
558    fn test_transcendentals_match_fbig() {
559        let x = CachedFBig::<mode::HalfAway, 10>::with_cache(
560            Repr::new(1234.into(), -3), // 1.234
561            Context::new(50),
562        );
563        let y = FBig::<mode::HalfAway, 10>::from_repr(Repr::new(1234.into(), -3), Context::new(50));
564
565        assert_eq!(x.clone().ln().into_fbig(), y.clone().ln());
566        assert_eq!(x.clone().exp().into_fbig(), y.clone().exp());
567        assert_eq!(x.clone().sin().into_fbig(), y.clone().sin());
568        assert_eq!(x.clone().cos().into_fbig(), y.clone().cos());
569        assert_eq!(x.clone().exp_m1().into_fbig(), y.clone().exp_m1());
570        assert_eq!(x.clone().ln_1p().into_fbig(), y.clone().ln_1p());
571        assert_eq!(x.clone().log2().into_fbig(), y.clone().log2());
572        assert_eq!(x.clone().log10().into_fbig(), y.clone().log10());
573        assert_eq!(x.powf(&x.clone()).into_fbig(), y.clone().powf(&y));
574    }
575
576    #[test]
577    fn test_cache_extension_matches_scratch() {
578        // Extending π 100 -> 1000 through one shared handle must equal a from-scratch compute.
579        let h = handle();
580        let _pi_100 = CachedFBig::<mode::HalfAway, 10>::pi(100, &h);
581        let pi_1000 = CachedFBig::<mode::HalfAway, 10>::pi(1000, &h).into_fbig();
582        let direct = Context::<mode::HalfAway>::new(1000).pi::<10>(None).value();
583        assert_eq!(pi_1000, direct);
584    }
585
586    #[test]
587    fn test_cache_survives_arithmetic() {
588        // a and b share one cache handle; the sum must keep it so the subsequent
589        // ln() reuses the same shared cache.
590        let h = handle();
591        let a = CachedFBig::<mode::HalfAway, 10>::from_repr(
592            Repr::new(2.into(), 0),
593            Context::new(30),
594            h.clone(),
595        );
596        let b = CachedFBig::<mode::HalfAway, 10>::from_repr(
597            Repr::new(3.into(), 0),
598            Context::new(30),
599            h.clone(),
600        );
601        let sum_ln = (a.clone() + b.clone()).ln().into_fbig();
602        let expected = (fbig(2, 30) + fbig(3, 30)).ln();
603        assert_eq!(sum_ln, expected);
604    }
605
606    #[test]
607    fn test_arithmetic_matches_fbig() {
608        let a =
609            CachedFBig::<mode::HalfAway, 10>::with_cache(Repr::new(2.into(), 0), Context::new(20));
610        let b =
611            CachedFBig::<mode::HalfAway, 10>::with_cache(Repr::new(3.into(), 0), Context::new(20));
612
613        assert_eq!((a.clone() + b.clone()).into_fbig(), fbig(2, 20) + fbig(3, 20));
614        assert_eq!((a.clone() - b.clone()).into_fbig(), fbig(2, 20) - fbig(3, 20));
615        assert_eq!((a.clone() * b.clone()).into_fbig(), fbig(2, 20) * fbig(3, 20));
616        assert_eq!((a.clone() / b.clone()).into_fbig(), fbig(2, 20) / fbig(3, 20));
617    }
618
619    #[test]
620    fn test_debug_compiles() {
621        let x = CachedFBig::<mode::HalfAway, 10>::with_cache(
622            Repr::new(1234.into(), -3),
623            Context::new(50),
624        );
625        let s = format!("{:?}", x);
626        assert!(s.contains("CachedFBig"));
627    }
628
629    #[test]
630    fn test_arithmetic_with_fbig() {
631        let a =
632            CachedFBig::<mode::HalfAway, 10>::with_cache(Repr::new(2.into(), 0), Context::new(20));
633        let b = fbig(3, 20);
634
635        // CachedFBig op FBig — cache preserved (LHS)
636        let c = a.clone() + b.clone();
637        assert_eq!(c.into_fbig(), fbig(2, 20) + fbig(3, 20));
638
639        // FBig op CachedFBig — cache preserved (RHS)
640        let d = b.clone() + a.clone();
641        assert_eq!(d.into_fbig(), fbig(3, 20) + fbig(2, 20));
642
643        // Sub, Mul, Div
644        assert_eq!((a.clone() - b.clone()).into_fbig(), fbig(2, 20) - fbig(3, 20));
645        assert_eq!((a.clone() * b.clone()).into_fbig(), fbig(2, 20) * fbig(3, 20));
646        assert_eq!((a.clone() / b.clone()).into_fbig(), fbig(2, 20) / fbig(3, 20));
647    }
648
649    #[test]
650    fn test_arithmetic_with_primitives() {
651        let a =
652            CachedFBig::<mode::HalfAway, 10>::with_cache(Repr::new(2.into(), 0), Context::new(20));
653
654        // CachedFBig op primitive
655        assert_eq!((a.clone() + 3u8).into_fbig(), fbig(2, 20) + 3u8);
656        assert_eq!((a.clone() - 3i32).into_fbig(), fbig(2, 20) - 3i32);
657        assert_eq!((a.clone() * 4u64).into_fbig(), fbig(2, 20) * 4u64);
658
659        // Primitive op CachedFBig
660        assert_eq!((3u8 + a.clone()).into_fbig(), 3u8 + fbig(2, 20));
661        assert_eq!((10i32 - a.clone()).into_fbig(), 10i32 - fbig(2, 20));
662    }
663
664    #[test]
665    fn test_cache_size() {
666        let x = CachedFBig::<mode::HalfAway, 10>::with_cache(
667            Repr::new(1234.into(), -3),
668            Context::new(50),
669        );
670        let _ = x.ln();
671        // After computing ln(1.234), the cache should have some state
672        assert!(x.cache().total_terms() > 0);
673        assert!(x.cache().total_words() > 0);
674    }
675
676    #[test]
677    fn test_cache_clear() {
678        let x = CachedFBig::<mode::HalfAway, 10>::with_cache(
679            Repr::new(1234.into(), -3),
680            Context::new(50),
681        );
682        let before_clear = x.ln().into_fbig();
683        assert!(x.cache().total_terms() > 0);
684
685        x.clear_cache();
686        assert_eq!(x.cache().total_terms(), 0);
687        assert_eq!(x.cache().total_words(), 0);
688
689        // After clearing, recomputation still produces the same result
690        let after_clear = x.ln().into_fbig();
691        assert_eq!(after_clear, before_clear);
692    }
693
694    #[test]
695    fn test_mirrored_methods_match_fbig() {
696        let cached = CachedFBig::<mode::HalfAway, 10>::with_cache(
697            Repr::new(1234.into(), -3), // 1.234
698            Context::new(50),
699        );
700        let plain = cached.as_fbig().clone();
701
702        // rounding / decomposition
703        assert_eq!(cached.round().into_fbig(), plain.round());
704        assert_eq!(cached.trunc().into_fbig(), plain.trunc());
705        assert_eq!(cached.ceil().into_fbig(), plain.ceil());
706        assert_eq!(cached.floor().into_fbig(), plain.floor());
707        assert_eq!(cached.fract().into_fbig(), plain.fract());
708        assert_eq!(cached.signum().into_fbig(), plain.signum());
709        assert_eq!(cached.ulp_lb().into_fbig(), plain.ulp_lb());
710
711        let (ci, cf) = cached.clone().split_at_point();
712        let (pi, pf) = plain.clone().split_at_point();
713        assert_eq!((ci.into_fbig(), cf.into_fbig()), (pi, pf));
714
715        // roots / norms
716        assert_eq!(cached.nth_root(2).into_fbig(), plain.nth_root(2));
717        let other = CachedFBig::<mode::HalfAway, 10>::with_cache(
718            Repr::new(300.into(), -2), // 3.00
719            Context::new(50),
720        );
721        assert_eq!(cached.hypot(&other).into_fbig(), plain.hypot(other.as_fbig()));
722
723        // base conversion / quantize (types change, values agree)
724        assert_eq!(cached.quantize(-1).value().into_fbig(), plain.quantize(-1).value());
725        assert_eq!(cached.to_decimal().value().into_fbig(), plain.to_decimal().value());
726        assert_eq!(cached.to_binary().value().into_fbig(), plain.to_binary().value());
727        assert_eq!(
728            cached.clone().with_base::<2>().value().into_fbig(),
729            plain.clone().with_base::<2>().value()
730        );
731        assert_eq!(
732            cached
733                .clone()
734                .with_base_and_precision::<2>(40)
735                .value()
736                .into_fbig(),
737            plain.clone().with_base_and_precision::<2>(40).value()
738        );
739    }
740}