malachite_nz/integer_polynomial/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::integer::Integer;
10use crate::integer_polynomial::IntegerPolynomial;
11use alloc::vec::Vec;
12use core::str::FromStr;
13use malachite_base::num::basic::traits::{NegativeOne, One, Zero};
14use malachite_base::polynomial::Polynomial;
15use malachite_base::vars::xyz::XyzVars;
16use malachite_base::vars::{Var, VarScheme, char_is_reserved};
17
18// Reads the variable-and-exponent part of a term, such as `x` or `x^2`, giving the exponent.
19//
20// The name must be the one `var` goes by; a name some other variable of the same scheme goes by is
21// no more acceptable than a name from another scheme altogether, since a polynomial in one variable
22// has nowhere to put a second. An exponent of 0 is refused, as it is what a term with no variable
23// would have, and that term is written as its coefficient alone.
24fn parse_monic<S: VarScheme + ?Sized>(var: Var<'_, S>, monic: &str) -> Option<usize> {
25 let (name, exponent) = match monic.split_once('^') {
26 Some((name, exponent)) => {
27 let exponent = usize::from_str(exponent).ok()?;
28 if exponent == 0 {
29 return None;
30 }
31 (name, exponent)
32 }
33 None => (monic, 1),
34 };
35 if var.scheme().parse_var(name) == Some(var.index()) {
36 Some(exponent)
37 } else {
38 None
39 }
40}
41
42// Splits a polynomial into its terms.
43//
44// A `+` separates two terms and is dropped; a `-` separates them too, but stays, since it belongs
45// to the coefficient that follows it. A sign at the very front is part of the first term rather
46// than a separator, which is how a polynomial that begins with a negative term is read.
47fn split_terms(s: &str) -> Vec<&str> {
48 let mut terms = Vec::new();
49 let mut start = 0;
50 for (i, c) in s.char_indices() {
51 if i != 0 && (c == '+' || c == '-') {
52 terms.push(&s[start..i]);
53 start = if c == '+' { i + 1 } else { i };
54 }
55 }
56 terms.push(&s[start..]);
57 terms
58}
59
60// Reads one term, giving its coefficient and the exponent of the variable in it.
61//
62// A term is a coefficient, or a coefficient and a variable part with a `*` between them, or a
63// variable part alone, whose coefficient is 1, or a variable part behind a `-`, whose coefficient
64// is -1. Which of those it is can be told from its first character or two: a name holds no reserved
65// character, and a coefficient is a run of digits behind an optional sign.
66//
67// A coefficient of 0 is refused. A term that is zero contributes nothing, and is not written at
68// all; the one polynomial with nothing to write is the zero polynomial, which is written `0`, and
69// that is read before this is ever reached.
70fn parse_term<S: VarScheme + ?Sized>(var: Var<'_, S>, term: &str) -> Option<(Integer, usize)> {
71 if term.starts_with(|c: char| !char_is_reserved(c)) {
72 return Some((Integer::ONE, parse_monic(var, term)?));
73 }
74 // `-x` is the variable part with a coefficient of -1, not a coefficient of `-` times anything.
75 if let Some(rest) = term.strip_prefix('-')
76 && rest.starts_with(|c: char| !char_is_reserved(c))
77 {
78 return Some((Integer::NEGATIVE_ONE, parse_monic(var, rest)?));
79 }
80 let (coefficient, exponent) = match term.split_once('*') {
81 Some((coefficient, monic)) => (coefficient, parse_monic(var, monic)?),
82 None => (term, 0),
83 };
84 let coefficient = Integer::from_str(coefficient).ok()?;
85 if coefficient == 0u32 {
86 None
87 } else {
88 Some((coefficient, exponent))
89 }
90}
91
92// The implementation of `IntegerPolynomial::from_string_with`.
93pub(crate) fn from_string_with<S: VarScheme + ?Sized>(
94 var: Var<'_, S>,
95 s: &str,
96) -> Option<IntegerPolynomial> {
97 // The zero polynomial has no terms, so it is the one string that the loop below could not read.
98 if s == "0" {
99 return Some(IntegerPolynomial::ZERO);
100 }
101 // `Display` never writes a leading `+`, so this does not read one.
102 if s.starts_with('+') {
103 return None;
104 }
105 let mut coefficients: Vec<Integer> = Vec::new();
106 for term in split_terms(s) {
107 let (coefficient, exponent) = parse_term(var, term)?;
108 if exponent >= coefficients.len() {
109 coefficients.resize(exponent + 1, Integer::ZERO);
110 } else if coefficients[exponent] != 0u32 {
111 // Two terms of the same degree. Which of them to believe is not for this to decide.
112 return None;
113 }
114 coefficients[exponent] = coefficient;
115 }
116 Some(IntegerPolynomial::from_coefficients_asc(coefficients))
117}
118
119impl FromStr for IntegerPolynomial {
120 type Err = ();
121
122 /// Converts a string to an [`IntegerPolynomial`].
123 ///
124 /// The variable is called `x`. [`from_string_with`](IntegerPolynomial::from_string_with) is the
125 /// way to call it something else.
126 ///
127 /// This reads back everything [`Display`](core::fmt::Display) writes, and more besides: the
128 /// terms may come in any order, an exponent may be written `^1`, and a coefficient may have
129 /// leading zeros. A term may be negative, in which case the `-` that separates it from the term
130 /// before it is the same `-` that its coefficient carries; a term at the front keeps its sign
131 /// and has nothing to be separated from. What it will not accept is a term whose coefficient is
132 /// zero, two terms of the same degree, a leading `+`, a variable other than the one asked for,
133 /// or anything with a space in it. The zero polynomial is `0`, and is the only string in which
134 /// a zero coefficient may be written.
135 ///
136 /// If the string does not represent an [`IntegerPolynomial`], an `Err` is returned.
137 ///
138 /// # Worst-case complexity
139 /// $T(n) = O(n (\log n)^2 \log\log n)$
140 ///
141 /// $M(n) = O(n \log n)$
142 ///
143 /// where $T$ is time, $M$ is additional memory, and $n$ is `s.len()`.
144 ///
145 /// # Examples
146 /// ```
147 /// use core::str::FromStr;
148 /// use malachite_nz::integer_polynomial::IntegerPolynomial;
149 ///
150 /// assert_eq!(
151 /// IntegerPolynomial::from_str("x^2+3*x+2")
152 /// .unwrap()
153 /// .to_string(),
154 /// "x^2+3*x+2"
155 /// );
156 /// assert_eq!(IntegerPolynomial::from_str("0").unwrap().to_string(), "0");
157 /// assert_eq!(IntegerPolynomial::from_str("5").unwrap().to_string(), "5");
158 /// assert_eq!(IntegerPolynomial::from_str("x").unwrap().to_string(), "x");
159 ///
160 /// // A term may be negative, and a leading `-` belongs to the first term rather than
161 /// // separating it from anything.
162 /// assert_eq!(IntegerPolynomial::from_str("-5").unwrap().to_string(), "-5");
163 /// assert_eq!(IntegerPolynomial::from_str("-x").unwrap().to_string(), "-x");
164 /// assert_eq!(
165 /// IntegerPolynomial::from_str("x^2-2*x+5")
166 /// .unwrap()
167 /// .to_string(),
168 /// "x^2-2*x+5"
169 /// );
170 ///
171 /// // The terms may come in any order.
172 /// assert_eq!(
173 /// IntegerPolynomial::from_str("2+3*x+x^2")
174 /// .unwrap()
175 /// .to_string(),
176 /// "x^2+3*x+2"
177 /// );
178 ///
179 /// assert!(IntegerPolynomial::from_str("").is_err());
180 /// assert!(IntegerPolynomial::from_str("y").is_err());
181 /// assert!(IntegerPolynomial::from_str("x^2 + 1").is_err());
182 /// assert!(IntegerPolynomial::from_str("0*x").is_err());
183 /// assert!(IntegerPolynomial::from_str("x+x").is_err());
184 /// // A `+` is a separator, so a leading one is neither written nor read, and two signs in a
185 /// // row leave a term with nothing in it.
186 /// assert!(IntegerPolynomial::from_str("+x").is_err());
187 /// assert!(IntegerPolynomial::from_str("x--1").is_err());
188 /// ```
189 #[inline]
190 fn from_str(s: &str) -> Result<Self, ()> {
191 Self::from_string_with(Var::new(&XyzVars, 0), s).ok_or(())
192 }
193}