malachite_nz/gaussian_integer/conversion/string/from_string.rs
1// Copyright © 2026 Mikhail Hogrefe
2//
3// This file is part of Malachite.
4//
5// Malachite is free software: you can redistribute it and/or modify it under the terms of the GNU
6// Lesser General Public License (LGPL) as published by the Free Software Foundation; either version
7// 3 of the License, or (at your option) any later version. See <https://www.gnu.org/licenses/>.
8
9use crate::gaussian_integer::GaussianInteger;
10use crate::integer::Integer;
11use core::str::FromStr;
12use malachite_base::num::basic::traits::{NegativeOne, One, Zero};
13
14// An imaginary term standing alone: an optional sign, then an optional coefficient in `Integer`
15// syntax. A missing coefficient means 1, so "i" and "-i" work, but so do "1i" and "0i": requiring
16// producers to elide degenerate coefficients would force a special case on them, so the parser
17// accepts both spellings. A single leading '+' is accepted, exactly as `Integer`'s parser accepts
18// "+1".
19fn parse_lone_imaginary_coefficient(s: &str) -> Result<Integer, ()> {
20 match s {
21 "" | "+" => Ok(Integer::ONE),
22 "-" => Ok(Integer::NEGATIVE_ONE),
23 _ => Integer::from_str(s),
24 }
25}
26
27// An imaginary term joined to a preceding real term, starting with the joining sign; the sign alone
28// means a coefficient of 1 or -1.
29fn parse_joined_imaginary_coefficient(s: &str) -> Result<Integer, ()> {
30 match s {
31 "+" => Ok(Integer::ONE),
32 "-" => Ok(Integer::NEGATIVE_ONE),
33 _ => {
34 if let Some(digits) = s.strip_prefix('+') {
35 Integer::from_str(digits)
36 } else {
37 Integer::from_str(s)
38 }
39 }
40 }
41}
42
43impl FromStr for GaussianInteger {
44 type Err = ();
45
46 /// Converts a string to a [`GaussianInteger`].
47 ///
48 /// If the string does not represent a valid [`GaussianInteger`], an `Err` is returned. The
49 /// grammar is strict about structure: the real term must precede the imaginary term, the
50 /// imaginary term must end in `'i'`, and no whitespace is allowed. It is permissive about
51 /// coefficients, much as `Rational`'s parser accepts fractions that are not in lowest terms:
52 /// `"1i"`, `"0i"`, `"2+0i"`, and `"0+1i"` are all accepted, although
53 /// [`Display`](core::fmt::Display) never produces them. Each component follows [`Integer`]'s
54 /// syntax, so leading zeros are allowed, and so is a single leading `'-'` or `'+'` on the
55 /// leading term.
56 ///
57 /// # Worst-case complexity
58 /// $T(n) = O(n (\log n)^2 \log\log n)$
59 ///
60 /// $M(n) = O(n \log n)$
61 ///
62 /// where $T$ is time, $M$ is additional memory, and $n$ is `s.len()`.
63 ///
64 /// # Examples
65 /// ```
66 /// use core::str::FromStr;
67 /// use malachite_nz::gaussian_integer::GaussianInteger;
68 ///
69 /// assert_eq!(GaussianInteger::from_str("0").unwrap().to_string(), "0");
70 /// assert_eq!(GaussianInteger::from_str("-2").unwrap().to_string(), "-2");
71 /// assert_eq!(GaussianInteger::from_str("i").unwrap().to_string(), "i");
72 /// assert_eq!(GaussianInteger::from_str("-i").unwrap().to_string(), "-i");
73 /// assert_eq!(
74 /// GaussianInteger::from_str("2-3i").unwrap().to_string(),
75 /// "2-3i"
76 /// );
77 /// assert_eq!(GaussianInteger::from_str("1i").unwrap().to_string(), "i");
78 /// assert_eq!(GaussianInteger::from_str("0i").unwrap().to_string(), "0");
79 /// assert_eq!(GaussianInteger::from_str("2+0i").unwrap().to_string(), "2");
80 /// assert_eq!(
81 /// GaussianInteger::from_str("+2+1i").unwrap().to_string(),
82 /// "2+i"
83 /// );
84 ///
85 /// assert!(GaussianInteger::from_str("").is_err());
86 /// assert!(GaussianInteger::from_str("i+1").is_err());
87 /// assert!(GaussianInteger::from_str("1 + i").is_err());
88 /// assert!(GaussianInteger::from_str("2+-3i").is_err());
89 /// assert!(GaussianInteger::from_str("2ii").is_err());
90 /// ```
91 fn from_str(s: &str) -> Result<Self, ()> {
92 let Some(r) = s.strip_suffix('i') else {
93 // No imaginary term: the whole string is the real part.
94 return Ok(Self {
95 real: Integer::from_str(s)?,
96 imaginary: Integer::ZERO,
97 });
98 };
99 // The sign joining the real and imaginary terms is the last '+' or '-', except at the start
100 // of the string, where a '-' is the imaginary term's own sign. Any other sign characters
101 // are left inside a component, whose parse then fails.
102 match r.rfind(['+', '-']).filter(|&k| k != 0) {
103 None => Ok(Self {
104 real: Integer::ZERO,
105 imaginary: parse_lone_imaginary_coefficient(r)?,
106 }),
107 Some(k) => Ok(Self {
108 real: Integer::from_str(&r[..k])?,
109 imaginary: parse_joined_imaginary_coefficient(&r[k..])?,
110 }),
111 }
112 }
113}