use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusKind {
Muss,
Soll,
Kann,
X,
M,
S,
K,
}
impl StatusKind {
#[must_use]
pub fn is_receiver_checkable(self) -> bool {
matches!(self, Self::Muss | Self::M | Self::X)
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Muss => "Muss",
Self::Soll => "Soll",
Self::Kann => "Kann",
Self::X => "X",
Self::M => "M",
Self::S => "S",
Self::K => "K",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Status {
pub kind: StatusKind,
pub expr: Option<Expr>,
}
impl Status {
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
let mut it = text.split_whitespace();
let word = it.next()?;
let kind = match word {
"Muss" => StatusKind::Muss,
"Soll" => StatusKind::Soll,
"Kann" => StatusKind::Kann,
"X" => StatusKind::X,
"M" => StatusKind::M,
"S" => StatusKind::S,
"K" => StatusKind::K,
_ => return None,
};
let rest: Vec<&str> = it.collect();
let expr = if rest.is_empty() {
None
} else {
Some(Expr::parse(&rest.join(" ")).ok()?)
};
Some(Self { kind, expr })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expr {
Cond(String),
Then(Vec<Expr>),
And(Vec<Expr>),
Or(Vec<Expr>),
Xor(Vec<Expr>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExprError {
Stray(char),
Unterminated,
EmptyCondition,
MissingOperand,
Parentheses,
Empty,
}
impl fmt::Display for ExprError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Stray(c) => write!(f, "stray character {c:?}"),
Self::Unterminated => f.write_str("a `[` with no `]`"),
Self::EmptyCondition => f.write_str("an empty `[]`"),
Self::MissingOperand => f.write_str("an operator with no operand"),
Self::Parentheses => f.write_str("unbalanced parentheses"),
Self::Empty => f.write_str("no expression"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EvalError {
ChoiceOverNeutral,
PaketCycle(String),
PaketExpression(String, ExprError),
}
impl fmt::Display for EvalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ChoiceOverNeutral => f.write_str(
"∨/⊻ between a Voraussetzung and a Formatbedingung, Wiederholbarkeit or Zeitpunktangabe",
),
Self::PaketCycle(p) => write!(f, "the Paketvoraussetzung of [{p}] cites [{p}]"),
Self::PaketExpression(p, e) => {
write!(f, "the Paketvoraussetzung of [{p}] does not read: {e}")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Paket {
pub id: String,
pub min: usize,
pub max: Option<usize>,
}
impl Paket {
#[must_use]
pub fn parse(cited: &str) -> Option<Self> {
let p = cited.find('P')?;
let (number, rest) = cited.split_at(p);
if number.is_empty() || !number.chars().all(|c| c.is_ascii_digit()) {
return None;
}
let id = format!("{number}P");
let merkmal = &rest[1..];
if merkmal.is_empty() {
return Some(Self {
id,
min: 0,
max: None,
});
}
let (low, high) = merkmal.split_once("..")?;
Some(Self {
id,
min: low.parse().ok()?,
max: if high == "n" {
None
} else {
Some(high.parse().ok()?)
},
})
}
}
impl Expr {
pub fn parse(text: &str) -> Result<Self, ExprError> {
let toks = lex(text)?;
if toks.is_empty() {
return Err(ExprError::Empty);
}
let mut pos = 0;
let e = parse_chain(&toks, &mut pos)?;
if pos != toks.len() {
return Err(ExprError::Parentheses);
}
Ok(e)
}
#[must_use]
pub fn cited(&self) -> Vec<&str> {
let mut out = Vec::new();
self.collect(&mut out);
out
}
#[must_use]
pub fn pakete(&self) -> Vec<Paket> {
self.cited()
.into_iter()
.filter(|id| ConditionKind::of(id) == ConditionKind::Paket)
.filter_map(Paket::parse)
.collect()
}
fn collect<'a>(&'a self, out: &mut Vec<&'a str>) {
match self {
Expr::Cond(c) => out.push(c),
Expr::Then(v) | Expr::And(v) | Expr::Or(v) | Expr::Xor(v) => {
for e in v {
e.collect(out);
}
}
}
}
fn is_hinweis(&self) -> bool {
let cited = self.cited();
!cited.is_empty()
&& cited
.iter()
.all(|id| ConditionKind::of(id) == ConditionKind::Hinweis)
}
fn gates(&self) -> bool {
self.cited().iter().any(|id| {
matches!(
ConditionKind::of(id),
ConditionKind::Voraussetzung | ConditionKind::Paket
)
})
}
pub fn eval(
&self,
oracle: &mut dyn FnMut(&str) -> Result<Truth, EvalError>,
) -> Result<Truth, EvalError> {
match self {
Expr::Cond(c) => oracle(c),
Expr::Then(v) | Expr::And(v) => {
let mut vals = Vec::with_capacity(v.len());
for e in v {
vals.push(e.eval(oracle)?);
}
Ok(if vals.contains(&Truth::False) {
Truth::False
} else if vals.contains(&Truth::Unknown) {
Truth::Unknown
} else if vals.iter().all(|t| *t == Truth::Neutral) {
Truth::Neutral
} else {
Truth::True
})
}
Expr::Or(v) => {
let vals = choice_values(v, oracle)?;
Ok(if vals.is_empty() {
Truth::Neutral
} else if vals.contains(&Truth::True) {
Truth::True
} else if vals.contains(&Truth::Unknown) {
Truth::Unknown
} else {
Truth::False
})
}
Expr::Xor(v) => {
let vals = choice_values(v, oracle)?;
if vals.is_empty() {
return Ok(Truth::Neutral);
}
let trues = vals.iter().filter(|t| **t == Truth::True).count();
Ok(if trues >= 2 {
Truth::False
} else if vals.contains(&Truth::Unknown) {
Truth::Unknown
} else if trues == 1 {
Truth::True
} else {
Truth::False
})
}
}
}
}
fn choice_values(
operands: &[Expr],
oracle: &mut dyn FnMut(&str) -> Result<Truth, EvalError>,
) -> Result<Vec<Truth>, EvalError> {
let kept: Vec<&Expr> = operands.iter().filter(|e| !e.is_hinweis()).collect();
let gating = kept.iter().filter(|e| e.gates()).count();
if gating > 0 && gating < kept.len() {
return Err(EvalError::ChoiceOverNeutral);
}
let mut vals = Vec::with_capacity(kept.len());
for e in kept {
let t = e.eval(oracle)?;
if t != Truth::Neutral {
vals.push(t);
}
}
Ok(vals)
}
fn precedence(e: &Expr) -> u8 {
match e {
Expr::Cond(_) => 3,
Expr::Then(_) => 2,
Expr::And(_) | Expr::Or(_) | Expr::Xor(_) => 1,
}
}
impl fmt::Display for Expr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn operand(f: &mut fmt::Formatter<'_>, e: &Expr) -> fmt::Result {
if precedence(e) < 2 {
write!(f, "({e})")
} else {
write!(f, "{e}")
}
}
fn join(f: &mut fmt::Formatter<'_>, v: &[Expr], op: &str) -> fmt::Result {
for (i, e) in v.iter().enumerate() {
if i > 0 {
write!(f, "{op}")?;
}
operand(f, e)?;
}
Ok(())
}
match self {
Expr::Cond(c) => write!(f, "[{c}]"),
Expr::Then(v) => join(f, v, " "),
Expr::And(v) => join(f, v, " ∧ "),
Expr::Or(v) => join(f, v, " ∨ "),
Expr::Xor(v) => join(f, v, " ⊻ "),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Tok {
Cond(String),
And,
Or,
Xor,
Open,
Close,
}
fn lex(text: &str) -> Result<Vec<Tok>, ExprError> {
let mut out = Vec::new();
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
match chars[i] {
'[' => {
let start = i + 1;
while i < chars.len() && chars[i] != ']' {
i += 1;
}
if i >= chars.len() {
return Err(ExprError::Unterminated);
}
let id: String = chars[start..i].iter().collect();
if id.trim().is_empty() {
return Err(ExprError::EmptyCondition);
}
out.push(Tok::Cond(id));
i += 1;
}
'∧' => {
out.push(Tok::And);
i += 1;
}
'∨' => {
out.push(Tok::Or);
i += 1;
}
'⊻' => {
out.push(Tok::Xor);
i += 1;
}
'(' => {
out.push(Tok::Open);
i += 1;
}
')' => {
out.push(Tok::Close);
i += 1;
}
c if c.is_whitespace() => i += 1,
c => return Err(ExprError::Stray(c)),
}
}
Ok(out)
}
fn parse_atom(toks: &[Tok], pos: &mut usize) -> Result<Expr, ExprError> {
match toks.get(*pos) {
Some(Tok::Cond(c)) => {
*pos += 1;
Ok(Expr::Cond(c.clone()))
}
Some(Tok::Open) => {
*pos += 1;
let inner = parse_chain(toks, pos)?;
if toks.get(*pos) != Some(&Tok::Close) {
return Err(ExprError::Parentheses);
}
*pos += 1;
Ok(inner)
}
_ => Err(ExprError::MissingOperand),
}
}
fn parse_juxtaposition(toks: &[Tok], pos: &mut usize) -> Result<Expr, ExprError> {
let mut items = vec![parse_atom(toks, pos)?];
while matches!(toks.get(*pos), Some(Tok::Cond(_) | Tok::Open)) {
items.push(parse_atom(toks, pos)?);
}
if items.len() == 1 {
Ok(items.remove(0))
} else {
Ok(Expr::Then(items))
}
}
fn parse_chain(toks: &[Tok], pos: &mut usize) -> Result<Expr, ExprError> {
let mut acc = parse_juxtaposition(toks, pos)?;
while let Some(op) = toks
.get(*pos)
.filter(|t| matches!(t, Tok::And | Tok::Or | Tok::Xor))
.cloned()
{
*pos += 1;
let rhs = parse_juxtaposition(toks, pos)?;
acc = match (op, acc) {
(Tok::And, Expr::And(mut v)) => {
v.push(rhs);
Expr::And(v)
}
(Tok::And, a) => Expr::And(vec![a, rhs]),
(Tok::Or, Expr::Or(mut v)) => {
v.push(rhs);
Expr::Or(v)
}
(Tok::Or, a) => Expr::Or(vec![a, rhs]),
(Tok::Xor, Expr::Xor(mut v)) => {
v.push(rhs);
Expr::Xor(v)
}
(_, a) => Expr::Xor(vec![a, rhs]),
};
}
Ok(acc)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Truth {
True,
False,
Unknown,
Neutral,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConditionKind {
Voraussetzung,
Hinweis,
Format,
Wiederholbarkeit,
Zeitpunkt,
Paket,
Other,
}
impl ConditionKind {
#[must_use]
pub fn of(id: &str) -> Self {
if id.starts_with("UB") {
return Self::Zeitpunkt;
}
if id.contains('P') {
return Self::Paket;
}
match id.parse::<u32>() {
Ok(1..=499) => Self::Voraussetzung,
Ok(500..=899) => Self::Hinweis,
Ok(900..=999) => Self::Format,
Ok(2000..=2499) => Self::Wiederholbarkeit,
_ => Self::Other,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SegmentPattern {
pub tag: String,
pub elements: Vec<Vec<Option<Vec<String>>>>,
}
impl SegmentPattern {
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
let text = text.trim_end_matches([',', '.', ';', ')']);
let (tag, rest) = match text.find('+') {
Some(i) => (&text[..i], &text[i + 1..]),
None => (text, ""),
};
if tag.len() != 3 || !tag.chars().all(|c| c.is_ascii_uppercase()) {
return None;
}
let elements = split_unescaped(rest, '+')
.into_iter()
.map(|el| {
split_unescaped(&el, ':')
.into_iter()
.map(|comp| {
let comp = comp.replace("?+", "+").replace("?:", ":");
if comp.is_empty()
|| comp.eq_ignore_ascii_case("xxx")
|| comp.eq_ignore_ascii_case("xxxx")
{
None
} else {
Some(comp.split('/').map(str::to_owned).collect())
}
})
.collect()
})
.collect();
Some(Self {
tag: tag.to_owned(),
elements,
})
}
#[must_use]
pub fn matches(&self, seg: &edifact_rs::Segment<'_>) -> bool {
if seg.tag != self.tag {
return false;
}
self.elements.iter().enumerate().all(|(ei, comps)| {
comps
.iter()
.enumerate()
.all(|(ci, alternatives)| match alternatives {
None => true,
Some(alts) => seg
.component_str(ei, ci)
.is_some_and(|v| alts.iter().any(|a| code_matches(a, v))),
})
})
}
}
fn code_matches(pattern: &str, value: &str) -> bool {
if pattern == value {
return true;
}
if !pattern.chars().any(|c| c.is_ascii_lowercase()) {
return false;
}
let (p, v): (Vec<char>, Vec<char>) = (pattern.chars().collect(), value.chars().collect());
p.len() == v.len()
&& p.iter()
.zip(&v)
.all(|(pc, vc)| pc.is_ascii_lowercase() || pc == vc)
}
fn split_unescaped(s: &str, sep: char) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '?'
&& let Some(&n) = chars.peek()
{
cur.push('?');
cur.push(n);
chars.next();
continue;
}
if c == sep {
out.push(std::mem::take(&mut cur));
} else {
cur.push(c);
}
}
out.push(cur);
if s.is_empty() {
out.clear();
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Scope {
Message,
Group(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProSegment {
pub pattern: SegmentPattern,
}
impl ProSegment {
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
let lower = text.to_lowercase();
let per_each = [
"für jede",
"für jeden",
"für jedes",
"je sg",
"einmal für jede",
]
.iter()
.any(|m| lower.contains(m));
if !per_each {
return None;
}
let pattern = first_group_segment(text)?;
if pattern.tag == "IDE" {
return None;
}
Some(Self { pattern })
}
}
fn first_group_segment(text: &str) -> Option<SegmentPattern> {
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i + 3 < chars.len() {
if chars[i] == 'S' && chars[i + 1] == 'G' && chars[i + 2].is_ascii_digit() {
let mut j = i + 2;
while j < chars.len() && chars[j].is_ascii_digit() {
j += 1;
}
while j < chars.len() && chars[j].is_whitespace() {
j += 1;
}
let start = j;
while j < chars.len() && !chars[j].is_whitespace() && chars[j] != '(' && chars[j] != ','
{
j += 1;
}
let token: String = chars[start..j].iter().collect();
if let Some(p) = SegmentPattern::parse(&token) {
return Some(p);
}
if token.len() == 3 && token.chars().all(|c| c.is_ascii_uppercase()) {
return Some(SegmentPattern {
tag: token,
elements: Vec::new(),
});
}
i = j;
continue;
}
i += 1;
}
None
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Voraussetzung {
Present {
scope: Scope,
pattern: SegmentPattern,
negate: bool,
},
ElementValue {
scope: Scope,
tag: String,
de: String,
values: Vec<String>,
negate: bool,
suffix: bool,
},
Count {
scope: Scope,
pattern: SegmentPattern,
more_than: usize,
},
}
fn join_wrapped_pattern(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let chars: Vec<char> = text.chars().collect();
let mut in_pattern = false;
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '+' && i >= 3 && chars[i - 3..i].iter().all(char::is_ascii_uppercase) {
in_pattern = true;
}
if c == ' ' && in_pattern {
let prev = out.chars().last();
let next = chars.get(i + 1).copied();
if matches!(prev, Some('+' | '-' | ':' | '/'))
&& next.is_some_and(|n| n.is_ascii_alphanumeric() || n == '?')
{
i += 1;
continue;
}
in_pattern = false;
}
out.push(c);
i += 1;
}
out
}
#[must_use]
pub fn is_precondition(text: &str) -> bool {
let t = text.trim_start().to_lowercase();
t.starts_with("wenn ") || t.starts_with("falls ") || t.starts_with("sofern ")
}
fn group_name(word: &str) -> Option<String> {
let n = word.strip_prefix("SG").or_else(|| word.strip_prefix('S'))?;
(!n.is_empty() && n.chars().all(|c| c.is_ascii_digit())).then(|| format!("SG{n}"))
}
fn named_scope(words: &[&str]) -> Option<String> {
words
.windows(2)
.filter(|w| {
matches!(
w[0],
"dieser" | "diesem" | "diese" | "derselben" | "demselben" | "dieselbe" | "selben"
)
})
.find_map(|w| group_name(w[1].trim_end_matches([',', '.', ';'])))
}
fn names_qualified_segment(word: &str) -> bool {
let word = word.trim_end_matches([',', '.', ';', ')']);
let Some((tag, rest)) = word.split_once('+') else {
return false;
};
!rest.is_empty() && tag.len() == 3 && tag.chars().all(|c| c.is_ascii_uppercase())
}
fn is_de_token(w: &str) -> bool {
w.len() == 6 && w.starts_with("DE") && w[2..].chars().all(|c| c.is_ascii_digit())
}
fn parse_comparison(words: &[&str]) -> Option<Voraussetzung> {
let di = words.iter().position(|w| is_de_token(w))?;
let negate = match *words.get(di + 1)? {
"=" => false,
"<>" | "\u{2260}" | "!=" => true,
_ => return None,
};
let value = words
.get(di + 2)?
.trim_end_matches([',', '.', ';', ')'])
.trim_matches(['"', '\u{201e}', '\u{201c}', '\u{201d}']);
let values: Vec<String> = value.split('/').map(str::to_owned).collect();
if values
.iter()
.any(|v| v.is_empty() || !v.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'))
{
return None;
}
let (scope, tag_idx) = locate_pattern(words);
let seg = words[tag_idx?].trim_end_matches([',', '.', ';', ')']);
let tag = seg.split('+').next().unwrap_or("");
if tag.len() != 3 {
return None;
}
Some(Voraussetzung::ElementValue {
suffix: false,
scope,
tag: tag.to_owned(),
de: words[di][2..].to_owned(),
values,
negate,
})
}
fn locate_pattern(words: &[&str]) -> (Scope, Option<usize>) {
let mut scope = named_scope(words).map_or(Scope::Message, Scope::Group);
let mut tag_idx: Option<usize> = None;
let mut after_group = false;
let is_tag = |w: &str| {
let seg_part = w.split('+').next().unwrap_or("");
seg_part.len() == 3 && seg_part.chars().all(|c| c.is_ascii_uppercase())
};
for (i, w) in words.iter().enumerate() {
let w = w.trim_end_matches([',', '.', ';']);
if let Some(n) = group_name(w) {
if scope == Scope::Message {
scope = Scope::Group(n);
}
after_group = true;
continue;
}
if i > 0 && is_tag(w) {
let article = words.get(i + 1).is_some_and(|n| {
matches!(
*n,
"das" | "der" | "die" | "ein" | "eine" | "einen" | "kein" | "keine"
)
});
if after_group
&& !w.contains('+')
&& article
&& words[i + 1..].iter().any(|later| is_tag(later))
{
after_group = false;
continue;
}
tag_idx = Some(i);
break;
}
after_group = false;
}
(scope, tag_idx)
}
impl Voraussetzung {
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
let joined = join_wrapped_pattern(text);
let t = joined.trim();
let lower = t.to_lowercase();
if !lower.starts_with("wenn ") {
return None;
}
let words: Vec<&str> = t.split_whitespace().collect();
if let Some(v) = parse_comparison(&words) {
return Some(v);
}
if !lower.contains("vorhanden") {
return None;
}
let negate = lower.contains(" nicht vorhanden")
|| words
.get(1)
.is_some_and(|w| matches!(*w, "kein" | "keine" | "keinen" | "nicht"));
let (scope, tag_idx) = locate_pattern(&words);
let ti = tag_idx?;
if words[ti + 1..].iter().any(|w| names_qualified_segment(w)) {
return None;
}
let seg_word = words[ti].trim_end_matches([',', '.', ';', ')']);
let de_idx = words
.iter()
.enumerate()
.find(|(_, w)| is_de_token(w))
.map(|(i, _)| i);
if let Some(de_word) = de_idx.and_then(|i| words.get(i))
&& (lower.contains(" mit wert ") || lower.contains(" mit dem wert "))
{
let v_idx = words.iter().position(|w| *w == "Wert")?;
let value = words
.get(v_idx + 1)?
.trim_end_matches([',', '.', ';', ')'])
.trim_matches(['"', '„', '“', '”'])
.to_owned();
let suffix = lower.contains("letzten beiden stellen");
return Some(Self::ElementValue {
suffix,
scope,
tag: seg_word[..3].to_owned(),
de: de_word[2..].to_owned(),
values: vec![value],
negate,
});
}
if let Some(di) = de_idx {
let values = code_alternatives(&words[di + 1..], &seg_word[..3]);
if !values.is_empty() {
return Some(Self::ElementValue {
suffix: false,
scope,
tag: seg_word[..3].to_owned(),
de: words[di][2..].to_owned(),
values,
negate,
});
}
}
let pattern = SegmentPattern::parse(seg_word)?;
if let Some(i) = lower.find("mehr als ") {
let n = lower[i + 9..].split_whitespace().next().unwrap_or("");
let more_than = match n {
"einmal" => 1,
"zweimal" | "zwei" => 2,
"dreimal" | "drei" => 3,
"viermal" | "vier" => 4,
_ => return None,
};
return Some(Self::Count {
scope,
pattern,
more_than,
});
}
if lower.contains("mal vorhanden") && !lower.contains("einmal vorhanden") {
return None;
}
Some(Self::Present {
scope,
pattern,
negate,
})
}
}
fn code_alternatives(rest: &[&str], tag: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for w in rest {
let w = w.trim_matches(|c: char| !c.is_ascii_alphanumeric() && c != '/');
if w.eq_ignore_ascii_case("vorhanden") {
break;
}
for part in w.split('/') {
if part.is_empty()
|| part == tag
|| (part.starts_with("SG") && part[2..].chars().all(|c| c.is_ascii_digit()))
{
continue;
}
let upper_code = (2..=4).contains(&part.len())
&& part
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
&& part.chars().any(|c| c.is_ascii_uppercase());
let numeric_code = part.len() >= 6 && part.chars().all(|c| c.is_ascii_digit());
if (upper_code || numeric_code) && !out.iter().any(|o| o == part) {
out.push(part.to_owned());
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn statuses_parse() {
let s = Status::parse("Muss [10] ∧ [11]").unwrap();
assert_eq!(s.kind, StatusKind::Muss);
assert_eq!(s.expr.unwrap().to_string(), "[10] ∧ [11]");
assert_eq!(Status::parse("X").unwrap().expr, None);
assert!(Status::parse("[10] ∧").is_none());
let s = Status::parse("Muss [7] ∧ ([577] ⊻ [UB1])").unwrap();
assert_eq!(s.expr.unwrap().to_string(), "[7] ∧ ([577] ⊻ [UB1])");
let s = Status::parse("X [931] [494]").unwrap();
assert_eq!(s.expr.unwrap().to_string(), "[931] [494]");
}
#[test]
fn juxtaposition_binds_tighter_than_the_operators() {
let e = Expr::parse("[2] ∧ ([3] ∨ [4])[901] ∧ [555]").unwrap();
assert_eq!(e.to_string(), "[2] ∧ ([3] ∨ [4]) [901] ∧ [555]");
let Expr::And(v) = &e else { panic!("{e}") };
assert_eq!(v.len(), 3);
assert!(matches!(v[1], Expr::Then(_)), "{e}");
let e = Expr::parse("[5] ∨ [930][6]").unwrap();
let Expr::Or(v) = &e else { panic!("{e}") };
assert!(matches!(v[1], Expr::Then(_)), "{e}");
}
#[test]
fn truncated_expressions_are_refused() {
for text in [
"[321]) ∨",
"([67] ∧ ([529] ∨",
"([UB1] ∧ ⊻ [272]",
"[74]) ∨ ∧ [524]",
"∨ [508]",
"[492] ∧ [27] ∧",
] {
assert!(Expr::parse(text).is_err(), "{text:?} must not read");
}
assert_eq!(Expr::parse("[10] & [11]"), Err(ExprError::Stray('&')));
assert_eq!(Expr::parse("[10"), Err(ExprError::Unterminated));
assert_eq!(Expr::parse("([10]"), Err(ExprError::Parentheses));
}
fn answers(f: impl Fn(&str) -> Truth) -> impl FnMut(&str) -> Result<Truth, EvalError> {
move |c: &str| Ok(f(c))
}
#[test]
fn expressions_evaluate_with_neutral_hinweise() {
let e = Expr::parse("[10] ∧ [519]").unwrap();
let mut o = answers(|c| match c {
"10" => Truth::True,
"519" => Truth::Neutral,
_ => Truth::Unknown,
});
assert_eq!(e.eval(&mut o), Ok(Truth::True));
let e = Expr::parse("[10] ⊻ [11]").unwrap();
let mut o = answers(|c| if c == "10" { Truth::True } else { Truth::False });
assert_eq!(e.eval(&mut o), Ok(Truth::True));
let mut o = answers(|_| Truth::True);
assert_eq!(e.eval(&mut o), Ok(Truth::False));
let e = Expr::parse("[1] ∨ [2]").unwrap();
let mut o = answers(|c| {
if c == "1" {
Truth::Unknown
} else {
Truth::False
}
});
assert_eq!(e.eval(&mut o), Ok(Truth::Unknown));
let e = Expr::parse("[10] ∨ [519]").unwrap();
let mut o = answers(|c| match c {
"10" => Truth::False,
_ => Truth::Neutral,
});
assert_eq!(e.eval(&mut o), Ok(Truth::False));
let e = Expr::parse("[10] ∨ [931]").unwrap();
let mut o = answers(|c| match c {
"10" => Truth::True,
_ => Truth::Neutral,
});
assert_eq!(e.eval(&mut o), Err(EvalError::ChoiceOverNeutral));
let e = Expr::parse("[932] ⊻ [933]").unwrap();
let mut o = answers(|_| Truth::Neutral);
assert_eq!(e.eval(&mut o), Ok(Truth::Neutral));
}
#[test]
fn and_keeps_the_reference_truth_table() {
let e = Expr::parse("[1] ∧ [2]").unwrap();
for (a, b, want) in [
(Truth::Unknown, Truth::False, Truth::False),
(Truth::Unknown, Truth::True, Truth::Unknown),
(Truth::Neutral, Truth::True, Truth::True),
(Truth::Neutral, Truth::Neutral, Truth::Neutral),
(Truth::True, Truth::True, Truth::True),
] {
let mut o = answers(move |c| if c == "1" { a } else { b });
assert_eq!(e.eval(&mut o), Ok(want), "{a:?} ∧ {b:?}");
}
let e = Expr::parse("[1] ∨ [2]").unwrap();
for (a, b, want) in [
(Truth::Unknown, Truth::False, Truth::Unknown),
(Truth::Unknown, Truth::True, Truth::True),
(Truth::False, Truth::False, Truth::False),
] {
let mut o = answers(move |c| if c == "1" { a } else { b });
assert_eq!(e.eval(&mut o), Ok(want), "{a:?} ∨ {b:?}");
}
}
#[test]
fn paketmerkmale_read() {
assert_eq!(
Paket::parse("1P0..1"),
Some(Paket {
id: "1P".into(),
min: 0,
max: Some(1)
})
);
assert_eq!(
Paket::parse("10P1..1"),
Some(Paket {
id: "10P".into(),
min: 1,
max: Some(1)
})
);
assert_eq!(
Paket::parse("2P0..n"),
Some(Paket {
id: "2P".into(),
min: 0,
max: None
})
);
assert_eq!(Paket::parse("UB1"), None);
let e = Expr::parse("[2P1..2] ⊻ [3P0..2]").unwrap();
let ids: Vec<String> = e.pakete().into_iter().map(|p| p.id).collect();
assert_eq!(ids, ["2P", "3P"]);
}
#[test]
fn condition_kinds_by_range() {
assert_eq!(ConditionKind::of("10"), ConditionKind::Voraussetzung);
assert_eq!(ConditionKind::of("519"), ConditionKind::Hinweis);
assert_eq!(ConditionKind::of("931"), ConditionKind::Format);
assert_eq!(ConditionKind::of("2061"), ConditionKind::Wiederholbarkeit);
assert_eq!(ConditionKind::of("UB1"), ConditionKind::Zeitpunkt);
assert_eq!(ConditionKind::of("1P0..1"), ConditionKind::Paket);
}
fn seg(edi: &str) -> edifact_rs::OwnedSegment {
edifact_rs::from_bytes(edi.as_bytes())
.next()
.unwrap()
.unwrap()
.into_owned()
}
#[test]
fn segment_patterns_match_the_wire() {
let p = SegmentPattern::parse("STS+7++xxx+xxx+E01/E03").unwrap();
assert!(p.matches(&seg("STS+7++E01+ZW4+E03'")));
assert!(!p.matches(&seg("STS+7++E01+ZW4'")));
let p = SegmentPattern::parse("STS+7++xxx+ZAP").unwrap();
assert!(p.matches(&seg("STS+7++E01+ZAP'")));
assert!(!p.matches(&seg("STS+7++E01+ZW4'")));
let p = SegmentPattern::parse("CCI+Z61++ZF9").unwrap();
assert!(p.matches(&seg("CCI+Z61++ZF9'")));
let p = SegmentPattern::parse("PIA+5+1-0?:56.5.54").unwrap();
assert!(p.matches(&seg("PIA+5+1-0?:56.5.54'")));
let p = SegmentPattern::parse("SG4").is_none();
assert!(p);
}
#[test]
fn voraussetzungen_parse() {
let v = Voraussetzung::parse(
"Wenn SG4 STS+7++xxx+xxx+E01/E03 (Transaktionsgrund befristete Anmeldung) vorhanden",
)
.unwrap();
assert!(
matches!(v, Voraussetzung::Present { scope: Scope::Group(ref g), negate: false, .. } if g == "SG4")
);
let v = Voraussetzung::parse(
"Wenn in derselben SG10 das CCI+Z61++ZF9 (Kunde erfüllt …) vorhanden",
)
.unwrap();
assert!(
matches!(v, Voraussetzung::Present { scope: Scope::Group(ref g), .. } if g == "SG10")
);
let v = Voraussetzung::parse("Wenn SG10 QTY DE6063 mit Wert 220 vorhanden").unwrap();
assert!(
matches!(v, Voraussetzung::ElementValue { ref de, ref values, .. } if de == "6063" && values == &["220".to_owned()])
);
let v = Voraussetzung::parse("Wenn SG8 SEQ+ZH0 (Priorisierung erforderliches Produktpaket) mehr als einmal vorhanden").unwrap();
assert!(matches!(v, Voraussetzung::Count { more_than: 1, .. }));
let v =
Voraussetzung::parse("Wenn kein SG6 RFF+Z47 (Verwendungszeitraum) vorhanden").unwrap();
assert!(matches!(v, Voraussetzung::Present { negate: true, .. }));
let v = Voraussetzung::parse("Wenn SG4 STS+E01++A06 (Status der Antwort) nicht vorhanden")
.unwrap();
assert!(matches!(v, Voraussetzung::Present { negate: true, .. }));
assert!(Voraussetzung::parse("Wenn Anmeldung/Änderung befristet").is_none());
assert!(
Voraussetzung::parse("Hinweis: Wenn in der Anmeldung der Code ZAP vorhanden war")
.is_none()
);
}
}