1#![allow(clippy::module_name_repetitions)]
2
3use thiserror::Error;
4
5use crate::{Calculation, Number};
6
7#[derive(Debug, Error)]
8pub enum GetNumberError {
9 #[error("Failed to parse number '{0}'")]
10 Parse(String),
11}
12
13pub fn split_on_char(
18 haystack: &str,
19 needle: char,
20 start: usize,
21) -> Result<Option<(&str, &str)>, GetNumberError> {
22 let mut pop_stack = vec![];
23
24 for (i, char) in haystack.chars().enumerate().skip(start) {
25 if pop_stack.is_empty() && char == needle {
26 let (a, b) = haystack.split_at(i);
27 return Ok(Some((a, &b[1..])));
28 }
29
30 match char {
31 '{' => {
32 pop_stack.insert(0, '}');
33 }
34 '}' => {
35 moosicbox_assert::assert_or_err!(
36 pop_stack.first() == Some(&'}'),
37 GetNumberError::Parse(format!(
38 "Failed to find ending match to '{{' in \"{haystack}\""
39 )),
40 );
41 pop_stack.remove(0);
42 }
43 '(' => {
44 pop_stack.insert(0, ')');
45 }
46 ')' => {
47 moosicbox_assert::assert_or_err!(
48 pop_stack.first() == Some(&')'),
49 GetNumberError::Parse(format!(
50 "Failed to find ending match to '(' in \"{haystack}\""
51 )),
52 );
53 if pop_stack.first() == Some(&')') {
54 pop_stack.remove(0);
55 }
56 }
57 _ => {}
58 }
59 }
60
61 Ok(None)
62}
63
64pub fn split_on_char_trimmed(
68 haystack: &str,
69 needle: char,
70 start: usize,
71) -> Result<Option<(&str, &str)>, GetNumberError> {
72 Ok(split_on_char(haystack, needle, start)?.map(|(x, y)| (x.trim(), y.trim())))
73}
74
75pub fn parse_grouping(calc: &str) -> Result<Calculation, GetNumberError> {
80 log::trace!("parse_grouping: '{calc}'");
81 if let Some(contents) = calc.strip_prefix('(').and_then(|x| x.strip_suffix(')')) {
82 log::trace!("parse_grouping: contents='{contents}'");
83 Ok(Calculation::Grouping(Box::new(parse_calculation(
84 contents,
85 )?)))
86 } else {
87 let message = format!("Invalid grouping: '{calc}'");
88 log::trace!("parse_grouping: failed='{message}'");
89 Err(GetNumberError::Parse(message))
90 }
91}
92
93pub fn parse_min(calc: &str) -> Result<Calculation, GetNumberError> {
98 log::trace!("parse_min: '{calc}'");
99 if let Some(contents) = calc
100 .strip_prefix("min")
101 .and_then(|x| x.trim_start().strip_prefix('('))
102 .and_then(|x| x.strip_suffix(')'))
103 {
104 log::trace!("parse_min: contents='{contents}'");
105 if let Some((left, right)) = split_on_char_trimmed(contents, ',', 0)? {
106 log::trace!("parse_min: left='{left}' right='{right}'");
107 return Ok(Calculation::Min(
108 Box::new(parse_calculation(left)?),
109 Box::new(parse_calculation(right)?),
110 ));
111 }
112 }
113
114 let message = format!("Invalid min: '{calc}'");
115 log::trace!("parse_min: failed='{message}'");
116 Err(GetNumberError::Parse(message))
117}
118
119pub fn parse_max(calc: &str) -> Result<Calculation, GetNumberError> {
124 log::trace!("parse_max: '{calc}'");
125 if let Some(contents) = calc
126 .strip_prefix("max")
127 .and_then(|x| x.trim_start().strip_prefix('('))
128 .and_then(|x| x.strip_suffix(')'))
129 {
130 log::trace!("parse_max: contents='{contents}'");
131 if let Some((left, right)) = split_on_char_trimmed(contents, ',', 0)? {
132 log::trace!("parse_max: left='{left}' right='{right}'");
133 return Ok(Calculation::Max(
134 Box::new(parse_calculation(left)?),
135 Box::new(parse_calculation(right)?),
136 ));
137 }
138 }
139
140 let message = format!("Invalid max: '{calc}'");
141 log::trace!("parse_max: failed='{message}'");
142 Err(GetNumberError::Parse(message))
143}
144
145pub fn parse_calc(calc: &str) -> Result<Number, GetNumberError> {
150 log::trace!("parse_calc: '{calc}'");
151 if let Some(contents) = calc
152 .strip_prefix("calc")
153 .and_then(|x| x.trim().strip_prefix('('))
154 .and_then(|x| x.strip_suffix(')'))
155 .map(str::trim)
156 {
157 log::trace!("parse_calc: contents='{contents}'");
158 return Ok(Number::Calc(parse_calculation(contents)?));
159 }
160
161 let message = format!("Invalid calc: '{calc}'");
162 log::trace!("parse_calc: failed='{message}'");
163 Err(GetNumberError::Parse(message))
164}
165
166pub fn parse_calculation(calc: &str) -> Result<Calculation, GetNumberError> {
170 if let Ok(min) = parse_min(calc) {
171 return Ok(min);
172 }
173 if let Ok(max) = parse_max(calc) {
174 return Ok(max);
175 }
176 if let Ok(grouping) = parse_grouping(calc) {
177 return Ok(grouping);
178 }
179 if let Ok((left, right)) = parse_operation(calc, '*') {
180 return Ok(Calculation::Multiply(Box::new(left), Box::new(right)));
181 }
182 if let Ok((left, right)) = parse_operation(calc, '/') {
183 return Ok(Calculation::Divide(Box::new(left), Box::new(right)));
184 }
185 if let Ok((left, right)) = parse_signed_operation(calc, '+') {
186 return Ok(Calculation::Add(Box::new(left), Box::new(right)));
187 }
188 if let Ok((left, right)) = parse_signed_operation(calc, '-') {
189 return Ok(Calculation::Subtract(Box::new(left), Box::new(right)));
190 }
191
192 Ok(Calculation::Number(Box::new(parse_number(calc)?)))
193}
194
195fn parse_operation(
196 calc: &str,
197 operator: char,
198) -> Result<(Calculation, Calculation), GetNumberError> {
199 log::trace!("parse_operation: '{calc}' operator={operator}");
200 if let Some((left, right)) = split_on_char_trimmed(calc, operator, 0)? {
201 log::trace!("parse_operation: left='{left}' right='{right}'");
202 return Ok((parse_calculation(left)?, parse_calculation(right)?));
203 }
204
205 let message = format!("Invalid operation: '{calc}'");
206 log::trace!("parse_operation: failed='{message}'");
207 Err(GetNumberError::Parse(message))
208}
209
210fn parse_signed_operation(
211 calc: &str,
212 operator: char,
213) -> Result<(Calculation, Calculation), GetNumberError> {
214 log::trace!("parse_signed_operation: '{calc}' operator={operator}");
215 if let Some((left, right)) = split_on_char_trimmed(calc, operator, 0)? {
216 if left.is_empty() {
217 if let Some((left, right)) = split_on_char_trimmed(calc, operator, 1)? {
218 log::trace!("parse_signed_operation: left='{left}' right='{right}'");
219 if !left.is_empty() && !right.is_empty() {
220 return Ok((parse_calculation(left)?, parse_calculation(right)?));
221 }
222 }
223 } else if !right.is_empty() {
224 log::trace!("parse_signed_operation: left='{left}' right='{right}'");
225 return Ok((parse_calculation(left)?, parse_calculation(right)?));
226 }
227 }
228
229 let message = format!("Invalid signed operation: '{calc}'");
230 log::trace!("parse_signed_operation: failed='{message}'");
231 Err(GetNumberError::Parse(message))
232}
233
234#[allow(clippy::too_many_lines)]
238pub fn parse_number(number: &str) -> Result<Number, GetNumberError> {
239 static EPSILON: f32 = 0.00001;
240
241 let mut number = if let Ok(calc) = parse_calc(number) {
242 calc
243 } else if let Some((number, _)) = number.split_once("dvw") {
244 if number.contains('.') {
245 Number::RealDvw(
246 number
247 .parse::<f32>()
248 .map_err(|_| GetNumberError::Parse(number.to_string()))?,
249 )
250 } else {
251 number
252 .parse::<i64>()
253 .ok()
254 .map(Number::IntegerDvw)
255 .or_else(|| number.parse::<f32>().ok().map(Number::RealDvw))
256 .ok_or_else(|| GetNumberError::Parse(number.to_string()))?
257 }
258 } else if let Some((number, _)) = number.split_once("dvh") {
259 if number.contains('.') {
260 Number::RealDvh(
261 number
262 .parse::<f32>()
263 .map_err(|_| GetNumberError::Parse(number.to_string()))?,
264 )
265 } else {
266 number
267 .parse::<i64>()
268 .ok()
269 .map(Number::IntegerDvh)
270 .or_else(|| number.parse::<f32>().ok().map(Number::RealDvh))
271 .ok_or_else(|| GetNumberError::Parse(number.to_string()))?
272 }
273 } else if let Some((number, _)) = number.split_once("vw") {
274 if number.contains('.') {
275 Number::RealVw(
276 number
277 .parse::<f32>()
278 .map_err(|_| GetNumberError::Parse(number.to_string()))?,
279 )
280 } else {
281 number
282 .parse::<i64>()
283 .ok()
284 .map(Number::IntegerVw)
285 .or_else(|| number.parse::<f32>().ok().map(Number::RealVw))
286 .ok_or_else(|| GetNumberError::Parse(number.to_string()))?
287 }
288 } else if let Some((number, _)) = number.split_once("vh") {
289 if number.contains('.') {
290 Number::RealVh(
291 number
292 .parse::<f32>()
293 .map_err(|_| GetNumberError::Parse(number.to_string()))?,
294 )
295 } else {
296 number
297 .parse::<i64>()
298 .ok()
299 .map(Number::IntegerVh)
300 .or_else(|| number.parse::<f32>().ok().map(Number::RealVh))
301 .ok_or_else(|| GetNumberError::Parse(number.to_string()))?
302 }
303 } else if let Some((number, _)) = number.split_once('%') {
304 if number.contains('.') {
305 Number::RealPercent(
306 number
307 .parse::<f32>()
308 .map_err(|_| GetNumberError::Parse(number.to_string()))?,
309 )
310 } else {
311 number
312 .parse::<i64>()
313 .ok()
314 .map(Number::IntegerPercent)
315 .or_else(|| number.parse::<f32>().ok().map(Number::RealPercent))
316 .ok_or_else(|| GetNumberError::Parse(number.to_string()))?
317 }
318 } else if number.contains('.') {
319 let number = number.strip_suffix("px").unwrap_or(number);
320 Number::Real(
321 number
322 .parse::<f32>()
323 .map_err(|_| GetNumberError::Parse(number.to_string()))?,
324 )
325 } else {
326 let number = number.strip_suffix("px").unwrap_or(number);
327 number
328 .parse::<i64>()
329 .ok()
330 .map(Number::Integer)
331 .or_else(|| number.parse::<f32>().ok().map(Number::Real))
332 .ok_or_else(|| GetNumberError::Parse(number.to_string()))?
333 };
334
335 match &mut number {
336 Number::Real(x)
337 | Number::RealPercent(x)
338 | Number::RealVw(x)
339 | Number::RealVh(x)
340 | Number::RealDvw(x)
341 | Number::RealDvh(x) => {
342 if x.is_sign_negative() && x.abs() < EPSILON {
343 *x = 0.0;
344 }
345 }
346 Number::Integer(..)
347 | Number::IntegerPercent(..)
348 | Number::Calc(..)
349 | Number::IntegerVw(..)
350 | Number::IntegerVh(..)
351 | Number::IntegerDvw(..)
352 | Number::IntegerDvh(..) => {}
353 }
354
355 Ok(number)
356}
357
358#[cfg(test)]
359mod test {
360 use pretty_assertions::assert_eq;
361
362 use crate::{
363 Calculation, Number,
364 parse::{parse_calculation, split_on_char, split_on_char_trimmed},
365 };
366
367 #[test_log::test]
368 fn split_on_char_returns_none_for_basic_floating_point_number() {
369 assert_eq!(split_on_char("123.5", '+', 0).unwrap(), None);
370 }
371
372 #[test_log::test]
373 fn split_on_char_returns_none_for_basic_integer_number() {
374 assert_eq!(split_on_char("123", '+', 0).unwrap(), None);
375 }
376
377 #[test_log::test]
378 fn split_on_char_returns_splits_on_plus_sign_with_floating_point_numbers() {
379 assert_eq!(
380 split_on_char("123.5 + 131.2", '+', 0).unwrap(),
381 Some(("123.5 ", " 131.2"))
382 );
383 }
384
385 #[test_log::test]
386 fn split_on_char_returns_splits_on_plus_sign_with_integer_numbers() {
387 assert_eq!(
388 split_on_char("123 + 131", '+', 0).unwrap(),
389 Some(("123 ", " 131"))
390 );
391 }
392
393 #[test_log::test]
394 fn split_on_char_trimmed_returns_splits_on_plus_sign_with_floating_point_numbers() {
395 assert_eq!(
396 split_on_char_trimmed("123.5 + 131.2", '+', 0).unwrap(),
397 Some(("123.5", "131.2"))
398 );
399 }
400
401 #[test_log::test]
402 fn split_on_char_trimmed_returns_splits_on_plus_sign_with_integer_numbers() {
403 assert_eq!(
404 split_on_char_trimmed("123 + 131", '+', 0).unwrap(),
405 Some(("123", "131"))
406 );
407 }
408
409 #[test_log::test]
410 fn split_on_char_trimmed_skips_char_in_parens_scope() {
411 assert_eq!(
412 split_on_char_trimmed("(123 + 131) + 100", '+', 0).unwrap(),
413 Some(("(123 + 131)", "100"))
414 );
415 }
416
417 #[test_log::test]
418 fn split_on_char_trimmed_skips_char_in_nested_parens_scope() {
419 assert_eq!(
420 split_on_char_trimmed("(123 + (131 * 99)) + 100", '+', 0).unwrap(),
421 Some(("(123 + (131 * 99))", "100"))
422 );
423 }
424
425 #[test_log::test]
426 fn parse_calculation_can_parse_basic_floating_point_number() {
427 assert_eq!(
428 parse_calculation("123.5").unwrap(),
429 Calculation::Number(Box::new(Number::Real(123.5)))
430 );
431 }
432
433 #[test_log::test]
434 fn parse_calculation_can_parse_basic_integer_number() {
435 assert_eq!(
436 parse_calculation("123").unwrap(),
437 Calculation::Number(Box::new(Number::Integer(123)))
438 );
439 }
440
441 #[test_log::test]
442 fn parse_calculation_can_parse_plus_sign_with_floating_point_numbers() {
443 assert_eq!(
444 parse_calculation("123.5 + 131.2").unwrap(),
445 Calculation::Add(
446 Box::new(Calculation::Number(Box::new(Number::Real(123.5)))),
447 Box::new(Calculation::Number(Box::new(Number::Real(131.2))))
448 )
449 );
450 }
451
452 #[test_log::test]
453 fn parse_calculation_can_parse_plus_sign_with_integer_numbers() {
454 assert_eq!(
455 parse_calculation("123 + 131").unwrap(),
456 Calculation::Add(
457 Box::new(Calculation::Number(Box::new(Number::Integer(123)))),
458 Box::new(Calculation::Number(Box::new(Number::Integer(131))))
459 )
460 );
461 }
462
463 #[test_log::test]
464 fn parse_calculation_can_parse_parens_scope() {
465 assert_eq!(
466 parse_calculation("(123 + 131) + 100").unwrap(),
467 Calculation::Add(
468 Box::new(Calculation::Grouping(Box::new(Calculation::Add(
469 Box::new(Calculation::Number(Box::new(Number::Integer(123)))),
470 Box::new(Calculation::Number(Box::new(Number::Integer(131))))
471 )))),
472 Box::new(Calculation::Number(Box::new(Number::Integer(100))))
473 )
474 );
475 }
476
477 #[test_log::test]
478 fn parse_calculation_can_parse_nested_parens_scope() {
479 assert_eq!(
480 parse_calculation("(123 + (131 * 99)) + 100").unwrap(),
481 Calculation::Add(
482 Box::new(Calculation::Grouping(Box::new(Calculation::Add(
483 Box::new(Calculation::Number(Box::new(Number::Integer(123)))),
484 Box::new(Calculation::Grouping(Box::new(Calculation::Multiply(
485 Box::new(Calculation::Number(Box::new(Number::Integer(131)))),
486 Box::new(Calculation::Number(Box::new(Number::Integer(99))))
487 )))),
488 )))),
489 Box::new(Calculation::Number(Box::new(Number::Integer(100))))
490 )
491 );
492 }
493
494 #[test_log::test]
495 fn parse_calculation_can_parse_min_with_two_integers() {
496 assert_eq!(
497 parse_calculation("min(123, 131)").unwrap(),
498 Calculation::Min(
499 Box::new(Calculation::Number(Box::new(Number::Integer(123)))),
500 Box::new(Calculation::Number(Box::new(Number::Integer(131))))
501 )
502 );
503 }
504
505 #[test_log::test]
506 fn parse_calculation_can_parse_min_with_a_space_before_paren() {
507 assert_eq!(
508 parse_calculation("min (123, 131)").unwrap(),
509 Calculation::Min(
510 Box::new(Calculation::Number(Box::new(Number::Integer(123)))),
511 Box::new(Calculation::Number(Box::new(Number::Integer(131))))
512 )
513 );
514 }
515
516 #[test_log::test]
517 fn parse_calculation_can_parse_min_with_two_floats() {
518 assert_eq!(
519 parse_calculation("min(123.5, 131.2)").unwrap(),
520 Calculation::Min(
521 Box::new(Calculation::Number(Box::new(Number::Real(123.5)))),
522 Box::new(Calculation::Number(Box::new(Number::Real(131.2))))
523 )
524 );
525 }
526
527 #[test_log::test]
528 fn parse_calculation_can_parse_max_with_two_integers() {
529 assert_eq!(
530 parse_calculation("max(123, 131)").unwrap(),
531 Calculation::Max(
532 Box::new(Calculation::Number(Box::new(Number::Integer(123)))),
533 Box::new(Calculation::Number(Box::new(Number::Integer(131))))
534 )
535 );
536 }
537
538 #[test_log::test]
539 fn parse_calculation_can_parse_max_with_a_space_before_paren() {
540 assert_eq!(
541 parse_calculation("max (123, 131)").unwrap(),
542 Calculation::Max(
543 Box::new(Calculation::Number(Box::new(Number::Integer(123)))),
544 Box::new(Calculation::Number(Box::new(Number::Integer(131))))
545 )
546 );
547 }
548
549 #[test_log::test]
550 fn parse_calculation_can_parse_max_with_two_floats() {
551 assert_eq!(
552 parse_calculation("max(123.5, 131.2)").unwrap(),
553 Calculation::Max(
554 Box::new(Calculation::Number(Box::new(Number::Real(123.5)))),
555 Box::new(Calculation::Number(Box::new(Number::Real(131.2))))
556 )
557 );
558 }
559
560 #[test_log::test]
561 fn parse_calculation_can_parse_nested_parens_scope_with_min_and_max_calls() {
562 assert_eq!(
563 parse_calculation("(123 + min(131 * max(100, 100%), 25)) + 100").unwrap(),
564 Calculation::Add(
565 Box::new(Calculation::Grouping(Box::new(Calculation::Add(
566 Box::new(Calculation::Number(Box::new(Number::Integer(123)))),
567 Box::new(Calculation::Min(
568 Box::new(Calculation::Multiply(
569 Box::new(Calculation::Number(Box::new(Number::Integer(131)))),
570 Box::new(Calculation::Max(
571 Box::new(Calculation::Number(Box::new(Number::Integer(100)))),
572 Box::new(Calculation::Number(Box::new(Number::IntegerPercent(
573 100
574 ))))
575 )),
576 )),
577 Box::new(Calculation::Number(Box::new(Number::Integer(25)))),
578 )),
579 )))),
580 Box::new(Calculation::Number(Box::new(Number::Integer(100))))
581 )
582 );
583 }
584
585 #[test_log::test]
586 fn parse_calculation_can_parse_negative_number_on_left_and_subtract() {
587 assert_eq!(
588 parse_calculation("-123 - 10").unwrap(),
589 Calculation::Subtract(
590 Box::new(Calculation::Number(Box::new(Number::Integer(-123)))),
591 Box::new(Calculation::Number(Box::new(Number::Integer(10))))
592 )
593 );
594 }
595
596 #[test_log::test]
597 fn parse_calculation_can_parse_negative_number_on_right_and_subtract() {
598 assert_eq!(
599 parse_calculation("123 - -10").unwrap(),
600 Calculation::Subtract(
601 Box::new(Calculation::Number(Box::new(Number::Integer(123)))),
602 Box::new(Calculation::Number(Box::new(Number::Integer(-10))))
603 )
604 );
605 }
606
607 #[test_log::test]
608 fn parse_calculation_can_parse_positive_number_on_left_and_subtract() {
609 assert_eq!(
610 parse_calculation("+123 - 10").unwrap(),
611 Calculation::Subtract(
612 Box::new(Calculation::Number(Box::new(Number::Integer(123)))),
613 Box::new(Calculation::Number(Box::new(Number::Integer(10))))
614 )
615 );
616 }
617
618 #[test_log::test]
619 fn parse_calculation_can_parse_positive_number_on_right_and_subtract() {
620 assert_eq!(
621 parse_calculation("123 - +10").unwrap(),
622 Calculation::Subtract(
623 Box::new(Calculation::Number(Box::new(Number::Integer(123)))),
624 Box::new(Calculation::Number(Box::new(Number::Integer(10))))
625 )
626 );
627 }
628
629 #[test_log::test]
630 fn parse_calculation_can_parse_negative_number_on_left_and_add() {
631 assert_eq!(
632 parse_calculation("-123 + 10").unwrap(),
633 Calculation::Add(
634 Box::new(Calculation::Number(Box::new(Number::Integer(-123)))),
635 Box::new(Calculation::Number(Box::new(Number::Integer(10))))
636 )
637 );
638 }
639
640 #[test_log::test]
641 fn parse_calculation_can_parse_negative_number_on_right_and_add() {
642 assert_eq!(
643 parse_calculation("123 + -10").unwrap(),
644 Calculation::Add(
645 Box::new(Calculation::Number(Box::new(Number::Integer(123)))),
646 Box::new(Calculation::Number(Box::new(Number::Integer(-10))))
647 )
648 );
649 }
650
651 #[test_log::test]
652 fn parse_calculation_can_parse_positive_number_on_left_and_add() {
653 assert_eq!(
654 parse_calculation("+123 + 10").unwrap(),
655 Calculation::Add(
656 Box::new(Calculation::Number(Box::new(Number::Integer(123)))),
657 Box::new(Calculation::Number(Box::new(Number::Integer(10))))
658 )
659 );
660 }
661
662 #[test_log::test]
663 fn parse_calculation_can_parse_positive_number_on_right_and_add() {
664 assert_eq!(
665 parse_calculation("123 + +10").unwrap(),
666 Calculation::Add(
667 Box::new(Calculation::Number(Box::new(Number::Integer(123)))),
668 Box::new(Calculation::Number(Box::new(Number::Integer(10))))
669 )
670 );
671 }
672}