Skip to main content

oxinum_complex/
convert.rs

1//! Conversions between [`CBig`] and ordinary Rust / `oxinum-float` scalars.
2//!
3//! These `From` impls let callers build a complex number from a single real
4//! component (placed on the real axis) or from an explicit `(re, im)` pair,
5//! using either [`DBig`] or plain integers. The lossy [`CBig::to_f64_parts`]
6//! escape hatch projects both components down to `f64`.
7//!
8//! # Integer conversions are exact
9//!
10//! `dashu-float`'s default `DBig::from(n: i64)` carries only the *one*
11//! significant decimal digit needed to print `n`, and `DBig` arithmetic
12//! rounds each result back to its operands' precision. Building both parts
13//! that way would make any later multiplication collapse precision — e.g.
14//! `CBig::from((3, 4)).norm_sqr()` would round `9 + 16` to a single digit
15//! and yield `30` rather than the exact `25`.
16//!
17//! To avoid that footgun, the integer `From` impls below rebind each part to
18//! `dashu-float`'s **unlimited** precision (precision `0`) via
19//! [`oxinum_float::precision::with_precision`]. At unlimited precision every
20//! `finite × finite` and `finite ± finite` operation is *exact*, so an
21//! integer-constructed `CBig` keeps full precision through subsequent
22//! `norm_sqr`, multiplication, and `pow`. The [`DBig`]-based conversions
23//! ([`From<(DBig, DBig)>`], [`From<DBig>`], [`From<&DBig>`]) pass their inputs
24//! through unchanged and so already carry whatever precision the caller chose.
25
26use crate::CBig;
27use core::convert::TryFrom;
28use oxinum_core::{OxiNumError, OxiNumResult};
29use oxinum_float::precision::with_precision;
30use oxinum_float::DBig;
31
32/// Build an *exact* [`DBig`] from a signed integer.
33///
34/// `DBig::from(n)` retains only the single significant digit it needs to
35/// render `n`, which causes later `DBig` arithmetic to round back to that
36/// precision. Rebinding to precision `0` (`dashu-float`'s "unlimited") makes
37/// the value carry no precision cap, so products and sums involving it stay
38/// exact across the whole `i64` range (and beyond).
39#[inline]
40fn exact_dbig(n: i64) -> DBig {
41    with_precision(&DBig::from(n), 0)
42}
43
44/// Build a complex number from an explicit `(re, im)` pair of [`DBig`] values.
45impl From<(DBig, DBig)> for CBig {
46    fn from((re, im): (DBig, DBig)) -> Self {
47        CBig::from_parts(re, im)
48    }
49}
50
51/// Embed a real [`DBig`] on the real axis (`im = 0`).
52impl From<DBig> for CBig {
53    fn from(re: DBig) -> Self {
54        CBig::from_real(re)
55    }
56}
57
58/// Embed a borrowed real [`DBig`] on the real axis (`im = 0`).
59impl From<&DBig> for CBig {
60    fn from(re: &DBig) -> Self {
61        CBig::from_real(re.clone())
62    }
63}
64
65/// Build a complex number from an integer `(re, im)` pair (convenience).
66///
67/// Both parts are represented **exactly** (at unlimited `DBig` precision), so
68/// the result keeps full precision through later arithmetic — e.g.
69/// `CBig::from((3, 4)).norm_sqr()` is the exact `25`. See the module-level
70/// "Integer conversions are exact" note for the rationale.
71impl From<(i64, i64)> for CBig {
72    fn from((re, im): (i64, i64)) -> Self {
73        CBig::from_parts(exact_dbig(re), exact_dbig(im))
74    }
75}
76
77/// Embed an integer on the real axis (`im = 0`).
78///
79/// The real part is represented **exactly** (at unlimited `DBig` precision),
80/// so the value keeps full precision through later arithmetic. See the
81/// module-level "Integer conversions are exact" note for the rationale.
82impl From<i64> for CBig {
83    fn from(re: i64) -> Self {
84        CBig::from_real(exact_dbig(re))
85    }
86}
87
88impl CBig {
89    /// Project both components down to `f64`, returning `(re, im)`.
90    ///
91    /// # Precision
92    ///
93    /// This conversion is **lossy**: each arbitrary-precision [`DBig`]
94    /// component is rounded to the nearest `f64`. Values whose magnitude
95    /// exceeds [`f64::MAX`] saturate to `±∞`, and digits beyond the 53-bit
96    /// mantissa are discarded. Use it only when an ordinary floating-point
97    /// approximation is acceptable.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use oxinum_complex::CBig;
103    /// let z = CBig::from_f64(3.5, -1.25).expect("finite parts");
104    /// assert_eq!(z.to_f64_parts(), (3.5, -1.25));
105    /// ```
106    pub fn to_f64_parts(&self) -> (f64, f64) {
107        (self.re.to_f64().value(), self.im.to_f64().value())
108    }
109}
110
111/// Try to project both components to `f64`, returning an error if either
112/// component is non-finite (infinite or NaN) after the conversion.
113///
114/// # Errors
115///
116/// Returns [`OxiNumError::Overflow`] when either the real or imaginary part
117/// exceeds [`f64::MAX`] in magnitude (i.e. the result would be ±∞ or NaN).
118impl TryFrom<&CBig> for (f64, f64) {
119    type Error = OxiNumError;
120
121    fn try_from(z: &CBig) -> OxiNumResult<(f64, f64)> {
122        let (re, im) = z.to_f64_parts();
123        if !re.is_finite() || !im.is_finite() {
124            return Err(OxiNumError::Overflow(
125                "component is non-finite (infinite or NaN) after f64 projection".into(),
126            ));
127        }
128        Ok((re, im))
129    }
130}
131
132// ---------------------------------------------------------------------------
133// Optional num-complex interop
134// ---------------------------------------------------------------------------
135
136#[cfg(feature = "num-complex")]
137mod num_complex_impls {
138    use super::exact_dbig;
139    use crate::CBig;
140    use num_complex::Complex;
141
142    /// Build a [`CBig`] from a `num_complex::Complex<f64>`.
143    ///
144    /// # Panics
145    ///
146    /// Panics if either component is non-finite (NaN or ±∞).
147    /// For a fallible conversion, use `CBig::from_f64(z.re, z.im)` directly.
148    impl From<Complex<f64>> for CBig {
149        fn from(z: Complex<f64>) -> Self {
150            CBig::from_f64(z.re, z.im)
151                .expect("Complex<f64> → CBig: components must be finite (no NaN/Inf)")
152        }
153    }
154
155    /// Build a [`CBig`] from a `num_complex::Complex<i64>` — both parts are exact.
156    ///
157    /// Each integer component is stored at unlimited `DBig` precision, so all
158    /// subsequent arithmetic (e.g. `norm_sqr`, `pow`) keeps full precision.
159    impl From<Complex<i64>> for CBig {
160        fn from(z: Complex<i64>) -> Self {
161            CBig::from_parts(exact_dbig(z.re), exact_dbig(z.im))
162        }
163    }
164
165    /// Convert to `num_complex::Complex<f64>` (lossy — precision is truncated
166    /// to 53 bits per component).
167    impl From<&CBig> for Complex<f64> {
168        fn from(z: &CBig) -> Self {
169            let (re, im) = z.to_f64_parts();
170            Complex::new(re, im)
171        }
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn from_dbig_pair() {
181        let re = DBig::from(7);
182        let im = DBig::from(-4);
183        let z: CBig = (re, im).into();
184        assert_eq!(z.re().to_string(), "7");
185        assert_eq!(z.im().to_string(), "-4");
186    }
187
188    #[test]
189    fn from_dbig_lands_on_real_axis() {
190        let d = DBig::from(5);
191        let z: CBig = d.into();
192        assert_eq!(z.re().to_string(), "5");
193        assert_eq!(z.im().to_string(), "0");
194        assert!(z.is_real());
195    }
196
197    #[test]
198    fn from_dbig_ref_lands_on_real_axis() {
199        let d = DBig::from(9);
200        let z: CBig = (&d).into();
201        assert_eq!(z.re().to_string(), "9");
202        assert_eq!(z.im().to_string(), "0");
203        // Source `DBig` is untouched (borrowed, not moved).
204        assert_eq!(d.to_string(), "9");
205    }
206
207    #[test]
208    fn from_integer_pair() {
209        let z: CBig = (1i64, 2i64).into();
210        assert_eq!(z.re().to_string(), "1");
211        assert_eq!(z.im().to_string(), "2");
212    }
213
214    #[test]
215    fn from_integer_lands_on_real_axis() {
216        let z: CBig = 42i64.into();
217        assert_eq!(z.re().to_string(), "42");
218        assert_eq!(z.im().to_string(), "0");
219        assert!(z.is_real());
220    }
221
222    #[test]
223    fn to_f64_parts_round_trips() {
224        let z = CBig::from_f64(3.5, -1.25).expect("finite parts");
225        assert_eq!(z.to_f64_parts(), (3.5, -1.25));
226    }
227
228    // ---- Regression: integer conversions must be EXACT --------------------
229    //
230    // Before the fix, `DBig::from(n)` kept only one significant digit and
231    // `DBig` arithmetic rounded back to that precision, so integer-built
232    // `CBig` values silently collapsed precision under multiplication
233    // (`from((3, 4)).norm_sqr()` returned ~30 instead of 25).
234
235    #[test]
236    fn integer_parts_carry_unlimited_precision() {
237        // Precision 0 is `dashu-float`'s "unlimited" — the marker that makes
238        // subsequent products/sums exact.
239        let z: CBig = (3i64, 4i64).into();
240        assert_eq!(
241            z.re().precision(),
242            0,
243            "real part must be unlimited-precision"
244        );
245        assert_eq!(
246            z.im().precision(),
247            0,
248            "imag part must be unlimited-precision"
249        );
250
251        let r: CBig = 7i64.into();
252        assert_eq!(
253            r.re().precision(),
254            0,
255            "real-axis part must be unlimited-precision"
256        );
257        assert_eq!(r.im().to_string(), "0");
258    }
259
260    #[test]
261    fn integer_norm_sqr_is_exact() {
262        // |3 + 4i|² = 9 + 16 = 25, exactly (the headline footgun).
263        let z: CBig = (3i64, 4i64).into();
264        assert_eq!(z.norm_sqr().to_string(), "25");
265    }
266
267    #[test]
268    fn integer_product_is_exact() {
269        // (1 + 2i)(3 + 4i) = (3 − 8) + (4 + 6)i = -5 + 10i, exactly.
270        let prod = CBig::from((1i64, 2i64)) * CBig::from((3i64, 4i64));
271        assert_eq!(prod.re().to_string(), "-5");
272        assert_eq!(prod.im().to_string(), "10");
273    }
274
275    #[test]
276    fn integer_large_magnitude_norm_sqr_is_exact() {
277        // 1_000_000_007² = 1_000_000_014_000_000_049 — far more than a single
278        // significant digit, so this fails loudly if precision collapses.
279        let z: CBig = (1_000_000_007i64, 0i64).into();
280        assert_eq!(z.norm_sqr().to_string(), "1000000014000000049");
281    }
282
283    #[test]
284    fn integer_i64_max_norm_sqr_is_exact() {
285        // i64::MAX = 9_223_372_036_854_775_807; its square is 39 digits and
286        // must be represented exactly under unlimited precision.
287        let z: CBig = (i64::MAX, 0i64).into();
288        assert_eq!(
289            z.norm_sqr().to_string(),
290            "85070591730234615847396907784232501249"
291        );
292    }
293
294    // ---- Item 5: TryFrom<&CBig> for (f64, f64) ----------------------------
295
296    #[test]
297    fn try_from_finite_ok() {
298        let z = CBig::from_f64(1.5, -2.25).expect("finite");
299        let r: (f64, f64) = (&z).try_into().expect("should succeed");
300        assert!((r.0 - 1.5).abs() < 1e-12);
301        assert!((r.1 + 2.25).abs() < 1e-12);
302    }
303
304    #[test]
305    fn try_from_zero_ok() {
306        let z = CBig::zero();
307        let r: (f64, f64) = (&z).try_into().expect("zero should succeed");
308        assert_eq!(r, (0.0, 0.0));
309    }
310
311    #[test]
312    fn try_from_overflow_err() {
313        // Build a CBig with a huge real part that overflows f64 using a
314        // bounded-precision DBig so that to_f64() does not panic.
315        // f64::MAX ≈ 1.8e308; 1e400 is safely beyond that.
316        const PREC: usize = 64; // enough decimal digits to represent 1e400
317        let base = with_precision(&DBig::from(10i64), PREC);
318        let mut acc = with_precision(&DBig::from(1i64), PREC);
319        for _ in 0..400 {
320            acc = with_precision(&(&acc * &base), PREC);
321        }
322        let huge = CBig::from_parts(acc, DBig::from(0i64));
323        let r = <(f64, f64)>::try_from(&huge);
324        assert!(r.is_err(), "should fail on overflow: {r:?}");
325    }
326
327    // ---- Item 3: num-complex feature bridge tests -------------------------
328
329    #[cfg(feature = "num-complex")]
330    mod num_complex_tests {
331        use super::*;
332        use num_complex::Complex;
333
334        #[test]
335        fn from_complex_f64_round_trip() {
336            let nc = Complex::new(1.5f64, -2.25f64);
337            let z = CBig::from(nc);
338            let back = Complex::<f64>::from(&z);
339            assert!((back.re - 1.5).abs() < 1e-12);
340            assert!((back.im + 2.25).abs() < 1e-12);
341        }
342
343        #[test]
344        fn from_complex_i64_is_exact() {
345            let nc = Complex::new(3i64, 4i64);
346            let z = CBig::from(nc);
347            assert_eq!(z.norm_sqr().to_string(), "25");
348        }
349
350        #[test]
351        fn from_complex_i64_large_is_exact() {
352            // (i64::MAX + 0i) — should preserve full precision.
353            let nc = Complex::new(i64::MAX, 1i64);
354            let z = CBig::from(nc);
355            assert_eq!(z.re().to_string(), i64::MAX.to_string());
356            assert_eq!(z.im().to_string(), "1");
357        }
358
359        #[test]
360        fn from_cbig_ref_to_complex_f64() {
361            let z = CBig::from_f64(3.0, -4.0).expect("finite");
362            let nc = Complex::<f64>::from(&z);
363            assert_eq!(nc.re, 3.0);
364            assert_eq!(nc.im, -4.0);
365        }
366
367        #[test]
368        fn from_complex_i64_norm_sqr_exact() {
369            // |5 + 12i|² = 25 + 144 = 169 = 13²
370            let nc = Complex::new(5i64, 12i64);
371            let z = CBig::from(nc);
372            assert_eq!(z.norm_sqr().to_string(), "169");
373        }
374    }
375}