oxinum_complex/ops.rs
1//! Arithmetic operator implementations for [`crate::CBig`].
2//!
3//! Every binary operator (`Add`, `Sub`, `Mul`, `Div`) provides all four
4//! ownership variants — `&T op &T`, `T op T`, `T op &T`, `&T op T` — each
5//! producing an owned [`CBig`]. The matching `*Assign` traits are wired up for
6//! both owned and borrowed right-hand sides, and [`Neg`] is provided for owned
7//! and borrowed receivers.
8//!
9//! # Complex arithmetic
10//!
11//! With `a = a_re + a_im·i` and `b = b_re + b_im·i`:
12//!
13//! ```text
14//! a + b = (a_re + b_re) + (a_im + b_im)·i
15//! a − b = (a_re − b_re) + (a_im − b_im)·i
16//! a · b = (a_re·b_re − a_im·b_im) + (a_re·b_im + a_im·b_re)·i
17//! a / b = (a · conj(b)) / |b|²
18//! = (a_re·b_re + a_im·b_im)/|b|² + (a_im·b_re − a_re·b_im)/|b|²·i
19//! ```
20//!
21//! # Zero-divisor policy
22//!
23//! Mirroring every sibling operator in this workspace (e.g.
24//! `oxinum_rational::native::BigRational`, whose `Div` panics on a zero
25//! divisor while `recip`/constructors return [`OxiNumError::DivByZero`]):
26//!
27//! * the [`Div`] **operator** panics on a zero divisor. The panic originates
28//! inside `dashu-float`'s `DBig` `/`, which documents that "division by zero
29//! … panic[s] instead of returning infinities" — so this file contains no
30//! explicit `unwrap`/`expect`/`panic!`;
31//! * the no-panic [`CBig::checked_div`] returns [`OxiNumError::DivByZero`]
32//! instead.
33
34use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
35
36use oxinum_core::{OxiNumError, OxiNumResult};
37
38use crate::CBig;
39
40// ---------------------------------------------------------------------------
41// Internal core helpers
42// ---------------------------------------------------------------------------
43
44/// Core `Add` body shared by all four ownership variants: component-wise.
45#[inline]
46fn add_core(a: &CBig, b: &CBig) -> CBig {
47 CBig {
48 re: &a.re + &b.re,
49 im: &a.im + &b.im,
50 }
51}
52
53/// Core `Sub` body shared by all four ownership variants: component-wise.
54#[inline]
55fn sub_core(a: &CBig, b: &CBig) -> CBig {
56 CBig {
57 re: &a.re - &b.re,
58 im: &a.im - &b.im,
59 }
60}
61
62/// Core `Mul` body: `(a_re·b_re − a_im·b_im) + (a_re·b_im + a_im·b_re)·i`.
63#[inline]
64fn mul_core(a: &CBig, b: &CBig) -> CBig {
65 let re = (&a.re * &b.re) - (&a.im * &b.im);
66 let im = (&a.re * &b.im) + (&a.im * &b.re);
67 CBig { re, im }
68}
69
70/// The unreduced numerators of `a / b`, i.e. the real and imaginary parts of
71/// `a · conj(b)`. The actual quotient divides each by `|b|²`.
72///
73/// Returned as `(num_re, num_im)` where
74/// `num_re = a_re·b_re + a_im·b_im` and `num_im = a_im·b_re − a_re·b_im`.
75/// Shared by [`div_core`] (which adds the explicit zero check) and the
76/// [`Div`] operator (which lets `DBig`'s `/` panic on a zero denominator).
77#[inline]
78fn div_numerators(a: &CBig, b: &CBig) -> (crate::DBig, crate::DBig) {
79 let num_re = (&a.re * &b.re) + (&a.im * &b.im);
80 let num_im = (&a.im * &b.re) - (&a.re * &b.im);
81 (num_re, num_im)
82}
83
84/// Core `Div` body with an explicit zero-divisor guard.
85///
86/// Computes `denom = |b|²`; if `b` is zero returns
87/// [`OxiNumError::DivByZero`]. Otherwise `denom` is guaranteed non-zero, so
88/// the two `DBig` divisions cannot panic.
89fn div_core(a: &CBig, b: &CBig) -> OxiNumResult<CBig> {
90 let denom = b.norm_sqr();
91 if b.is_zero() {
92 return Err(OxiNumError::DivByZero);
93 }
94 let (num_re, num_im) = div_numerators(a, b);
95 Ok(CBig {
96 re: &num_re / &denom,
97 im: &num_im / &denom,
98 })
99}
100
101/// Core `Div` body for the operator path: no early zero check.
102///
103/// When `b` is zero, `denom = |b|² = 0` and the `DBig` `/` panics, exactly as
104/// the sibling `BigRational` `/` operator does. This function therefore
105/// contains no explicit `unwrap`/`expect`/`panic!`.
106#[inline]
107fn div_op_core(a: &CBig, b: &CBig) -> CBig {
108 let denom = b.norm_sqr();
109 let (num_re, num_im) = div_numerators(a, b);
110 CBig {
111 re: &num_re / &denom,
112 im: &num_im / &denom,
113 }
114}
115
116// ---------------------------------------------------------------------------
117// Public no-panic division
118// ---------------------------------------------------------------------------
119
120impl CBig {
121 /// Divides `self` by `rhs` without panicking.
122 ///
123 /// Returns [`OxiNumError::DivByZero`] when `rhs` is zero; otherwise yields
124 /// `self / rhs` computed as `(self · conj(rhs)) / |rhs|²`.
125 ///
126 /// This is the panic-free counterpart of the [`Div`] operator (which
127 /// panics on a zero divisor, matching the rest of the workspace) and is
128 /// the entry point sibling routines such as complex `tan`/`tanh` use.
129 ///
130 /// # Examples
131 ///
132 /// ```
133 /// use oxinum_complex::CBig;
134 ///
135 /// let a = CBig::from_f64(1.0, 1.0).expect("finite parts");
136 /// let q = a.checked_div(&a).expect("non-zero divisor");
137 /// assert_eq!(q.re().to_string(), "1");
138 /// assert_eq!(q.im().to_string(), "0");
139 ///
140 /// assert!(a.checked_div(&CBig::zero()).is_err());
141 /// ```
142 pub fn checked_div(&self, rhs: &CBig) -> OxiNumResult<CBig> {
143 div_core(self, rhs)
144 }
145}
146
147// ---------------------------------------------------------------------------
148// Macros — wire the four owned/borrowed variants and the *Assign traits to the
149// *_core helpers.
150// ---------------------------------------------------------------------------
151
152macro_rules! impl_binop {
153 ($Trait:ident, $method:ident, $core:ident) => {
154 impl $Trait<&CBig> for &CBig {
155 type Output = CBig;
156 #[inline]
157 fn $method(self, rhs: &CBig) -> CBig {
158 $core(self, rhs)
159 }
160 }
161
162 impl $Trait<CBig> for CBig {
163 type Output = CBig;
164 #[inline]
165 fn $method(self, rhs: CBig) -> CBig {
166 $core(&self, &rhs)
167 }
168 }
169
170 impl $Trait<&CBig> for CBig {
171 type Output = CBig;
172 #[inline]
173 fn $method(self, rhs: &CBig) -> CBig {
174 $core(&self, rhs)
175 }
176 }
177
178 impl $Trait<CBig> for &CBig {
179 type Output = CBig;
180 #[inline]
181 fn $method(self, rhs: CBig) -> CBig {
182 $core(self, &rhs)
183 }
184 }
185 };
186}
187
188macro_rules! impl_assign {
189 ($Trait:ident, $method:ident, $core:ident) => {
190 impl $Trait<&CBig> for CBig {
191 #[inline]
192 fn $method(&mut self, rhs: &CBig) {
193 *self = $core(&*self, rhs);
194 }
195 }
196
197 impl $Trait<CBig> for CBig {
198 #[inline]
199 fn $method(&mut self, rhs: CBig) {
200 *self = $core(&*self, &rhs);
201 }
202 }
203 };
204}
205
206impl_binop!(Add, add, add_core);
207impl_binop!(Sub, sub, sub_core);
208impl_binop!(Mul, mul, mul_core);
209impl_binop!(Div, div, div_op_core);
210
211impl_assign!(AddAssign, add_assign, add_core);
212impl_assign!(SubAssign, sub_assign, sub_core);
213impl_assign!(MulAssign, mul_assign, mul_core);
214impl_assign!(DivAssign, div_assign, div_op_core);
215
216// ---------------------------------------------------------------------------
217// Neg
218// ---------------------------------------------------------------------------
219
220impl Neg for CBig {
221 type Output = CBig;
222 #[inline]
223 fn neg(self) -> CBig {
224 CBig {
225 re: -self.re,
226 im: -self.im,
227 }
228 }
229}
230
231impl Neg for &CBig {
232 type Output = CBig;
233 #[inline]
234 fn neg(self) -> CBig {
235 CBig {
236 re: -&self.re,
237 im: -&self.im,
238 }
239 }
240}
241
242// ---------------------------------------------------------------------------
243// PartialEq (component-wise; no Eq — see crate-level docs)
244// ---------------------------------------------------------------------------
245
246impl PartialEq for CBig {
247 #[inline]
248 fn eq(&self, other: &Self) -> bool {
249 self.re == other.re && self.im == other.im
250 }
251}
252
253// ---------------------------------------------------------------------------
254// Default
255// ---------------------------------------------------------------------------
256
257impl Default for CBig {
258 #[inline]
259 fn default() -> Self {
260 CBig::zero()
261 }
262}
263
264// ---------------------------------------------------------------------------
265// Unit tests
266// ---------------------------------------------------------------------------
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 /// Build a `CBig` from two small integer-valued `f64`s.
273 fn c(re: f64, im: f64) -> CBig {
274 CBig::from_f64(re, im).expect("finite parts")
275 }
276
277 /// Assert that `z` equals `re + im·i` via exact decimal string compare.
278 fn assert_parts(z: &CBig, re: &str, im: &str) {
279 assert_eq!(z.re().to_string(), re, "real part mismatch");
280 assert_eq!(z.im().to_string(), im, "imag part mismatch");
281 }
282
283 #[test]
284 fn add_component_wise() {
285 // (1 + 2i) + (3 + 4i) = 4 + 6i
286 let sum = c(1.0, 2.0) + c(3.0, 4.0);
287 assert_parts(&sum, "4", "6");
288 }
289
290 #[test]
291 fn sub_component_wise() {
292 // (1 + 2i) − (3 + 4i) = -2 − 2i
293 let diff = c(1.0, 2.0) - c(3.0, 4.0);
294 assert_parts(&diff, "-2", "-2");
295 }
296
297 #[test]
298 fn mul_hand_computed() {
299 // (1 + 2i)(3 + 4i) = (3 − 8) + (4 + 6)i = -5 + 10i
300 let prod = c(1.0, 2.0) * c(3.0, 4.0);
301 assert_parts(&prod, "-5", "10");
302 }
303
304 #[test]
305 fn mul_i_squared_is_minus_one() {
306 // i · i = -1
307 let prod = CBig::i() * CBig::i();
308 assert_parts(&prod, "-1", "0");
309 }
310
311 #[test]
312 fn mul_one_plus_i_squared_is_two_i() {
313 // (1 + i)² = 1 + 2i + i² = 2i
314 let z = c(1.0, 1.0);
315 let sq = &z * &z;
316 assert_parts(&sq, "0", "2");
317 }
318
319 #[test]
320 fn div_self_is_one() {
321 // (1 + i) / (1 + i) = 1
322 let z = c(1.0, 1.0);
323 let q = &z / &z;
324 assert_parts(&q, "1", "0");
325 }
326
327 #[test]
328 fn div_general() {
329 // (3 + 4i) / (1 + 2i):
330 // conj denom math → num = (3·1 + 4·2) + (4·1 − 3·2)i = 11 − 2i
331 // |1 + 2i|² = 5 → (11/5) + (-2/5)i = 2.2 − 0.4i
332 let q = c(3.0, 4.0) / c(1.0, 2.0);
333 assert_parts(&q, "2.2", "-0.4");
334 }
335
336 #[test]
337 fn checked_div_self_is_one() {
338 let z = c(1.0, 1.0);
339 let q = z.checked_div(&z).expect("non-zero divisor");
340 assert_parts(&q, "1", "0");
341 }
342
343 #[test]
344 fn checked_div_by_zero_is_err() {
345 let z = c(1.0, 1.0);
346 // `CBig` intentionally has no `Debug` (provided, if at all, by the
347 // sibling `fmt` module), so match on the error variant directly rather
348 // than unwrapping the `Ok` payload.
349 assert!(matches!(
350 z.checked_div(&CBig::zero()),
351 Err(OxiNumError::DivByZero)
352 ));
353 }
354
355 #[test]
356 #[should_panic]
357 fn div_operator_by_zero_panics() {
358 let z = c(1.0, 1.0);
359 let _ = z / CBig::zero();
360 }
361
362 #[test]
363 fn norm_sqr_exact() {
364 // |3 + 4i|² = 25 (exact, integer result)
365 assert_eq!(c(3.0, 4.0).norm_sqr().to_string(), "25");
366 }
367
368 #[test]
369 fn default_is_zero() {
370 let d = CBig::default();
371 assert!(d.is_zero());
372 // Exercise `PartialEq` without relying on `Debug` (asserted via the
373 // boolean form because `CBig` has no `Debug` in this module).
374 assert!(d == CBig::zero());
375 }
376
377 #[test]
378 fn partial_eq_component_wise() {
379 assert!(c(1.5, -2.0) == c(1.5, -2.0));
380 assert!(c(1.5, -2.0) != c(1.5, 2.0));
381 assert!(c(1.5, -2.0) != c(-1.5, -2.0));
382 }
383
384 #[test]
385 fn neg_owned_and_borrowed() {
386 let z = c(2.0, -3.0);
387 let n_ref = -&z;
388 assert_parts(&n_ref, "-2", "3");
389 let n_owned = -z;
390 assert_parts(&n_owned, "-2", "3");
391 }
392
393 #[test]
394 fn add_assign_accumulates() {
395 let mut a = c(1.0, 2.0);
396 a += c(3.0, 4.0);
397 assert_parts(&a, "4", "6");
398 a += &c(-4.0, -6.0);
399 assert!(a.is_zero());
400 }
401
402 #[test]
403 fn mul_assign_updates_in_place() {
404 // (1 + i) *= (1 + i) → 2i
405 let mut a = c(1.0, 1.0);
406 a *= &c(1.0, 1.0);
407 assert_parts(&a, "0", "2");
408 }
409
410 #[test]
411 fn sub_and_div_assign() {
412 let mut a = c(5.0, 5.0);
413 a -= c(2.0, 1.0);
414 assert_parts(&a, "3", "4");
415 // (3 + 4i) /= (3 + 4i) → 1
416 a /= c(3.0, 4.0);
417 assert_parts(&a, "1", "0");
418 }
419
420 #[test]
421 fn ownership_variants_consistent() {
422 // All four flavours of Add agree. Compared via `PartialEq`'s boolean
423 // form so the test needs no `Debug` impl for `CBig`.
424 let a = c(1.0, 2.0);
425 let b = c(3.0, 4.0);
426 let target = c(4.0, 6.0);
427 assert!(&a + &b == target);
428 assert!(a.clone() + b.clone() == target);
429 assert!(a.clone() + &b == target);
430 assert!(&a + b.clone() == target);
431 }
432}