rustnomial/
terms.rs

1use alloc::string::String;
2use core::ops::Range;
3use core::str::{CharIndices, FromStr};
4
5use num::{One, Zero};
6
7use crate::err::TermFromStringError;
8
9#[derive(Debug, Clone, PartialEq)]
10/// Degree is a type which represents the degree of a polynomial.
11pub enum Degree {
12    /// The degree of a zero-polynomial.
13    NegInf,
14    /// The degree of a non-zero polynomial.
15    Num(usize),
16}
17
18#[derive(Debug, Clone, PartialEq)]
19/// Term is a type which represents a term in a polynomial.
20pub enum Term<N> {
21    /// A term with coefficient zero. Has degree -inf.
22    ZeroTerm,
23    /// A term with non-zero coefficient and a degree.
24    Term(N, usize),
25}
26
27impl<N: Zero> Term<N> {
28    pub fn new(coeff: N, deg: usize) -> Term<N> {
29        if coeff.is_zero() {
30            Term::ZeroTerm
31        } else {
32            Term::Term(coeff, deg)
33        }
34    }
35}
36
37impl<N> FromStr for Term<N>
38where
39    N: Zero + One + FromStr + Copy,
40{
41    type Err = TermFromStringError;
42
43    fn from_str(s: &str) -> Result<Self, Self::Err> {
44        let mut num_vec = vec![];
45        let mut coeff = N::one();
46        let mut seen_x = false;
47        let mut seen_caret = false;
48        for c in s.chars() {
49            match c {
50                // Allow mixing commas, underscores, and spaces in separating numbers:
51                // eg. 123 456 789 x ^ 123 456 is ok
52                // eg. 123_456_789
53                // Only +, - considered separators.
54                ' ' | ',' | '_' => {}
55                // Assume that number parser handles +, -, .'s if they shouldn't
56                // be there.
57                '+' | '-' | '.' | '0'..='9' => num_vec.push(c),
58                'x' => {
59                    if seen_x {
60                        return Err(TermFromStringError::MoreThanOneX);
61                    }
62                    if !num_vec.is_empty() {
63                        if num_vec.len() == 1 && (num_vec[0] == '-' || num_vec[0] == '+') {
64                            num_vec.push('1')
65                        }
66                        let str: String = num_vec.iter().collect();
67                        num_vec.clear();
68                        coeff = match N::from_str(&str) {
69                            Ok(val) => val,
70                            Err(_) => {
71                                return Err(TermFromStringError::CoeffCouldNotBeParsed);
72                            }
73                        };
74                    }
75                    seen_x = true;
76                }
77                '^' => {
78                    if seen_caret {
79                        return Err(TermFromStringError::MultipleCarets);
80                    }
81                    if !seen_x {
82                        return Err(TermFromStringError::CaretWithoutXInFront);
83                    }
84                    seen_caret = true;
85                }
86                _ => {
87                    return Err(TermFromStringError::UnexpectedChar(c));
88                }
89            }
90        }
91        return if seen_x {
92            if num_vec.is_empty() {
93                if seen_caret {
94                    return Err(TermFromStringError::CaretWithoutDegree);
95                }
96                return Ok(Term::new(coeff, 1));
97            }
98            let str: String = num_vec.iter().collect();
99            if let Ok(degree) = usize::from_str(&str) {
100                Ok(Term::new(coeff, degree))
101            } else {
102                Err(TermFromStringError::CoeffCouldNotBeParsed)
103            }
104        } else {
105            let str: String = num_vec.iter().collect();
106            return match N::from_str(&str) {
107                Ok(val) => Ok(Term::new(val, 0)),
108                Err(_) => {
109                    return Err(TermFromStringError::CoeffCouldNotBeParsed);
110                }
111            };
112        };
113    }
114}
115
116pub(crate) struct TermTokenizer<'a> {
117    char_indices: CharIndices<'a>,
118    seen_start: bool,
119    start_index: usize,
120    len: usize,
121}
122
123impl<'a> TermTokenizer<'a> {
124    pub(crate) fn new(s: &'a str) -> Self {
125        TermTokenizer {
126            char_indices: s.char_indices(),
127            seen_start: false,
128            start_index: 0,
129            len: s.len(),
130        }
131    }
132}
133
134impl<'a> Iterator for TermTokenizer<'a> {
135    type Item = Range<usize>;
136
137    fn next(&mut self) -> Option<Self::Item> {
138        while let Some((index, c)) = self.char_indices.next() {
139            if c == '+' || c == '-' {
140                if self.seen_start {
141                    let range = self.start_index..index;
142                    self.start_index = index;
143                    return Some(range);
144                } else {
145                    self.seen_start = true;
146                }
147            } else if !self.seen_start && !c.is_whitespace() {
148                self.seen_start = true;
149            }
150        }
151
152        if self.start_index != self.len {
153            let range = self.start_index..self.len;
154            self.start_index = self.len;
155            Some(range)
156        } else {
157            None
158        }
159    }
160}
161
162#[cfg(test)]
163mod test {
164    use crate::Term;
165    use core::str::FromStr;
166
167    #[test]
168    fn test_to_str_easy() {
169        assert_eq!(Ok(Term::Term(5i32, 2)), Term::from_str("5x^2"));
170    }
171
172    #[test]
173    fn test_to_str_harder() {
174        assert_eq!(Ok(Term::Term(5i32, 2)), Term::from_str("+5 x ^ 2"));
175    }
176
177    #[test]
178    fn test_to_str_dangling_caret() {
179        assert!(Term::<i32>::from_str("+5 x ^ ").is_err());
180    }
181
182    #[test]
183    fn test_to_str_multiple_carets() {
184        assert!(Term::<i32>::from_str("5x^^2").is_err());
185    }
186
187    #[test]
188    fn test_to_str_no_degree() {
189        assert_eq!(Ok(Term::Term(5i32, 1)), Term::from_str("+5 x "));
190    }
191
192    #[test]
193    fn test_to_str_no_x() {
194        assert_eq!(Ok(Term::Term(5i32, 0)), Term::from_str("+5 "));
195    }
196
197    #[test]
198    fn test_to_str_no_x_and_caret() {
199        assert!(Term::<i32>::from_str("+5^ ").is_err());
200    }
201
202    #[test]
203    fn test_to_str_negative() {
204        assert_eq!(Ok(Term::Term(-1i32, 0)), Term::from_str("-1"));
205    }
206
207    #[test]
208    fn x_no_coeff() {
209        assert_eq!(Ok(Term::Term(1i32, 1)), Term::from_str("x"));
210    }
211
212    #[test]
213    fn neg_x_no_coeff_neg() {
214        assert_eq!(Ok(Term::Term(-1i32, 1)), Term::from_str("-x"));
215    }
216
217    #[test]
218    fn neg_x_no_coeff_pos() {
219        assert_eq!(Ok(Term::Term(1i32, 1)), Term::from_str("+x"));
220    }
221
222    #[test]
223    fn x_no_coeff_float() {
224        assert_eq!(Ok(Term::Term(1.0f32, 1)), Term::from_str("x"));
225    }
226
227    #[test]
228    fn neg_x_no_coeff_neg_float() {
229        assert_eq!(Ok(Term::Term(-1.0f32, 1)), Term::from_str("-x"));
230    }
231
232    #[test]
233    fn neg_x_no_coeff_pos_float() {
234        assert_eq!(Ok(Term::Term(1.0f32, 1)), Term::from_str("+x"));
235    }
236
237    #[test]
238    fn term_from_zero() {
239        assert_eq!(Ok(Term::ZeroTerm), Term::<i32>::from_str("000"));
240    }
241
242    #[test]
243    fn sign_only() {
244        assert!(Term::<i32>::from_str("+").is_err());
245        assert!(Term::<i32>::from_str("-").is_err());
246    }
247
248    #[test]
249    fn empty_str() {
250        assert!(Term::<i32>::from_str("").is_err());
251        assert!(Term::<i32>::from_str("    ").is_err());
252    }
253}