Skip to main content

automapper_validation/expr/
parser.rs

1//! Recursive descent parser for condition expressions.
2//!
3//! Grammar (from lowest to highest precedence):
4//!
5//! ```text
6//! expression  = xor_expr
7//! xor_expr    = or_expr (XOR or_expr)*
8//! or_expr     = and_expr (OR and_expr)*
9//! and_expr    = not_expr ((AND | implicit) not_expr)*
10//! not_expr    = NOT not_expr | primary
11//! primary     = CONDITION_ID | '(' expression ')'
12//! ```
13//!
14//! Implicit AND: two adjacent condition references or a condition followed by
15//! `(` without an intervening operator are treated as AND.
16
17use std::collections::BTreeMap;
18
19use super::ast::ConditionExpr;
20use super::status::{status_lines, StatusBranch, StatusKind};
21use super::token::{strip_status_prefix, tokenize, SpannedToken, Token};
22use crate::error::ParseError;
23
24/// Parser for AHB condition expressions.
25pub struct ConditionParser;
26
27impl ConditionParser {
28    /// Parse an AHB status string into a condition expression.
29    ///
30    /// Returns `Ok(None)` if the input contains no condition references
31    /// (e.g., bare `"Muss"` or empty string).
32    ///
33    /// # Examples
34    ///
35    /// ```
36    /// use automapper_validation::expr::ConditionParser;
37    /// use automapper_validation::expr::ConditionExpr;
38    ///
39    /// let expr = ConditionParser::parse("Muss [494]").unwrap().unwrap();
40    /// assert_eq!(expr, ConditionExpr::Ref(494));
41    /// ```
42    pub fn parse(input: &str) -> Result<Option<ConditionExpr>, ParseError> {
43        Self::parse_with_ub(input, &BTreeMap::new())
44    }
45
46    /// Parse an AHB status string, expanding UB condition references inline.
47    ///
48    /// When `[UB1]` is encountered and `ub_definitions` contains "UB1", the
49    /// corresponding pre-parsed expression is substituted. Unknown UB references
50    /// fall back to the normal `parse_condition_id` behavior.
51    pub fn parse_with_ub(
52        input: &str,
53        ub_definitions: &BTreeMap<String, ConditionExpr>,
54    ) -> Result<Option<ConditionExpr>, ParseError> {
55        let input = input.trim();
56        if input.is_empty() {
57            return Ok(None);
58        }
59
60        // AHB_Status can carry alternative branches on separate lines, each with its
61        // own status prefix, e.g.
62        //     Muss [315] ∧ [707]
63        //     Soll [8] ∧ [301] ∧ [707]
64        // As one expression the branches form a disjunction: the part applies if
65        // any branch's condition holds. Which status then applies is
66        // `ConditionExprEvaluator::resolve_status`'s business.
67        let alternatives: Vec<ConditionExpr> = Self::parse_status_branches(input, ub_definitions)?
68            .into_iter()
69            .filter_map(|b| b.condition)
70            .collect();
71
72        match alternatives.len() {
73            0 => Ok(None),
74            1 => Ok(alternatives.into_iter().next()),
75            _ => Ok(Some(ConditionExpr::Or(alternatives))),
76        }
77    }
78
79    /// Parse an AHB status into one [`StatusBranch`] per status line.
80    pub fn parse_status_branches(
81        input: &str,
82        ub_definitions: &BTreeMap<String, ConditionExpr>,
83    ) -> Result<Vec<StatusBranch>, ParseError> {
84        let mut branches = Vec::new();
85        for line in status_lines(input) {
86            let kind = StatusKind::of(line);
87            let stripped = strip_status_prefix(line);
88            // A bare status word ("Kann") is left as it is: no condition.
89            let condition = if kind.is_some() && stripped == line {
90                None
91            } else {
92                let tokens = tokenize(stripped)?;
93                let mut pos = 0;
94                parse_expression(&tokens, &mut pos, ub_definitions)?
95            };
96            branches.push(StatusBranch { kind, condition });
97        }
98        Ok(branches)
99    }
100
101    /// Parse an expression that is known to contain conditions (no prefix stripping).
102    ///
103    /// Returns `Err` if the input cannot be parsed. Returns `Ok(None)` if empty.
104    pub fn parse_raw(input: &str) -> Result<Option<ConditionExpr>, ParseError> {
105        let input = input.trim();
106        if input.is_empty() {
107            return Ok(None);
108        }
109
110        let tokens = tokenize(input)?;
111        if tokens.is_empty() {
112            return Ok(None);
113        }
114
115        let mut pos = 0;
116        let expr = parse_expression(&tokens, &mut pos, &BTreeMap::new())?;
117
118        Ok(expr)
119    }
120}
121
122/// Parse a full expression (entry point for precedence climbing).
123fn parse_expression(
124    tokens: &[SpannedToken],
125    pos: &mut usize,
126    ub_definitions: &BTreeMap<String, ConditionExpr>,
127) -> Result<Option<ConditionExpr>, ParseError> {
128    parse_xor(tokens, pos, ub_definitions)
129}
130
131/// XOR has the lowest precedence.
132fn parse_xor(
133    tokens: &[SpannedToken],
134    pos: &mut usize,
135    ub_definitions: &BTreeMap<String, ConditionExpr>,
136) -> Result<Option<ConditionExpr>, ParseError> {
137    let mut left = match parse_or(tokens, pos, ub_definitions)? {
138        Some(expr) => expr,
139        None => return Ok(None),
140    };
141
142    while *pos < tokens.len() && tokens[*pos].token == Token::Xor {
143        *pos += 1; // consume XOR
144        let right = match parse_or(tokens, pos, ub_definitions)? {
145            Some(expr) => expr,
146            None => return Ok(Some(left)),
147        };
148        left = ConditionExpr::Xor(Box::new(left), Box::new(right));
149    }
150
151    Ok(Some(left))
152}
153
154/// OR has middle-low precedence.
155fn parse_or(
156    tokens: &[SpannedToken],
157    pos: &mut usize,
158    ub_definitions: &BTreeMap<String, ConditionExpr>,
159) -> Result<Option<ConditionExpr>, ParseError> {
160    let mut left = match parse_and(tokens, pos, ub_definitions)? {
161        Some(expr) => expr,
162        None => return Ok(None),
163    };
164
165    while *pos < tokens.len() && tokens[*pos].token == Token::Or {
166        *pos += 1; // consume OR
167        let right = match parse_and(tokens, pos, ub_definitions)? {
168            Some(expr) => expr,
169            None => return Ok(Some(left)),
170        };
171        // Flatten nested ORs into a single Or(vec![...])
172        left = match left {
173            ConditionExpr::Or(mut exprs) => {
174                exprs.push(right);
175                ConditionExpr::Or(exprs)
176            }
177            _ => ConditionExpr::Or(vec![left, right]),
178        };
179    }
180
181    Ok(Some(left))
182}
183
184/// AND has middle-high precedence. Also handles implicit AND between adjacent
185/// conditions or parenthesized groups.
186fn parse_and(
187    tokens: &[SpannedToken],
188    pos: &mut usize,
189    ub_definitions: &BTreeMap<String, ConditionExpr>,
190) -> Result<Option<ConditionExpr>, ParseError> {
191    let mut left = match parse_not(tokens, pos, ub_definitions)? {
192        Some(expr) => expr,
193        None => return Ok(None),
194    };
195
196    while *pos < tokens.len() {
197        if tokens[*pos].token == Token::And {
198            *pos += 1; // consume explicit AND
199            let right = match parse_not(tokens, pos, ub_definitions)? {
200                Some(expr) => expr,
201                None => return Ok(Some(left)),
202            };
203            left = flatten_and(left, right);
204        } else if matches!(
205            tokens[*pos].token,
206            Token::ConditionId(_) | Token::LeftParen | Token::Not
207        ) {
208            // Implicit AND: adjacent condition, paren, or NOT without operator
209            let right = match parse_not(tokens, pos, ub_definitions)? {
210                Some(expr) => expr,
211                None => return Ok(Some(left)),
212            };
213            left = flatten_and(left, right);
214        } else {
215            break;
216        }
217    }
218
219    Ok(Some(left))
220}
221
222/// Flatten nested ANDs into a single And(vec![...]).
223fn flatten_and(left: ConditionExpr, right: ConditionExpr) -> ConditionExpr {
224    match left {
225        ConditionExpr::And(mut exprs) => {
226            exprs.push(right);
227            ConditionExpr::And(exprs)
228        }
229        _ => ConditionExpr::And(vec![left, right]),
230    }
231}
232
233/// NOT has the highest precedence (unary prefix).
234fn parse_not(
235    tokens: &[SpannedToken],
236    pos: &mut usize,
237    ub_definitions: &BTreeMap<String, ConditionExpr>,
238) -> Result<Option<ConditionExpr>, ParseError> {
239    if *pos < tokens.len() && tokens[*pos].token == Token::Not {
240        *pos += 1; // consume NOT
241        let inner = match parse_not(tokens, pos, ub_definitions)? {
242            Some(expr) => expr,
243            None => {
244                return Err(ParseError::UnexpectedToken {
245                    position: if *pos < tokens.len() {
246                        tokens[*pos].position
247                    } else {
248                        0
249                    },
250                    expected: "expression after NOT".to_string(),
251                    found: "end of input".to_string(),
252                });
253            }
254        };
255        return Ok(Some(ConditionExpr::Not(Box::new(inner))));
256    }
257    parse_primary(tokens, pos, ub_definitions)
258}
259
260/// Primary: a condition reference or a parenthesized expression.
261///
262/// When a `ConditionId` matches a key in `ub_definitions`, the pre-parsed
263/// UB expression is substituted inline instead of calling `parse_condition_id`.
264fn parse_primary(
265    tokens: &[SpannedToken],
266    pos: &mut usize,
267    ub_definitions: &BTreeMap<String, ConditionExpr>,
268) -> Result<Option<ConditionExpr>, ParseError> {
269    if *pos >= tokens.len() {
270        return Ok(None);
271    }
272
273    match &tokens[*pos].token {
274        Token::ConditionId(id) => {
275            *pos += 1;
276            // Check for UB expansion before falling back to parse_condition_id
277            if let Some(ub_expr) = ub_definitions.get(id.as_str()) {
278                return Ok(Some(ub_expr.clone()));
279            }
280            Ok(Some(parse_condition_id(id)))
281        }
282        Token::LeftParen => {
283            *pos += 1; // consume (
284            let expr = parse_expression(tokens, pos, ub_definitions)?;
285            // Consume closing paren if present (graceful handling of missing)
286            if *pos < tokens.len() && tokens[*pos].token == Token::RightParen {
287                *pos += 1;
288            }
289            Ok(expr)
290        }
291        _ => Ok(None),
292    }
293}
294
295/// Parse a condition ID string into a ConditionExpr.
296///
297/// Numeric IDs become `Ref(n)`. Non-numeric IDs (like `UB1`, `10P1..5`)
298/// are kept as-is by extracting numeric portions. For the Rust port,
299/// pure numeric IDs use `Ref(u32)`. Non-numeric IDs extract leading
300/// digits if present, otherwise use 0 as a sentinel.
301fn parse_condition_id(id: &str) -> ConditionExpr {
302    // Try pure numeric ID first
303    if let Ok(num) = id.parse::<u32>() {
304        return ConditionExpr::Ref(num);
305    }
306
307    // Check for package condition: NP or NPmin..max
308    if let Some(p_pos) = id.find('P') {
309        let num_part = &id[..p_pos];
310        let range_part = &id[p_pos + 1..];
311        if let Ok(pkg_id) = num_part.parse::<u32>() {
312            let (min, max) = parse_package_range(range_part);
313            return ConditionExpr::Package {
314                id: pkg_id,
315                min,
316                max,
317            };
318        }
319    }
320
321    // For non-numeric, non-package IDs (e.g., "UB1"), extract leading digits
322    let numeric_part: String = id.chars().take_while(|c| c.is_ascii_digit()).collect();
323    if let Ok(num) = numeric_part.parse::<u32>() {
324        ConditionExpr::Ref(num)
325    } else {
326        ConditionExpr::Ref(0)
327    }
328}
329
330/// Parse the range part of a package condition: `"0..1"` → `(0, 1)`, `""` → `(0, MAX)`.
331fn parse_package_range(range: &str) -> (u32, u32) {
332    if range.is_empty() {
333        return (0, u32::MAX);
334    }
335    if let Some((min_str, max_str)) = range.split_once("..") {
336        let min = min_str.parse::<u32>().unwrap_or(0);
337        let max = max_str.parse::<u32>().unwrap_or(u32::MAX);
338        (min, max)
339    } else {
340        let n = range.parse::<u32>().unwrap_or(0);
341        (n, n)
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use pretty_assertions::assert_eq;
349
350    // === Basic parsing ===
351
352    #[test]
353    fn test_parse_single_condition() {
354        let result = ConditionParser::parse("[931]").unwrap().unwrap();
355        assert_eq!(result, ConditionExpr::Ref(931));
356    }
357
358    #[test]
359    fn test_parse_with_muss_prefix() {
360        let result = ConditionParser::parse("Muss [494]").unwrap().unwrap();
361        assert_eq!(result, ConditionExpr::Ref(494));
362    }
363
364    #[test]
365    fn test_parse_with_soll_prefix() {
366        let result = ConditionParser::parse("Soll [494]").unwrap().unwrap();
367        assert_eq!(result, ConditionExpr::Ref(494));
368    }
369
370    #[test]
371    fn test_parse_with_kann_prefix() {
372        let result = ConditionParser::parse("Kann [182]").unwrap().unwrap();
373        assert_eq!(result, ConditionExpr::Ref(182));
374    }
375
376    #[test]
377    fn test_parse_with_x_prefix() {
378        let result = ConditionParser::parse("X [567]").unwrap().unwrap();
379        assert_eq!(result, ConditionExpr::Ref(567));
380    }
381
382    // === Binary operators ===
383
384    #[test]
385    fn test_parse_simple_and() {
386        let result = ConditionParser::parse("[182] ∧ [152]").unwrap().unwrap();
387        assert_eq!(
388            result,
389            ConditionExpr::And(vec![ConditionExpr::Ref(182), ConditionExpr::Ref(152)])
390        );
391    }
392
393    #[test]
394    fn test_parse_simple_or() {
395        let result = ConditionParser::parse("[1] ∨ [2]").unwrap().unwrap();
396        assert_eq!(
397            result,
398            ConditionExpr::Or(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)])
399        );
400    }
401
402    #[test]
403    fn test_parse_simple_xor() {
404        let result = ConditionParser::parse("[1] ⊻ [2]").unwrap().unwrap();
405        assert_eq!(
406            result,
407            ConditionExpr::Xor(
408                Box::new(ConditionExpr::Ref(1)),
409                Box::new(ConditionExpr::Ref(2)),
410            )
411        );
412    }
413
414    // === Chained operators ===
415
416    #[test]
417    fn test_parse_three_way_and() {
418        let result = ConditionParser::parse("[1] ∧ [2] ∧ [3]").unwrap().unwrap();
419        assert_eq!(
420            result,
421            ConditionExpr::And(vec![
422                ConditionExpr::Ref(1),
423                ConditionExpr::Ref(2),
424                ConditionExpr::Ref(3),
425            ])
426        );
427    }
428
429    #[test]
430    fn test_parse_three_way_and_with_prefix() {
431        let result = ConditionParser::parse("Kann [182] ∧ [6] ∧ [570]")
432            .unwrap()
433            .unwrap();
434        assert_eq!(
435            result,
436            ConditionExpr::And(vec![
437                ConditionExpr::Ref(182),
438                ConditionExpr::Ref(6),
439                ConditionExpr::Ref(570),
440            ])
441        );
442        assert_eq!(result.condition_ids(), [6, 182, 570].into());
443    }
444
445    #[test]
446    fn test_parse_multiple_xor() {
447        let result = ConditionParser::parse("[1] ⊻ [2] ⊻ [3] ⊻ [4]")
448            .unwrap()
449            .unwrap();
450        assert_eq!(result.condition_ids(), [1, 2, 3, 4].into());
451    }
452
453    // === Parentheses ===
454
455    #[test]
456    fn test_parse_parenthesized_expression() {
457        let result = ConditionParser::parse("([1] ∨ [2]) ∧ [3]")
458            .unwrap()
459            .unwrap();
460        assert_eq!(
461            result,
462            ConditionExpr::And(vec![
463                ConditionExpr::Or(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]),
464                ConditionExpr::Ref(3),
465            ])
466        );
467    }
468
469    #[test]
470    fn test_parse_nested_parentheses() {
471        // (([1] ∧ [2]) ∨ ([3] ∧ [4])) ∧ [5]
472        let result = ConditionParser::parse("(([1] ∧ [2]) ∨ ([3] ∧ [4])) ∧ [5]")
473            .unwrap()
474            .unwrap();
475        assert_eq!(result.condition_ids(), [1, 2, 3, 4, 5].into());
476        // Outer is AND
477        match &result {
478            ConditionExpr::And(exprs) => {
479                assert_eq!(exprs.len(), 2);
480                assert!(matches!(&exprs[0], ConditionExpr::Or(_)));
481                assert_eq!(exprs[1], ConditionExpr::Ref(5));
482            }
483            other => panic!("Expected And, got {other:?}"),
484        }
485    }
486
487    // === Operator precedence ===
488
489    #[test]
490    fn test_and_has_higher_precedence_than_or() {
491        // [1] ∨ [2] ∧ [3] should parse as [1] ∨ ([2] ∧ [3])
492        let result = ConditionParser::parse("[1] ∨ [2] ∧ [3]").unwrap().unwrap();
493        assert_eq!(
494            result,
495            ConditionExpr::Or(vec![
496                ConditionExpr::Ref(1),
497                ConditionExpr::And(vec![ConditionExpr::Ref(2), ConditionExpr::Ref(3)]),
498            ])
499        );
500    }
501
502    #[test]
503    fn test_or_has_higher_precedence_than_xor() {
504        // [1] ⊻ [2] ∨ [3] should parse as [1] ⊻ ([2] ∨ [3])
505        let result = ConditionParser::parse("[1] ⊻ [2] ∨ [3]").unwrap().unwrap();
506        assert_eq!(
507            result,
508            ConditionExpr::Xor(
509                Box::new(ConditionExpr::Ref(1)),
510                Box::new(ConditionExpr::Or(vec![
511                    ConditionExpr::Ref(2),
512                    ConditionExpr::Ref(3),
513                ])),
514            )
515        );
516    }
517
518    // === Implicit AND ===
519
520    #[test]
521    fn test_adjacent_conditions_implicit_and() {
522        // "[1] [2]" is equivalent to "[1] ∧ [2]"
523        let result = ConditionParser::parse("[1] [2]").unwrap().unwrap();
524        assert_eq!(
525            result,
526            ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)])
527        );
528    }
529
530    #[test]
531    fn test_adjacent_conditions_no_space_implicit_and() {
532        // "[939][14]" from real AHB XML
533        let result = ConditionParser::parse("[939][14]").unwrap().unwrap();
534        assert_eq!(
535            result,
536            ConditionExpr::And(vec![ConditionExpr::Ref(939), ConditionExpr::Ref(14)])
537        );
538    }
539
540    // === NOT operator ===
541
542    #[test]
543    fn test_parse_not() {
544        let result = ConditionParser::parse("NOT [1]").unwrap().unwrap();
545        assert_eq!(result, ConditionExpr::Not(Box::new(ConditionExpr::Ref(1))));
546    }
547
548    #[test]
549    fn test_parse_not_with_and() {
550        // NOT [1] ∧ [2] should parse as (NOT [1]) ∧ [2] because NOT has highest precedence
551        let result = ConditionParser::parse("NOT [1] ∧ [2]").unwrap().unwrap();
552        assert_eq!(
553            result,
554            ConditionExpr::And(vec![
555                ConditionExpr::Not(Box::new(ConditionExpr::Ref(1))),
556                ConditionExpr::Ref(2),
557            ])
558        );
559    }
560
561    // === Real-world AHB expressions ===
562
563    #[test]
564    fn test_real_world_orders_expression() {
565        // From ORDERS AHB: "X (([939] [147]) ∨ ([940] [148])) ∧ [567]"
566        let result = ConditionParser::parse("X (([939] [147]) ∨ ([940] [148])) ∧ [567]")
567            .unwrap()
568            .unwrap();
569        assert_eq!(result.condition_ids(), [147, 148, 567, 939, 940].into());
570    }
571
572    #[test]
573    fn test_real_world_xor_expression() {
574        // "Muss ([102] ∧ [2006]) ⊻ ([103] ∧ [2005])"
575        let result = ConditionParser::parse("Muss ([102] ∧ [2006]) ⊻ ([103] ∧ [2005])")
576            .unwrap()
577            .unwrap();
578        assert!(matches!(result, ConditionExpr::Xor(_, _)));
579        assert_eq!(result.condition_ids(), [102, 103, 2005, 2006].into());
580    }
581
582    #[test]
583    fn test_real_world_complex_nested_with_implicit_and() {
584        // "([939][14]) ∨ ([940][15])"
585        let result = ConditionParser::parse("([939][14]) ∨ ([940][15])")
586            .unwrap()
587            .unwrap();
588        assert!(matches!(result, ConditionExpr::Or(_)));
589        assert_eq!(result.condition_ids(), [14, 15, 939, 940].into());
590    }
591
592    // === Edge cases ===
593
594    #[test]
595    fn test_parse_empty_string() {
596        assert!(ConditionParser::parse("").unwrap().is_none());
597    }
598
599    #[test]
600    fn test_parse_whitespace_only() {
601        assert!(ConditionParser::parse("   \t  ").unwrap().is_none());
602    }
603
604    #[test]
605    fn test_parse_bare_muss() {
606        assert!(ConditionParser::parse("Muss").unwrap().is_none());
607    }
608
609    #[test]
610    fn test_parse_bare_x() {
611        // "X" alone has no conditions after it
612        assert!(ConditionParser::parse("X").unwrap().is_none());
613    }
614
615    #[test]
616    fn test_parse_unmatched_open_paren_graceful() {
617        // ([1] ∧ [2] — missing closing paren
618        let result = ConditionParser::parse("([1] ∧ [2]").unwrap().unwrap();
619        assert_eq!(
620            result,
621            ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)])
622        );
623    }
624
625    #[test]
626    fn test_parse_text_and_operator() {
627        let result = ConditionParser::parse("[1] AND [2]").unwrap().unwrap();
628        assert_eq!(
629            result,
630            ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)])
631        );
632    }
633
634    #[test]
635    fn test_parse_text_or_operator() {
636        let result = ConditionParser::parse("[1] OR [2]").unwrap().unwrap();
637        assert_eq!(
638            result,
639            ConditionExpr::Or(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)])
640        );
641    }
642
643    #[test]
644    fn test_parse_text_xor_operator() {
645        let result = ConditionParser::parse("[1] XOR [2]").unwrap().unwrap();
646        assert_eq!(
647            result,
648            ConditionExpr::Xor(
649                Box::new(ConditionExpr::Ref(1)),
650                Box::new(ConditionExpr::Ref(2)),
651            )
652        );
653    }
654
655    #[test]
656    fn test_parse_mixed_unicode_and_text_operators() {
657        let result = ConditionParser::parse("[1] ∧ [2] OR [3]").unwrap().unwrap();
658        assert_eq!(
659            result,
660            ConditionExpr::Or(vec![
661                ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]),
662                ConditionExpr::Ref(3),
663            ])
664        );
665    }
666
667    #[test]
668    fn test_parse_deeply_nested() {
669        // ((([1])))
670        let result = ConditionParser::parse("((([1])))").unwrap().unwrap();
671        assert_eq!(result, ConditionExpr::Ref(1));
672    }
673
674    // === Package conditions ===
675
676    #[test]
677    fn test_parse_package_condition_0_1() {
678        let result = ConditionParser::parse("[4P0..1]").unwrap().unwrap();
679        assert_eq!(
680            result,
681            ConditionExpr::Package {
682                id: 4,
683                min: 0,
684                max: 1
685            }
686        );
687    }
688
689    #[test]
690    fn test_parse_package_condition_1_5() {
691        let result = ConditionParser::parse("[10P1..5]").unwrap().unwrap();
692        assert_eq!(
693            result,
694            ConditionExpr::Package {
695                id: 10,
696                min: 1,
697                max: 5
698            }
699        );
700    }
701
702    #[test]
703    fn test_parse_package_in_expression() {
704        let result = ConditionParser::parse("X [4P0..1] ⊻ [5P0..1]")
705            .unwrap()
706            .unwrap();
707        assert_eq!(
708            result,
709            ConditionExpr::Xor(
710                Box::new(ConditionExpr::Package {
711                    id: 4,
712                    min: 0,
713                    max: 1
714                }),
715                Box::new(ConditionExpr::Package {
716                    id: 5,
717                    min: 0,
718                    max: 1
719                }),
720            )
721        );
722    }
723
724    #[test]
725    fn test_parse_package_bare_p() {
726        let result = ConditionParser::parse("[1P]").unwrap().unwrap();
727        assert_eq!(
728            result,
729            ConditionExpr::Package {
730                id: 1,
731                min: 0,
732                max: u32::MAX
733            }
734        );
735    }
736
737    #[test]
738    fn test_condition_ids_extraction_full() {
739        let result = ConditionParser::parse("Muss ([102] ∧ [2006]) ⊻ ([103] ∧ [2005])")
740            .unwrap()
741            .unwrap();
742        let ids = result.condition_ids();
743        assert!(ids.contains(&102));
744        assert!(ids.contains(&103));
745        assert!(ids.contains(&2005));
746        assert!(ids.contains(&2006));
747        assert_eq!(ids.len(), 4);
748    }
749
750    // === UB inline expansion ===
751
752    #[test]
753    fn test_parse_ub_inline_expansion() {
754        let ub1_expr = ConditionParser::parse("[931] ∧ [932]").unwrap().unwrap();
755        let mut ub_map = BTreeMap::new();
756        ub_map.insert("UB1".to_string(), ub1_expr.clone());
757
758        let result = ConditionParser::parse_with_ub("X [UB1]", &ub_map)
759            .unwrap()
760            .unwrap();
761        assert_eq!(result, ub1_expr);
762    }
763
764    #[test]
765    fn test_parse_ub_unknown_falls_back() {
766        let ub_map = BTreeMap::new();
767        let result = ConditionParser::parse_with_ub("[UB99]", &ub_map)
768            .unwrap()
769            .unwrap();
770        assert_eq!(result, ConditionExpr::Ref(0));
771    }
772
773    #[test]
774    fn test_parse_with_ub_empty_map_same_as_parse() {
775        let ub_map = BTreeMap::new();
776        // Normal conditions should work unchanged
777        let result = ConditionParser::parse_with_ub("X [931] ∧ [932]", &ub_map)
778            .unwrap()
779            .unwrap();
780        let expected = ConditionParser::parse("X [931] ∧ [932]").unwrap().unwrap();
781        assert_eq!(result, expected);
782    }
783
784    #[test]
785    fn test_parse_ub_in_complex_expression() {
786        // UB expands inside a larger expression
787        let ub1_expr = ConditionParser::parse("[931] ∧ [932]").unwrap().unwrap();
788        let mut ub_map = BTreeMap::new();
789        ub_map.insert("UB1".to_string(), ub1_expr);
790
791        let result = ConditionParser::parse_with_ub("X [UB1] ∨ [100]", &ub_map)
792            .unwrap()
793            .unwrap();
794        // Should be: ([931] ∧ [932]) ∨ [100]
795        assert_eq!(
796            result,
797            ConditionExpr::Or(vec![
798                ConditionExpr::And(vec![ConditionExpr::Ref(931), ConditionExpr::Ref(932)]),
799                ConditionExpr::Ref(100),
800            ])
801        );
802    }
803
804    #[test]
805    fn test_parse_ub_multiple_references() {
806        // Two different UB references in one expression
807        let ub1_expr = ConditionParser::parse("[931]").unwrap().unwrap();
808        let ub2_expr = ConditionParser::parse("[932]").unwrap().unwrap();
809        let mut ub_map = BTreeMap::new();
810        ub_map.insert("UB1".to_string(), ub1_expr);
811        ub_map.insert("UB2".to_string(), ub2_expr);
812
813        let result = ConditionParser::parse_with_ub("X [UB1] ∧ [UB2]", &ub_map)
814            .unwrap()
815            .unwrap();
816        assert_eq!(
817            result,
818            ConditionExpr::And(vec![ConditionExpr::Ref(931), ConditionExpr::Ref(932)])
819        );
820    }
821
822    #[test]
823    fn test_parse_multi_line_alternatives_are_or() {
824        // AHB_Status with alternative branches on separate lines, each with its
825        // own status prefix. Branches form a disjunction (at least one must hold).
826        let status = "Muss [315] ∧ [707]\nSoll [8] ∧ [301] ∧ [707]";
827        let result = ConditionParser::parse(status).unwrap().unwrap();
828        assert_eq!(
829            result,
830            ConditionExpr::Or(vec![
831                ConditionExpr::And(vec![ConditionExpr::Ref(315), ConditionExpr::Ref(707)]),
832                ConditionExpr::And(vec![
833                    ConditionExpr::Ref(8),
834                    ConditionExpr::Ref(301),
835                    ConditionExpr::Ref(707),
836                ]),
837            ])
838        );
839    }
840
841    #[test]
842    fn test_parse_multi_line_with_crlf() {
843        // XML parsing produces \r\n line endings for multi-line AHB_Status values.
844        let result = ConditionParser::parse("Muss [10]\r\nSoll [20]")
845            .unwrap()
846            .unwrap();
847        assert_eq!(
848            result,
849            ConditionExpr::Or(vec![ConditionExpr::Ref(10), ConditionExpr::Ref(20)])
850        );
851    }
852
853    #[test]
854    fn test_parse_single_line_unchanged() {
855        // Single-line inputs continue to produce a bare expression (not wrapped in Or).
856        let result = ConditionParser::parse("Muss [315] ∧ [707]")
857            .unwrap()
858            .unwrap();
859        assert_eq!(
860            result,
861            ConditionExpr::And(vec![ConditionExpr::Ref(315), ConditionExpr::Ref(707)])
862        );
863    }
864
865    // --- parse_status_branches: one branch per status line ---
866
867    fn branches(status: &str) -> Vec<StatusBranch> {
868        ConditionParser::parse_status_branches(status, &BTreeMap::new()).unwrap()
869    }
870
871    #[test]
872    fn test_status_branches_keep_each_lines_status() {
873        assert_eq!(
874            branches("Muss [2119] \r\nSoll [130]"),
875            vec![
876                StatusBranch {
877                    kind: Some(StatusKind::Muss),
878                    condition: Some(ConditionExpr::Ref(2119)),
879                },
880                StatusBranch {
881                    kind: Some(StatusKind::Soll),
882                    condition: Some(ConditionExpr::Ref(130)),
883                },
884            ]
885        );
886    }
887
888    #[test]
889    fn test_status_branches_bare_line_has_no_condition() {
890        assert_eq!(
891            branches("Muss [48]\r\nKann"),
892            vec![
893                StatusBranch {
894                    kind: Some(StatusKind::Muss),
895                    condition: Some(ConditionExpr::Ref(48)),
896                },
897                StatusBranch {
898                    kind: Some(StatusKind::Kann),
899                    condition: None,
900                },
901            ]
902        );
903    }
904
905    #[test]
906    fn test_status_branches_x_is_muss() {
907        assert_eq!(
908            branches("X [931]"),
909            vec![StatusBranch {
910                kind: Some(StatusKind::Muss),
911                condition: Some(ConditionExpr::Ref(931)),
912            }]
913        );
914    }
915
916    #[test]
917    fn test_status_branches_split_a_second_status_on_the_same_line() {
918        // IFTSTA writes some two-status cells without a line break.
919        assert_eq!(
920            branches("Muss [56] ∧ [59] Soll [70]"),
921            vec![
922                StatusBranch {
923                    kind: Some(StatusKind::Muss),
924                    condition: Some(ConditionExpr::And(vec![
925                        ConditionExpr::Ref(56),
926                        ConditionExpr::Ref(59),
927                    ])),
928                },
929                StatusBranch {
930                    kind: Some(StatusKind::Soll),
931                    condition: Some(ConditionExpr::Ref(70)),
932                },
933            ]
934        );
935    }
936
937    #[test]
938    fn test_status_branches_do_not_split_inside_parentheses() {
939        // The PID schema joins nested group statuses into one parent status.
940        // IFTSTA 21038's SG18 carries a one-line two-status cell, which ends up
941        // parenthesised: splitting there would cut the parentheses apart.
942        let status = "Muss ([525]) ∧ ([56] ∧ [59] Soll [70]) ∧ ([54])";
943        let found = branches(status);
944        assert_eq!(found.len(), 1, "{found:?}");
945        assert_eq!(found[0].kind, Some(StatusKind::Muss));
946        assert_eq!(
947            found[0].condition,
948            ConditionParser::parse_raw("([525]) ∧ ([56] ∧ [59] [70]) ∧ ([54])").unwrap()
949        );
950    }
951
952    #[test]
953    fn test_parse_same_line_second_status_is_or() {
954        let result = ConditionParser::parse("Muss [56] ∧ [59] Soll [70]")
955            .unwrap()
956            .unwrap();
957        assert_eq!(
958            result,
959            ConditionExpr::Or(vec![
960                ConditionExpr::And(vec![ConditionExpr::Ref(56), ConditionExpr::Ref(59)]),
961                ConditionExpr::Ref(70),
962            ])
963        );
964    }
965}