Skip to main content

mate_rs/
utils.rs

1//
2// Copyright 2022-present theiskaa. All rights reserved.
3// Use of this source code is governed by MIT license
4// that can be found in the LICENSE file.
5//
6
7use crate::token::{Token, TokenType};
8
9//
10// A interface for custom char-type-checking utility methods.
11// Has a various methods (checkers) based on [&str].
12//
13pub trait ChUtils {
14    // Checks if the given [&self] object is number or not.
15    fn is_number(&self) -> bool;
16
17    // Checks if the given [&self] object is point(comma, dot) or not.
18    //
19    // Like <.> in 3.14 or <,> in 3,14
20    fn is_point(&self) -> bool;
21
22    // Checks if the given [&self] object is plus sign or minus sign.
23    //
24    // Plus signs   --> <+>
25    // Minus signs  --> <->
26    fn is_plus_or_minus(&self) -> bool;
27
28    // Checks if the given [&self] object is division sign or multiplication sign.
29    //
30    // Division signs        --> <:> and </>
31    // Multiplication signs  --> <*> and <•>
32    fn is_div_or_prod(&self) -> bool;
33
34    // A function that combines [is_plus_or_minus] and [is_div_or_prod].
35    // So, it checks if [&self] object is operation sign or not.
36    //
37    // Plus signs            --> <+>
38    // Minus signs           --> <->
39    // Division signs        --> <:> and </>
40    // Multiplication signs  --> <*> and <•>
41    fn is_operation_sign(&self) -> bool;
42
43    // Checks if the given [&self] object is left parentheses or right parentheses sign.
44    //
45    // Left  Parentheses --> (
46    // Right Parentheses --> )
47    fn is_parentheses(&self) -> (bool, bool);
48
49    // Checks if the given [&self] object is left abs or right abs sign.
50    //
51    // Left  ABS --> [
52    // Right ABS --> ]
53    fn is_abs(&self) -> (bool, bool);
54
55    // Checks if the given [%self] object is percentage sign or not.
56    fn is_percentage(&self) -> bool;
57}
58
59impl ChUtils for String {
60    fn is_number(&self) -> bool {
61        let trimmed = self.trim();
62        if trimmed.is_empty() {
63            return false;
64        }
65        // Check if string can be parsed as a number
66        // Allow optional leading +/- sign, followed by digits, optional decimal point, more digits
67        let mut chars = trimmed.chars().peekable();
68
69        // Skip optional leading sign
70        if let Some(&c) = chars.peek() {
71            if c == '+' || c == '-' {
72                chars.next();
73            }
74        }
75
76        // Must have at least one digit
77        let mut has_digit = false;
78        let mut has_decimal = false;
79
80        for c in chars {
81            if c.is_ascii_digit() {
82                has_digit = true;
83            } else if c == '.' || c == ',' {
84                if has_decimal {
85                    return false; // Multiple decimal points
86                }
87                has_decimal = true;
88            } else if c == ' ' {
89                // Allow spaces within numbers (e.g., "1 000")
90                continue;
91            } else {
92                return false; // Non-numeric character
93            }
94        }
95
96        has_digit
97    }
98
99    fn is_point(&self) -> bool {
100        self.trim().eq(".") || self.trim().eq(",")
101    }
102
103    fn is_plus_or_minus(&self) -> bool {
104        self.trim().eq("+") || self.trim().eq("-")
105    }
106
107    fn is_div_or_prod(&self) -> bool {
108        let is_div: bool = self.trim().eq(":") || self.trim().eq("/");
109        let is_prod: bool = self.trim().eq("*") || self.trim().eq("•");
110
111        is_div || is_prod
112    }
113
114    fn is_operation_sign(&self) -> bool {
115        self.is_plus_or_minus() || self.is_div_or_prod()
116    }
117
118    fn is_parentheses(&self) -> (bool, bool) {
119        (self.trim().eq("("), self.trim().eq(")"))
120    }
121
122    fn is_abs(&self) -> (bool, bool) {
123        (self.trim().eq("["), self.trim().eq("]"))
124    }
125
126    fn is_percentage(&self) -> bool {
127        self.trim().eq("%")
128    }
129}
130
131impl ChUtils for Token {
132    fn is_number(&self) -> bool {
133        matches!(self.typ, TokenType::NUMBER)
134    }
135
136    fn is_point(&self) -> bool {
137        false
138    }
139
140    fn is_plus_or_minus(&self) -> bool {
141        matches!(self.typ, TokenType::PLUS | TokenType::MINUS)
142    }
143
144    fn is_div_or_prod(&self) -> bool {
145        matches!(self.typ, TokenType::PRODUCT | TokenType::DIVIDE)
146    }
147
148    fn is_operation_sign(&self) -> bool {
149        self.is_plus_or_minus() || self.is_div_or_prod()
150    }
151
152    fn is_parentheses(&self) -> (bool, bool) {
153        match self.typ {
154            TokenType::LPAREN => (true, false),
155            TokenType::RPAREN => (false, true),
156            _ => (false, false),
157        }
158    }
159
160    fn is_abs(&self) -> (bool, bool) {
161        match self.typ {
162            TokenType::LABS => (true, false),
163            TokenType::RABS => (false, true),
164            _ => (false, false),
165        }
166    }
167
168    fn is_percentage(&self) -> bool {
169        matches!(self.typ, TokenType::PERCENTAGE)
170    }
171}
172
173// Includes tests for only String implementation of [ChUtils].
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use std::collections::HashMap;
178
179    #[test]
180    fn is_number() {
181        let test_data: HashMap<String, bool> = HashMap::from([
182            (String::from("42"), true),
183            (String::from("-25"), true),
184            (String::from("+50"), true),
185            (String::from("-"), false),
186            (String::from("+"), false),
187            (String::from("/"), false),
188        ]);
189
190        for (target, expected) in test_data {
191            assert_eq!(target.is_number(), expected);
192            assert_eq!(Token::from(target, (0, 0)).is_number(), expected);
193        }
194    }
195
196    #[test]
197    fn is_point() {
198        let test_data: HashMap<String, bool> = HashMap::from([
199            (String::from("."), true),
200            (String::from(","), true),
201            (String::from("-"), false),
202            (String::from("+"), false),
203            (String::from("/"), false),
204            (String::from("5"), false),
205        ]);
206
207        for (target, expected) in test_data {
208            assert_eq!(target.is_point(), expected);
209        }
210    }
211
212    #[test]
213    fn is_plus_or_minus() {
214        let test_data: HashMap<String, bool> = HashMap::from([
215            (String::from("-"), true),
216            (String::from("+"), true),
217            (String::from("/"), false),
218            (String::from(".5"), false),
219            (String::from("/"), false),
220            (String::from("*"), false),
221        ]);
222
223        for (target, expected) in test_data {
224            assert_eq!(target.is_plus_or_minus(), expected);
225            assert_eq!(Token::from(target, (0, 0)).is_plus_or_minus(), expected);
226        }
227    }
228
229    #[test]
230    fn is_div_or_prod() {
231        let test_data: HashMap<String, bool> = HashMap::from([
232            (String::from("/"), true),
233            (String::from("*"), true),
234            (String::from(":"), true),
235            (String::from("•"), true),
236            (String::from("-"), false),
237            (String::from("+"), false),
238            (String::from(".5"), false),
239        ]);
240
241        for (target, expected) in test_data {
242            assert_eq!(target.is_div_or_prod(), expected);
243            assert_eq!(Token::from(target, (0, 0)).is_div_or_prod(), expected);
244        }
245    }
246
247    #[test]
248    fn is_operation_sign() {
249        let test_data: HashMap<String, bool> = HashMap::from([
250            (String::from("/"), true),
251            (String::from("*"), true),
252            (String::from(":"), true),
253            (String::from("•"), true),
254            (String::from("-"), true),
255            (String::from("+"), true),
256            (String::from("5"), false),
257            (String::from("."), false),
258            (String::from(","), false),
259        ]);
260
261        for (target, expected) in test_data {
262            assert_eq!(target.is_operation_sign(), expected);
263            assert_eq!(Token::from(target, (0, 0)).is_operation_sign(), expected);
264        }
265    }
266
267    #[test]
268    fn is_parentheses() {
269        let test_data: HashMap<String, (bool, bool)> = HashMap::from([
270            (String::from("/"), (false, false)),
271            (String::from("*"), (false, false)),
272            (String::from(":"), (false, false)),
273            (String::from("•"), (false, false)),
274            (String::from("-"), (false, false)),
275            (String::from("+"), (false, false)),
276            (String::from("5"), (false, false)),
277            (String::from(")"), (false, true)),
278            (String::from("("), (true, false)),
279        ]);
280
281        for (target, expected) in test_data {
282            assert_eq!(target.is_parentheses(), expected);
283            assert_eq!(Token::from(target, (0, 0)).is_parentheses(), expected);
284        }
285    }
286
287    #[test]
288    fn is_abs() {
289        let test_data: HashMap<String, (bool, bool)> = HashMap::from([
290            (String::from("/"), (false, false)),
291            (String::from("*"), (false, false)),
292            (String::from(":"), (false, false)),
293            (String::from("•"), (false, false)),
294            (String::from("-"), (false, false)),
295            (String::from("+"), (false, false)),
296            (String::from("5"), (false, false)),
297            (String::from("]"), (false, true)),
298            (String::from("["), (true, false)),
299        ]);
300
301        for (target, expected) in test_data {
302            assert_eq!(target.is_abs(), expected);
303            assert_eq!(Token::from(target, (0, 0)).is_abs(), expected);
304        }
305    }
306
307    #[test]
308    fn is_percentage() {
309        let test_data: HashMap<bool, String> = HashMap::from([
310            (false, String::from("-25")),
311            (false, String::from("-")),
312            (false, String::from("(")),
313            (true, String::from("%")),
314        ]);
315
316        for (expected, data) in test_data {
317            assert_eq!(expected, data.is_percentage());
318            assert_eq!(expected, Token::from(data, (0, 0)).is_percentage());
319        }
320    }
321}