Skip to main content

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}