Skip to main content

oxinum_complex/
lib.rs

1#![forbid(unsafe_code)]
2//! Arbitrary-precision complex arithmetic for the OxiNum ecosystem.
3//!
4//! Provides [`CBig`], a complex number whose real and imaginary parts are
5//! each a [`DBig`] (decimal arbitrary-precision float re-exported from
6//! [`oxinum_float`]). Everything is Pure Rust — no GMP, no MPFR, no C/C++
7//! — so the type works unchanged on every platform the rest of the
8//! COOLJAPAN stack targets.
9//!
10//! `CBig` is the high-level, decimal-backed front end. For a ground-up
11//! binary-base complex built directly on [`oxinum_float::native::BigFloat`]
12//! with explicit rounding-mode control, see [`native::BigComplex`].
13//!
14//! # Layout
15//!
16//! A `CBig` is the ordered pair `(re, im)` representing `re + im·i`. The two
17//! components are independent `DBig` values and may carry different
18//! precisions; arithmetic and transcendental routines (added by sibling
19//! modules) reconcile precision as needed.
20//!
21//! # Why `Hash`, `Eq`, and `Ord` are not implemented
22//!
23//! * **No `Ord` / `PartialOrd`.** The complex field is *not* ordered: there
24//!   is no order relation compatible with the ring structure, so any total
25//!   order would be arbitrary and mathematically misleading. Callers who
26//!   need a sort key should compare on a derived scalar (e.g. magnitude or
27//!   the lexicographic `(re, im)` pair) explicitly.
28//! * **No `Hash` / `Eq`.** `CBig` is built from `DBig`, which deliberately
29//!   does **not** implement [`core::hash::Hash`] (distinct representations
30//!   can compare equal across precisions, mirroring the IEEE-754 reasons
31//!   `f32`/`f64` skip `Hash` in the standard library). `PartialEq` is
32//!   provided (component-wise) where useful, but a lawful `Eq`/`Hash` pair
33//!   would require a canonicalisation policy that varies by use case.
34//!
35//! # Examples
36//!
37//! ```
38//! use oxinum_complex::CBig;
39//!
40//! let z = CBig::from_f64(3.0, 4.0).expect("finite parts");
41//! // |3 + 4i|^2 = 9 + 16 = 25
42//! assert_eq!(z.norm_sqr().to_string(), "25");
43//! ```
44
45use core::str::FromStr;
46
47pub use oxinum_core::{OxiNumError, OxiNumResult};
48pub use oxinum_float::DBig;
49
50/// Arbitrary-precision complex number `re + im·i`, with each component a
51/// decimal [`DBig`].
52///
53/// See the crate-level documentation for the rationale behind the absence of
54/// `Hash`, `Eq`, and `Ord`.
55#[derive(Clone)]
56pub struct CBig {
57    pub(crate) re: DBig,
58    pub(crate) im: DBig,
59}
60
61impl CBig {
62    /// Construct a complex number from its real and imaginary parts.
63    ///
64    /// Alias of [`CBig::from_parts`].
65    pub fn new(re: DBig, im: DBig) -> Self {
66        Self::from_parts(re, im)
67    }
68
69    /// Construct a complex number from its real and imaginary parts.
70    pub fn from_parts(re: DBig, im: DBig) -> Self {
71        Self { re, im }
72    }
73
74    /// Construct a purely real complex number (`im = 0`).
75    pub fn from_real(re: DBig) -> Self {
76        Self {
77            re,
78            im: DBig::from(0u32),
79        }
80    }
81
82    /// Construct a purely imaginary complex number (`re = 0`).
83    pub fn from_imag(im: DBig) -> Self {
84        Self {
85            re: DBig::from(0u32),
86            im,
87        }
88    }
89
90    /// The additive identity `0 + 0·i`.
91    pub fn zero() -> Self {
92        Self {
93            re: DBig::from(0u32),
94            im: DBig::from(0u32),
95        }
96    }
97
98    /// The multiplicative identity `1 + 0·i`.
99    pub fn one() -> Self {
100        Self {
101            re: DBig::from(1u32),
102            im: DBig::from(0u32),
103        }
104    }
105
106    /// The imaginary unit `0 + 1·i`.
107    pub fn i() -> Self {
108        Self {
109            re: DBig::from(0u32),
110            im: DBig::from(1u32),
111        }
112    }
113
114    /// Construct a complex number from a pair of `f64` values.
115    ///
116    /// Each component is converted to `DBig` by formatting it with 17
117    /// significant digits (enough to round-trip any finite `f64`) and parsing
118    /// the resulting string.
119    ///
120    /// # Errors
121    ///
122    /// Returns [`OxiNumError::Parse`] if either input is `NaN` or infinite
123    /// (`DBig` models neither) or if the formatted decimal string fails to
124    /// parse.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// use oxinum_complex::CBig;
130    /// let z = CBig::from_f64(1.5, -2.25).expect("finite parts");
131    /// assert_eq!(z.re().to_string(), "1.5");
132    /// assert_eq!(z.im().to_string(), "-2.25");
133    /// ```
134    pub fn from_f64(re: f64, im: f64) -> OxiNumResult<Self> {
135        Ok(Self {
136            re: f64_to_dbig(re)?,
137            im: f64_to_dbig(im)?,
138        })
139    }
140
141    /// A shared reference to the real part.
142    pub fn re(&self) -> &DBig {
143        &self.re
144    }
145
146    /// A shared reference to the imaginary part.
147    pub fn im(&self) -> &DBig {
148        &self.im
149    }
150
151    /// A clone of the real part.
152    pub fn real(&self) -> DBig {
153        self.re.clone()
154    }
155
156    /// A clone of the imaginary part.
157    pub fn imag(&self) -> DBig {
158        self.im.clone()
159    }
160
161    /// Decompose into the owned `(re, im)` pair.
162    pub fn into_parts(self) -> (DBig, DBig) {
163        (self.re, self.im)
164    }
165
166    /// The complex conjugate `re − im·i`.
167    ///
168    /// # Examples
169    ///
170    /// ```
171    /// use oxinum_complex::CBig;
172    /// let z = CBig::from_f64(2.0, 3.0).expect("finite parts");
173    /// let c = z.conj();
174    /// assert_eq!(c.re().to_string(), "2");
175    /// assert_eq!(c.im().to_string(), "-3");
176    /// ```
177    pub fn conj(&self) -> Self {
178        Self {
179            re: self.re.clone(),
180            im: -self.im.clone(),
181        }
182    }
183
184    /// The squared magnitude `re² + im²` (always real, non-negative).
185    ///
186    /// # Examples
187    ///
188    /// ```
189    /// use oxinum_complex::CBig;
190    /// let z = CBig::from_f64(3.0, 4.0).expect("finite parts");
191    /// assert_eq!(z.norm_sqr().to_string(), "25");
192    /// ```
193    pub fn norm_sqr(&self) -> DBig {
194        (&self.re * &self.re) + (&self.im * &self.im)
195    }
196
197    /// Returns `true` if both components are zero.
198    pub fn is_zero(&self) -> bool {
199        let zero = DBig::from(0u32);
200        self.re == zero && self.im == zero
201    }
202
203    /// Returns `true` if the imaginary part is zero (the value is real).
204    pub fn is_real(&self) -> bool {
205        self.im == DBig::from(0u32)
206    }
207
208    /// Returns `true` if the real part is zero (the value is purely imaginary).
209    pub fn is_imaginary(&self) -> bool {
210        self.re == DBig::from(0u32)
211    }
212}
213
214/// Convert an `f64` to `DBig` via a 17-significant-digit decimal string.
215///
216/// `dashu-float` does not implement `From<f64>` for `DBig`; the reliable
217/// route is to format the value (17 digits uniquely identifies any finite
218/// `f64`) and parse it back. Non-finite inputs are rejected because `DBig`
219/// has no `NaN` / `Inf` representation.
220fn f64_to_dbig(v: f64) -> OxiNumResult<DBig> {
221    if v.is_nan() {
222        return Err(OxiNumError::Parse("cannot encode NaN as DBig".into()));
223    }
224    if v.is_infinite() {
225        return Err(OxiNumError::Parse("cannot encode infinity as DBig".into()));
226    }
227    let s = format!("{v:.17e}");
228    DBig::from_str(&s).map_err(|e| OxiNumError::Parse(format!("invalid f64→DBig: {e:?}").into()))
229}
230
231// ---------------------------------------------------------------------------
232// Sub-modules. The CBig type itself lives entirely in this file; the modules
233// below add operator impls, transcendental / trig functions, conversions,
234// formatting, and optional feature integrations.
235// ---------------------------------------------------------------------------
236
237mod convert;
238mod fmt;
239mod ops;
240mod transcendental;
241mod trig;
242
243#[cfg(feature = "num-traits")]
244mod num_traits_impl;
245#[cfg(feature = "serde")]
246mod serde_impl;
247
248/// Native binary-base arbitrary-precision complex implementation built on
249/// [`oxinum_float::native::BigFloat`].
250///
251/// This module is additive: the crate-root [`Complex`] alias remains the
252/// decimal-backed [`CBig`]. Reach for [`native::BigComplex`] when you want
253/// ground-up Pure Rust binary complex arithmetic with explicit rounding-mode
254/// control.
255pub mod native;
256
257/// Convenience alias for [`CBig`], mirroring the `BigFloat = DBig` convention
258/// used throughout the OxiNum crates.
259pub type Complex = CBig;
260
261// ---------------------------------------------------------------------------
262// Tests
263// ---------------------------------------------------------------------------
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn zero_one_i_constructors() {
271        let z = CBig::zero();
272        assert!(z.is_zero());
273        assert_eq!(z.re().to_string(), "0");
274        assert_eq!(z.im().to_string(), "0");
275
276        let one = CBig::one();
277        assert!(one.is_real());
278        assert_eq!(one.re().to_string(), "1");
279        assert_eq!(one.im().to_string(), "0");
280
281        let imag = CBig::i();
282        assert!(imag.is_imaginary());
283        assert_eq!(imag.re().to_string(), "0");
284        assert_eq!(imag.im().to_string(), "1");
285    }
286
287    #[test]
288    fn from_parts_round_trip() {
289        let z = CBig::from_f64(2.5, -7.0).expect("finite parts");
290        let (re, im) = z.clone().into_parts();
291        assert_eq!(re.to_string(), "2.5");
292        assert_eq!(im.to_string(), "-7");
293        assert_eq!(z.real().to_string(), "2.5");
294        assert_eq!(z.imag().to_string(), "-7");
295    }
296
297    #[test]
298    fn conj_negates_imag_and_norm_sqr() {
299        let z = CBig::from_f64(3.0, 4.0).expect("finite parts");
300        let c = z.conj();
301        assert_eq!(c.re().to_string(), "3");
302        assert_eq!(c.im().to_string(), "-4");
303        // |3 + 4i|^2 = 25.
304        assert_eq!(z.norm_sqr().to_string(), "25");
305    }
306
307    #[test]
308    fn from_f64_rejects_non_finite() {
309        assert!(CBig::from_f64(f64::NAN, 0.0).is_err());
310        assert!(CBig::from_f64(0.0, f64::INFINITY).is_err());
311        assert!(CBig::from_f64(f64::NEG_INFINITY, 1.0).is_err());
312    }
313}