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}