hyperchad_transformer/
parse.rs

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
13/// # Errors
14///
15/// * If there is an unmatched ending ')'
16/// * If there is an unmatched ending '}'
17pub 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
64/// # Errors
65///
66/// * If the `split_on_char` fn failed.
67pub 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
75/// # Errors
76///
77/// * If the input is not a grouping.
78/// * If the contents fails to parse.
79pub 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
93/// # Errors
94///
95/// * If the input is not a `min` function.
96/// * If the contents fails to parse.
97pub 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
119/// # Errors
120///
121/// * If the input is not a `max` function.
122/// * If the contents fails to parse.
123pub 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
145/// # Errors
146///
147/// * If the input is not a `calc` function.
148/// * If the contents fails to parse.
149pub 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
166/// # Errors
167///
168/// * If the `calc` fails to parse.
169pub 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/// # Errors
235///
236/// * If the input string is not a valid number.
237#[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}