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 oxinum_float::precision::with_precision;
28use oxinum_float::DBig;
29
30/// Build an *exact* [`DBig`] from a signed integer.
31///
32/// `DBig::from(n)` retains only the single significant digit it needs to
33/// render `n`, which causes later `DBig` arithmetic to round back to that
34/// precision. Rebinding to precision `0` (`dashu-float`'s "unlimited") makes
35/// the value carry no precision cap, so products and sums involving it stay
36/// exact across the whole `i64` range (and beyond).
37#[inline]
38fn exact_dbig(n: i64) -> DBig {
39    with_precision(&DBig::from(n), 0)
40}
41
42/// Build a complex number from an explicit `(re, im)` pair of [`DBig`] values.
43impl From<(DBig, DBig)> for CBig {
44    fn from((re, im): (DBig, DBig)) -> Self {
45        CBig::from_parts(re, im)
46    }
47}
48
49/// Embed a real [`DBig`] on the real axis (`im = 0`).
50impl From<DBig> for CBig {
51    fn from(re: DBig) -> Self {
52        CBig::from_real(re)
53    }
54}
55
56/// Embed a borrowed real [`DBig`] on the real axis (`im = 0`).
57impl From<&DBig> for CBig {
58    fn from(re: &DBig) -> Self {
59        CBig::from_real(re.clone())
60    }
61}
62
63/// Build a complex number from an integer `(re, im)` pair (convenience).
64///
65/// Both parts are represented **exactly** (at unlimited `DBig` precision), so
66/// the result keeps full precision through later arithmetic — e.g.
67/// `CBig::from((3, 4)).norm_sqr()` is the exact `25`. See the module-level
68/// "Integer conversions are exact" note for the rationale.
69impl From<(i64, i64)> for CBig {
70    fn from((re, im): (i64, i64)) -> Self {
71        CBig::from_parts(exact_dbig(re), exact_dbig(im))
72    }
73}
74
75/// Embed an integer on the real axis (`im = 0`).
76///
77/// The real part is represented **exactly** (at unlimited `DBig` precision),
78/// so the value keeps full precision through later arithmetic. See the
79/// module-level "Integer conversions are exact" note for the rationale.
80impl From<i64> for CBig {
81    fn from(re: i64) -> Self {
82        CBig::from_real(exact_dbig(re))
83    }
84}
85
86impl CBig {
87    /// Project both components down to `f64`, returning `(re, im)`.
88    ///
89    /// # Precision
90    ///
91    /// This conversion is **lossy**: each arbitrary-precision [`DBig`]
92    /// component is rounded to the nearest `f64`. Values whose magnitude
93    /// exceeds [`f64::MAX`] saturate to `±∞`, and digits beyond the 53-bit
94    /// mantissa are discarded. Use it only when an ordinary floating-point
95    /// approximation is acceptable.
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use oxinum_complex::CBig;
101    /// let z = CBig::from_f64(3.5, -1.25).expect("finite parts");
102    /// assert_eq!(z.to_f64_parts(), (3.5, -1.25));
103    /// ```
104    pub fn to_f64_parts(&self) -> (f64, f64) {
105        (self.re.to_f64().value(), self.im.to_f64().value())
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn from_dbig_pair() {
115        let re = DBig::from(7);
116        let im = DBig::from(-4);
117        let z: CBig = (re, im).into();
118        assert_eq!(z.re().to_string(), "7");
119        assert_eq!(z.im().to_string(), "-4");
120    }
121
122    #[test]
123    fn from_dbig_lands_on_real_axis() {
124        let d = DBig::from(5);
125        let z: CBig = d.into();
126        assert_eq!(z.re().to_string(), "5");
127        assert_eq!(z.im().to_string(), "0");
128        assert!(z.is_real());
129    }
130
131    #[test]
132    fn from_dbig_ref_lands_on_real_axis() {
133        let d = DBig::from(9);
134        let z: CBig = (&d).into();
135        assert_eq!(z.re().to_string(), "9");
136        assert_eq!(z.im().to_string(), "0");
137        // Source `DBig` is untouched (borrowed, not moved).
138        assert_eq!(d.to_string(), "9");
139    }
140
141    #[test]
142    fn from_integer_pair() {
143        let z: CBig = (1i64, 2i64).into();
144        assert_eq!(z.re().to_string(), "1");
145        assert_eq!(z.im().to_string(), "2");
146    }
147
148    #[test]
149    fn from_integer_lands_on_real_axis() {
150        let z: CBig = 42i64.into();
151        assert_eq!(z.re().to_string(), "42");
152        assert_eq!(z.im().to_string(), "0");
153        assert!(z.is_real());
154    }
155
156    #[test]
157    fn to_f64_parts_round_trips() {
158        let z = CBig::from_f64(3.5, -1.25).expect("finite parts");
159        assert_eq!(z.to_f64_parts(), (3.5, -1.25));
160    }
161
162    // ---- Regression: integer conversions must be EXACT --------------------
163    //
164    // Before the fix, `DBig::from(n)` kept only one significant digit and
165    // `DBig` arithmetic rounded back to that precision, so integer-built
166    // `CBig` values silently collapsed precision under multiplication
167    // (`from((3, 4)).norm_sqr()` returned ~30 instead of 25).
168
169    #[test]
170    fn integer_parts_carry_unlimited_precision() {
171        // Precision 0 is `dashu-float`'s "unlimited" — the marker that makes
172        // subsequent products/sums exact.
173        let z: CBig = (3i64, 4i64).into();
174        assert_eq!(
175            z.re().precision(),
176            0,
177            "real part must be unlimited-precision"
178        );
179        assert_eq!(
180            z.im().precision(),
181            0,
182            "imag part must be unlimited-precision"
183        );
184
185        let r: CBig = 7i64.into();
186        assert_eq!(
187            r.re().precision(),
188            0,
189            "real-axis part must be unlimited-precision"
190        );
191        assert_eq!(r.im().to_string(), "0");
192    }
193
194    #[test]
195    fn integer_norm_sqr_is_exact() {
196        // |3 + 4i|² = 9 + 16 = 25, exactly (the headline footgun).
197        let z: CBig = (3i64, 4i64).into();
198        assert_eq!(z.norm_sqr().to_string(), "25");
199    }
200
201    #[test]
202    fn integer_product_is_exact() {
203        // (1 + 2i)(3 + 4i) = (3 − 8) + (4 + 6)i = -5 + 10i, exactly.
204        let prod = CBig::from((1i64, 2i64)) * CBig::from((3i64, 4i64));
205        assert_eq!(prod.re().to_string(), "-5");
206        assert_eq!(prod.im().to_string(), "10");
207    }
208
209    #[test]
210    fn integer_large_magnitude_norm_sqr_is_exact() {
211        // 1_000_000_007² = 1_000_000_014_000_000_049 — far more than a single
212        // significant digit, so this fails loudly if precision collapses.
213        let z: CBig = (1_000_000_007i64, 0i64).into();
214        assert_eq!(z.norm_sqr().to_string(), "1000000014000000049");
215    }
216
217    #[test]
218    fn integer_i64_max_norm_sqr_is_exact() {
219        // i64::MAX = 9_223_372_036_854_775_807; its square is 39 digits and
220        // must be represented exactly under unlimited precision.
221        let z: CBig = (i64::MAX, 0i64).into();
222        assert_eq!(
223            z.norm_sqr().to_string(),
224            "85070591730234615847396907784232501249"
225        );
226    }
227}