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}