Skip to main content

dashu_cmplx/
parse.rs

1//! [`FromStr`] for [`CBig`] — the algebraic `a+bi` grammar that [`Display`](crate::CBig) emits.
2//!
3//! Accepts an optional real term and an optional `"<sign><coeff>i"` imaginary term (at least one
4//! required): `"5"`, `"-7i"`, `"i"`, `"-i"`, `"1+2i"`, `"-3-4i"`. Each coefficient parses via
5//! [`FBig`]'s `FromStr` (so `inf`/`-inf` are accepted). The MPC parenthesized `"(re im)"` form and
6//! anything else malformed yield a [`ParseError`].
7
8use crate::cbig::CBig;
9use core::str::FromStr;
10use dashu_base::ParseError;
11use dashu_float::round::Round;
12use dashu_float::{FBig, Repr};
13use dashu_int::Word;
14
15impl<R: Round, const B: Word> FromStr for CBig<R, B> {
16    type Err = ParseError;
17
18    fn from_str(s: &str) -> Result<Self, ParseError> {
19        let s = s.trim();
20        if s.is_empty() {
21            return Err(ParseError::NoDigits);
22        }
23        // Reject the MPC parenthesized form "(re im)" outright.
24        if s.contains('(') || s.contains(')') {
25            return Err(ParseError::InvalidDigit);
26        }
27
28        // The only valid 'i' is the trailing imaginary-unit marker.
29        let i_count = s.bytes().filter(|&c| c == b'i').count();
30        if i_count > 1 {
31            return Err(ParseError::InvalidDigit);
32        }
33
34        if i_count == 0 {
35            // pure real term
36            let re = FBig::<R, B>::from_str(s)?;
37            return Ok(CBig::from(re));
38        }
39
40        // exactly one 'i', and it must be the final character
41        if !s.ends_with('i') {
42            return Err(ParseError::InvalidDigit);
43        }
44        let prefix = &s[..s.len() - 1];
45
46        // Split prefix into the real term and the imaginary coefficient. The imaginary coefficient
47        // starts at the last '+' / '-' that is *not* at the leading position; if there is none, the
48        // whole prefix (if any) is the imaginary coefficient and the real term is empty.
49        let split = prefix.rfind(['+', '-']).filter(|&pos| pos > 0);
50        let (real_str, imag_str) = match split {
51            Some(pos) => (&prefix[..pos], &prefix[pos..]),
52            None => ("", prefix),
53        };
54
55        let im = match imag_str {
56            "" | "+" => FBig::<R, B>::ONE,
57            "-" => FBig::<R, B>::NEG_ONE,
58            other => FBig::<R, B>::from_str(other)?,
59        };
60
61        let re = if real_str.is_empty() {
62            // implicit real zero carries the imaginary part's precision
63            FBig::from_repr(Repr::zero(), im.context())
64        } else {
65            FBig::<R, B>::from_str(real_str)?
66        };
67
68        Ok(CBig::from_parts(re, im))
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use alloc::format;
76    use dashu_float::round::mode;
77
78    type C = CBig<mode::HalfAway, 10>;
79
80    fn parse_ok(s: &str) -> C {
81        s.parse()
82            .unwrap_or_else(|e| panic!("failed to parse {s:?}: {e:?}"))
83    }
84
85    #[test]
86    fn roundtrip_display_fromstr() {
87        let cases = [
88            "0", "5", "-7i", "i", "-i", "1+2i", "-3-4i", "1+i", "2-i", "4i",
89        ];
90        for s in cases {
91            let z: C = parse_ok(s);
92            assert_eq!(format!("{}", z), s, "roundtrip failed for {s:?}");
93        }
94    }
95
96    #[test]
97    fn pure_real() {
98        let z: C = "5".parse().unwrap();
99        assert!(z.im().is_pos_zero());
100        assert_eq!(z.re().significand(), &5.into());
101    }
102
103    #[test]
104    fn pure_imaginary_unit() {
105        let z: C = "i".parse().unwrap();
106        assert!(z.re().is_pos_zero());
107        assert_eq!(z.im().significand(), &1.into());
108
109        let z: C = "-i".parse().unwrap();
110        assert_eq!(z.im().significand(), &(-1i32).into());
111    }
112
113    #[test]
114    fn malformed_rejected() {
115        assert!("(1 2)".parse::<C>().is_err());
116        assert!("".parse::<C>().is_err());
117        assert!("1+2".parse::<C>().is_err()); // 'i' required for an imaginary term
118        assert!("ii".parse::<C>().is_err());
119        assert!("1+2ii".parse::<C>().is_err());
120        assert!("i5".parse::<C>().is_err()); // 'i' must be trailing
121    }
122}