Skip to main content

automapper_validation/expr/
status.rs

1//! AHB status lines: the status word (`Muss`/`Soll`/`Kann`) and its condition.
2//!
3//! An AHB cell can carry several lines, each with a status word of its own:
4//!
5//! ```text
6//! Muss [2119]
7//! Soll [130]
8//! ```
9//!
10//! "Required if [2119] holds, recommended if [130] holds." Each line is one
11//! [`StatusBranch`]; `ConditionExprEvaluator::resolve_status` decides which
12//! applies.
13
14use super::ast::ConditionExpr;
15
16/// The status word of one AHB status line. `X` is `Muss`.
17///
18/// Ordered by strictness: `Kann < Soll < Muss`.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub enum StatusKind {
21    Kann,
22    Soll,
23    Muss,
24}
25
26impl StatusKind {
27    /// The status word `line` starts with, if any.
28    pub fn of(line: &str) -> Option<Self> {
29        let line = line.trim_start();
30        if line.starts_with("Muss") || line.starts_with('X') {
31            Some(Self::Muss)
32        } else if line.starts_with("Soll") {
33            Some(Self::Soll)
34        } else if line.starts_with("Kann") {
35            Some(Self::Kann)
36        } else {
37            None
38        }
39    }
40}
41
42/// One line of an AHB status: its status word and its condition.
43///
44/// `kind` is `None` for a line without a status word (a bare `[1]`);
45/// `condition` is `None` for a line without a condition (`Kann`).
46#[derive(Debug, Clone, PartialEq)]
47pub struct StatusBranch {
48    pub kind: Option<StatusKind>,
49    pub condition: Option<ConditionExpr>,
50}
51
52/// Split an AHB status into its status lines.
53///
54/// Lines are separated by line breaks, and also by a status word in the middle
55/// of a line: IFTSTA writes some two-status cells as `Muss [56] ∧ [59] Soll [70]`.
56///
57/// Only a status word outside parentheses starts a line. The PID schema joins
58/// nested group statuses into one parent status, which parenthesises such a
59/// cell (`Muss ([525]) ∧ ([56] ∧ [59] Soll [70]) ∧ ([54])`); the word inside is
60/// left to the tokenizer, which skips it.
61pub(crate) fn status_lines(input: &str) -> Vec<&str> {
62    let mut out = Vec::new();
63    for line in input.lines() {
64        let mut rest = line.trim();
65        while !rest.is_empty() {
66            let mut depth = 0usize;
67            let split = rest
68                .char_indices()
69                .find(|&(i, c)| {
70                    match c {
71                        '(' => depth += 1,
72                        ')' => depth = depth.saturating_sub(1),
73                        _ => {}
74                    }
75                    i > 0
76                        && depth == 0
77                        && rest[..i].ends_with(char::is_whitespace)
78                        && ["Muss", "Soll", "Kann"]
79                            .iter()
80                            .any(|w| rest[i..].starts_with(w))
81                })
82                .map_or(rest.len(), |(i, _)| i);
83            out.push(rest[..split].trim());
84            rest = rest[split..].trim_start();
85        }
86    }
87    out
88}