Skip to main content

automapper_validation/eval/
evaluator.rs

1//! Core condition evaluation traits.
2
3use super::context::EvaluationContext;
4
5/// Three-valued result of evaluating a single condition.
6///
7/// Unlike the C# implementation which uses `bool`, we use three-valued logic
8/// to support partial evaluation when external conditions are unavailable.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum ConditionResult {
11    /// The condition is satisfied.
12    True,
13    /// The condition is not satisfied.
14    False,
15    /// The condition cannot be determined (e.g., external condition without a provider).
16    Unknown,
17}
18
19impl ConditionResult {
20    /// Returns `true` if this is `ConditionResult::True`.
21    pub fn is_true(self) -> bool {
22        matches!(self, ConditionResult::True)
23    }
24
25    /// Returns `true` if this is `ConditionResult::False`.
26    pub fn is_false(self) -> bool {
27        matches!(self, ConditionResult::False)
28    }
29
30    /// Returns `true` if this is `ConditionResult::Unknown`.
31    pub fn is_unknown(self) -> bool {
32        matches!(self, ConditionResult::Unknown)
33    }
34
35    /// Three-valued AND: `False` if either is, `True` if both are, else `Unknown`.
36    pub fn and(self, other: ConditionResult) -> ConditionResult {
37        match (self, other) {
38            (ConditionResult::False, _) | (_, ConditionResult::False) => ConditionResult::False,
39            (ConditionResult::True, ConditionResult::True) => ConditionResult::True,
40            _ => ConditionResult::Unknown,
41        }
42    }
43
44    /// Three-valued OR: `True` if either is, `False` if both are, else `Unknown`.
45    pub fn or(self, other: ConditionResult) -> ConditionResult {
46        match (self, other) {
47            (ConditionResult::True, _) | (_, ConditionResult::True) => ConditionResult::True,
48            (ConditionResult::False, ConditionResult::False) => ConditionResult::False,
49            _ => ConditionResult::Unknown,
50        }
51    }
52
53    /// Three-valued NOT.
54    pub fn negate(self) -> ConditionResult {
55        match self {
56            ConditionResult::True => ConditionResult::False,
57            ConditionResult::False => ConditionResult::True,
58            ConditionResult::Unknown => ConditionResult::Unknown,
59        }
60    }
61
62    /// Converts to `Option<bool>`: True -> Some(true), False -> Some(false), Unknown -> None.
63    pub fn to_option(self) -> Option<bool> {
64        match self {
65            ConditionResult::True => Some(true),
66            ConditionResult::False => Some(false),
67            ConditionResult::Unknown => None,
68        }
69    }
70}
71
72impl From<bool> for ConditionResult {
73    fn from(value: bool) -> Self {
74        if value {
75            ConditionResult::True
76        } else {
77            ConditionResult::False
78        }
79    }
80}
81
82impl std::fmt::Display for ConditionResult {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        match self {
85            ConditionResult::True => write!(f, "True"),
86            ConditionResult::False => write!(f, "False"),
87            ConditionResult::Unknown => write!(f, "Unknown"),
88        }
89    }
90}
91
92/// Evaluates individual AHB conditions by number.
93///
94/// Implementations are typically generated from AHB XML schemas (one per
95/// message type and format version). Each condition number maps to a
96/// specific business rule check.
97pub trait ConditionEvaluator: Send + Sync {
98    /// Evaluate a single condition by number.
99    ///
100    /// Returns `ConditionResult::Unknown` for unrecognized condition numbers
101    /// or conditions that require unavailable external context.
102    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult;
103
104    /// Returns `true` if the given condition requires external context
105    /// (i.e., cannot be determined from the EDIFACT message alone).
106    fn is_external(&self, condition: u32) -> bool;
107
108    /// Returns `true` if this evaluator has an implementation for the given
109    /// condition number (whether internal or external). Conditions that fall
110    /// through to the `_ => Unknown` wildcard return `false`.
111    ///
112    /// This allows distinguishing "implemented but returned Unknown because
113    /// the relevant data isn't present in the message" from "not implemented
114    /// at all".
115    fn is_known(&self, _condition: u32) -> bool {
116        false
117    }
118
119    /// Returns the message type this evaluator handles (e.g., "UTILMD").
120    fn message_type(&self) -> &str;
121
122    /// Returns the format version this evaluator handles (e.g., "FV2510").
123    fn format_version(&self) -> &str;
124}
125
126impl<T: ConditionEvaluator + ?Sized> ConditionEvaluator for std::sync::Arc<T> {
127    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
128        (**self).evaluate(condition, ctx)
129    }
130
131    fn is_external(&self, condition: u32) -> bool {
132        (**self).is_external(condition)
133    }
134
135    fn is_known(&self, condition: u32) -> bool {
136        (**self).is_known(condition)
137    }
138
139    fn message_type(&self) -> &str {
140        (**self).message_type()
141    }
142
143    fn format_version(&self) -> &str {
144        (**self).format_version()
145    }
146}
147
148/// Provider for external conditions that depend on context outside the EDIFACT message.
149///
150/// External conditions are things like:
151/// - [1] "Wenn Aufteilung vorhanden" (message splitting status)
152/// - [14] "Wenn Datum bekannt" (whether a date is known)
153/// - [30] "Wenn Antwort auf Aktivierung" (response to activation)
154///
155/// These cannot be determined from the EDIFACT content alone and require
156/// business context from the calling system.
157pub trait ExternalConditionProvider: Send + Sync {
158    /// Evaluate an external condition by name.
159    ///
160    /// The `condition_name` corresponds to the speaking name from the
161    /// generated external conditions constants (e.g., "MessageSplitting",
162    /// "DateKnown").
163    fn evaluate(&self, condition_name: &str) -> ConditionResult;
164
165    /// The present moment, for conditions that compare against it: UTILMD
166    /// `[494]` "das hier genannte Datum muss der Zeitpunkt sein, zu dem das
167    /// Dokument erstellt wurde, oder ein Zeitpunkt, der davor liegt". The
168    /// wall clock by default; a caller on a simulated clock overrides it.
169    fn now(&self) -> chrono::DateTime<chrono::Utc> {
170        chrono::Utc::now()
171    }
172}
173
174/// A no-op external condition provider that returns `Unknown` for everything.
175///
176/// Useful when no external context is available — conditions will propagate
177/// as `Unknown` through the expression evaluator.
178pub struct NoOpExternalProvider;
179
180impl ExternalConditionProvider for NoOpExternalProvider {
181    fn evaluate(&self, _condition_name: &str) -> ConditionResult {
182        ConditionResult::Unknown
183    }
184}
185
186/// The number of a message type's bare "Wenn vorhanden" condition, if it has one.
187///
188/// The condition is self-referential: its value is whether the element or
189/// group it annotates is present. `Soll [166]` on a group means "send it if
190/// you have it", which only the sender knows, so an absent group annotated
191/// with it is never missing. The generated evaluators answer it with `True`
192/// ("the rule applies wherever it is evaluated"), which is right for a present
193/// element and wrong for an absent one — see [`AbsentTarget`].
194///
195/// The numbers are stable across every format version in
196/// `xml-migs-and-ahbs/` (FV2410–FV2610); keyed by [`ConditionEvaluator::message_type`]
197/// rather than by evaluator so aliased and regenerated evaluators need no edit.
198pub fn presence_condition(message_type: &str) -> Option<u32> {
199    match message_type {
200        "UTILMD_Strom" | "UTILMD_Gas" => Some(166),
201        "INVOIC" => Some(22),
202        "ORDERS" => Some(12),
203        _ => None,
204    }
205}
206
207/// Every condition of a message type that answers "does the annotated thing
208/// exist?", which an absent element or group answers with no.
209///
210/// The [`presence_condition`], plus conditions that ask for one repetition per
211/// real-world object the message cannot see. UTILMD_Strom [2003] ("Einmal für
212/// jede ruhende Marktlokation, die der Marktlokation »Kundenanlage« …
213/// untergeordnet ist", FV2504–FV2610, absent in Gas) is `Soll` on the ruhende
214/// Marktlokation: zero ruhende MaLos means zero groups, and only the sender
215/// knows how many exist. The generated evaluators cannot decide it and answer
216/// `Unknown`, which reported every message without a ruhende MaLo.
217pub fn presence_conditions(message_type: &str) -> &'static [u32] {
218    match message_type {
219        "UTILMD_Strom" => &[166, 2003],
220        "UTILMD_Gas" => &[166],
221        "INVOIC" => &[22],
222        "ORDERS" => &[12],
223        _ => &[],
224    }
225}
226
227/// The condition of a message type that allows a group or segment once per
228/// transaction, and the transaction group: UTILMD's `[2061]` "Segment bzw.
229/// Segmentgruppe ist genau einmal je SG4 IDE (Vorgang) anzugeben" (Strom and
230/// Gas, FV2504–FV2610). The conditions evaluate it as `True` wherever there is
231/// a transaction, so what it limits — the count — is checked on its own; see
232/// [`binds_once`].
233pub fn once_per_transaction(message_type: &str) -> Option<(u32, &'static str)> {
234    match message_type {
235        "UTILMD_Strom" | "UTILMD_Gas" => Some((2061, "SG4")),
236        _ => None,
237    }
238}
239
240/// A transaction (Vorgang) of a message type: the top-level group it repeats
241/// as, and where that group's entry segment carries the transaction's
242/// reference, which an APERAK names in SG5 RFF+TN DE1154.
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244pub struct Vorgang {
245    /// The group (`SG4`).
246    pub group: &'static str,
247    /// Its entry segment (`IDE`).
248    pub tag: &'static str,
249    /// The reference's element and component in that segment.
250    pub element: usize,
251    pub component: usize,
252}
253
254/// The transactions of a message type, as the APERAK MIG lists them (SG5
255/// RFF+TN DE1154, FV2504–FV2610):
256///
257/// - IFTSTA: SG4 EQD DE8260, or SG14 CNI DE1490 — each Prüfidentifikator has
258///   one of the two;
259/// - INSRPT: SG3 DOC DE1004;
260/// - UTILMD: SG4 IDE DE7402;
261/// - UTILTS: SG5 IDE DE7402.
262///
263/// Every other message type has none. `message_type` is a variant
264/// (`UTILMD_Strom`) or a message type (`UTILMD`).
265pub fn transactions(message_type: &str) -> &'static [Vorgang] {
266    const IDE_SG4: Vorgang = Vorgang {
267        group: "SG4",
268        tag: "IDE",
269        element: 1,
270        component: 0,
271    };
272    const IDE_SG5: Vorgang = Vorgang {
273        group: "SG5",
274        tag: "IDE",
275        element: 1,
276        component: 0,
277    };
278    const DOC: Vorgang = Vorgang {
279        group: "SG3",
280        tag: "DOC",
281        element: 1,
282        component: 0,
283    };
284    const EQD: Vorgang = Vorgang {
285        group: "SG4",
286        tag: "EQD",
287        element: 1,
288        component: 0,
289    };
290    const CNI: Vorgang = Vorgang {
291        group: "SG14",
292        tag: "CNI",
293        element: 0,
294        component: 0,
295    };
296    match message_type.split('_').next().unwrap_or_default() {
297        "UTILMD" => &[IDE_SG4],
298        "UTILTS" => &[IDE_SG5],
299        "INSRPT" => &[DOC],
300        "IFTSTA" => &[EQD, CNI],
301        _ => &[],
302    }
303}
304
305/// Whether `group_id` is a transaction group of `message_type`, whose
306/// conditions the AHB states per transaction. The validator evaluates each
307/// repetition against the message as that transaction sees it
308/// (`mig_assembly::transaction_view`, #337).
309pub fn is_transaction_group(message_type: &str, group_id: &str) -> bool {
310    transactions(message_type)
311        .iter()
312        .any(|v| v.group == group_id)
313}
314
315/// Whether `status` limits what it annotates to once per transaction: the
316/// condition `once` is a conjunct of every line (`Muss [483] ∧ [2061]`). Where
317/// it sits in one branch only (`Soll [165] ∧ (([2061] ∧ [583]) ∨ [584])`), the
318/// other branch allows more.
319pub fn binds_once(status: &str, once: u32) -> bool {
320    fn conjunct(expr: &crate::expr::ConditionExpr, once: u32) -> bool {
321        match expr {
322            crate::expr::ConditionExpr::Ref(id) => *id == once,
323            crate::expr::ConditionExpr::And(exprs) => exprs.iter().any(|e| conjunct(e, once)),
324            _ => false,
325        }
326    }
327    let lines: Vec<&str> = status
328        .lines()
329        .map(str::trim)
330        .filter(|l| !l.is_empty())
331        .collect();
332    !lines.is_empty()
333        && lines.iter().all(|line| {
334            matches!(
335                crate::expr::ConditionParser::parse(line),
336                Ok(Some(expr)) if conjunct(&expr, once)
337            )
338        })
339}
340
341/// Evaluates conditions for an element or group that is known to be absent.
342///
343/// Identical to the wrapped evaluator except that the message type's
344/// [`presence_conditions`] are `False`: whatever the condition annotates is not
345/// there. Use it wherever a status is evaluated to decide whether something
346/// missing is required.
347pub struct AbsentTarget<'a, E: ConditionEvaluator + ?Sized>(pub &'a E);
348
349impl<E: ConditionEvaluator + ?Sized> ConditionEvaluator for AbsentTarget<'_, E> {
350    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
351        if presence_conditions(self.0.message_type()).contains(&condition) {
352            return ConditionResult::False;
353        }
354        self.0.evaluate(condition, ctx)
355    }
356
357    fn is_external(&self, condition: u32) -> bool {
358        self.0.is_external(condition)
359    }
360
361    fn is_known(&self, condition: u32) -> bool {
362        self.0.is_known(condition)
363    }
364
365    fn message_type(&self) -> &str {
366        self.0.message_type()
367    }
368
369    fn format_version(&self) -> &str {
370        self.0.format_version()
371    }
372}
373
374/// Evaluates conditions for an element or group that is known to be present,
375/// to decide whether it is allowed there.
376///
377/// The message type's [`presence_conditions`] are `True`: whatever the
378/// condition annotates is there. Only the AHB's conditions proper (`[1]` to
379/// `[499]`) decide; notes (`[500]`–`[899]`), formats (`[900]`–`[999]`) and
380/// repetition rules (`[2000]` and up: "genau einmal je SG8 …") say nothing about
381/// whether something may be sent, and are `Unknown` — so `[197] ∧ [2308]` is
382/// false when `[197]` is, and `[2317] ⊻ [2318]` is never.
383pub struct PresentTarget<'a, E: ConditionEvaluator + ?Sized>(pub &'a E);
384
385impl<E: ConditionEvaluator + ?Sized> ConditionEvaluator for PresentTarget<'_, E> {
386    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
387        if presence_conditions(self.0.message_type()).contains(&condition) {
388            return ConditionResult::True;
389        }
390        if !(1..500).contains(&condition) {
391            return ConditionResult::Unknown;
392        }
393        self.0.evaluate(condition, ctx)
394    }
395
396    fn is_external(&self, condition: u32) -> bool {
397        self.0.is_external(condition)
398    }
399
400    fn is_known(&self, condition: u32) -> bool {
401        self.0.is_known(condition)
402    }
403
404    fn message_type(&self) -> &str {
405        self.0.message_type()
406    }
407
408    fn format_version(&self) -> &str {
409        self.0.format_version()
410    }
411}
412
413/// Whether something present under `status` is not allowed where `ctx` is: a
414/// `Muss`/`X` status with a condition that evaluates to false through
415/// [`PresentTarget`]. `Soll`/`Kann`, an unconditional status, an undecidable
416/// condition, a package (`[19P1..1]`, judged by its own check) and a status of
417/// several lines refuse nothing.
418pub fn refuses_presence<E: ConditionEvaluator + ?Sized>(
419    status: &str,
420    evaluator: &E,
421    ctx: &EvaluationContext,
422    ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
423) -> bool {
424    refuses(status, &PresentTarget(evaluator), ctx, ub_definitions)
425}
426
427/// The repetition conditions of a message type whose evaluators count what
428/// they limit in the group they name, so that a false one means "sent too
429/// often there": ORDERS `[2005]` / `[2006]`, "Pro SG29 LIN ist die SG34
430/// RFF+Z09 (Gerätenummer) genau einmal / bis zu dreimal anzugeben". Most
431/// `[2000]`+ evaluators still count over the whole message; a condition joins
432/// this list once its evaluator is verified to count per instance.
433pub fn counted_repetitions(message_type: &str) -> &'static [u32] {
434    match message_type {
435        "ORDERS" => &[2005, 2006],
436        _ => &[],
437    }
438}
439
440/// The conditions of a message type that say which value a reference holds:
441/// UTILMD Strom `[22]` and UTILTS `[59]`, "Es ist die Zeitraum-ID vom DE1156
442/// aus einem passenden SG6 RFF+Z49 (…) einzutragen" on the SEQ's DE1050. One
443/// that is false names a Zeitraum the transaction does not have: the
444/// reference is wrong, not merely not required (APERAK Z21).
445pub fn reference_conditions(message_type: &str) -> &'static [u32] {
446    match message_type {
447        "UTILMD_Strom" => &[22],
448        "UTILTS" => &[59],
449        _ => &[],
450    }
451}
452
453/// The pairs of presence conditions that make two parts of a message
454/// alternatives: each part is `Muss` only where the other is absent. UTILMD
455/// Strom FV2610 `[80]`/`[81]`: the PIA+5 of an SG8 SEQ+Z61 "wenn … das SG10
456/// CCI+11 … nicht vorhanden", the CCI+11 (Kein Produkt zugeordnet) "wenn …
457/// das PIA+5 … nicht vorhanden". Both sent, neither can be ignored as
458/// superfluous content: the message says two contradicting things, an
459/// [`ExclusiveAlternatives`](crate::validator::IssueKind::ExclusiveAlternatives)
460/// error (mako-twin, 0.50.5). Found in the AHBs by status (A `Muss [x]`, B
461/// `Muss [y]` in one group, `[x]` naming B absent and `[y]` A) and checked by
462/// their texts.
463pub fn exclusive_conditions(format_version: &str, message_type: &str) -> &'static [(u32, u32)] {
464    let up_to_2604 = matches!(format_version, "FV2504" | "FV2510" | "FV2604");
465    match message_type {
466        // SG7 STS+Z01/Z02/Z03; SG14 STS+Z37/Z38.
467        "IFTSTA" => &[(3, 4), (4, 5), (78, 79)],
468        // SG26 DTM+155/156 and DTM+203.
469        "INVOIC" => &[(9, 10)],
470        // DTM+203 and SG1 RFF+Z41.
471        "ORDERS" => &[(122, 123)],
472        // FTX+Z27 and FTX+Z28.
473        "ORDRSP" => &[(76, 77)],
474        // SG31 RNG+Z03 and RNG+Z04 (not in FV2504).
475        "QUOTES" if format_version != "FV2504" => &[(93, 94)],
476        // DTM+203 and DTM+469; DTM+76 and SG1 RFF+Z41.
477        "REQOTE" => &[(1, 2), (15, 16)],
478        // SG4 DTM+93 and DTM+471.
479        "UTILMD_Gas" => &[(12, 18)],
480        // SG4 DTM+93 and DTM+471; PIA+5 and SG10 CCI+11 of the product
481        // SG8s: Marktlokation (111/112), Netzlokation (444/445), Steuerbare
482        // Ressource (80/81) and Netzlokation OBIS (158/159).
483        "UTILMD_Strom" if up_to_2604 => &[(12, 18), (111, 112), (444, 445)],
484        "UTILMD_Strom" if format_version == "FV2610" => {
485            &[(12, 18), (80, 81), (158, 159), (444, 445)]
486        }
487        _ => &[],
488    }
489}
490
491/// The repetition conditions of a message type that allow a top-level group
492/// once per message, as `(condition, group, kind)`: ORDERS `[2050]` "Pro
493/// Nachricht ist die SG29 genau einmal anzugeben", and `[2060]`–`[2064]`
494/// "… die SG29 LIN++Z64 … maximal einmal", which count only the repetitions
495/// whose entry (LIN DE1229) holds `kind`. Each repetition of another kind sees
496/// the whole message, so its presence conditions (`[135]` "Wenn SG29 LIN++Z64
497/// nicht vorhanden") refuse a second line only as superfluous content (AHB002,
498/// no APERAK error); two of one kind refused nothing at all. Counted by the
499/// validator ([`RepetitionCount`](crate::validator::IssueKind::RepetitionCount)).
500pub fn message_repetition_limits(
501    message_type: &str,
502) -> &'static [(u32, &'static str, Option<&'static str>)] {
503    const PER_LINE_SG29: [(u32, &str, Option<&str>); 5] = [
504        (2060, "SG29", Some("Z64")),
505        (2061, "SG29", Some("Z65")),
506        (2062, "SG29", Some("Z66")),
507        (2063, "SG29", Some("Z67")),
508        (2064, "SG29", Some("Z68")),
509    ];
510    const PER_LINE_SG27: [(u32, &str, Option<&str>); 5] = [
511        (2060, "SG27", Some("Z64")),
512        (2061, "SG27", Some("Z65")),
513        (2062, "SG27", Some("Z66")),
514        (2063, "SG27", Some("Z67")),
515        (2064, "SG27", Some("Z68")),
516    ];
517    const ORDERS: [(u32, &str, Option<&str>); 7] = [
518        (2050, "SG29", None),
519        (2092, "SG29", None),
520        PER_LINE_SG29[0],
521        PER_LINE_SG29[1],
522        PER_LINE_SG29[2],
523        PER_LINE_SG29[3],
524        PER_LINE_SG29[4],
525    ];
526    const REQOTE: [(u32, &str, Option<&str>); 6] = [
527        (2005, "SG27", None),
528        PER_LINE_SG27[0],
529        PER_LINE_SG27[1],
530        PER_LINE_SG27[2],
531        PER_LINE_SG27[3],
532        PER_LINE_SG27[4],
533    ];
534    match message_type {
535        "ORDERS" => &ORDERS,
536        "QUOTES" => &PER_LINE_SG27,
537        "REQOTE" => &REQOTE,
538        "ORDRSP" => &[(2036, "SG27", None)],
539        _ => &[],
540    }
541}
542
543/// The repetition conditions of a message type that set how often a segment
544/// is sent in its group instance where they apply, as `(condition, min,
545/// max)`: UTILMD Strom `[2317]` "… CAV+MIW/MPW/MUW vorhanden, ist das Segment
546/// mindestens zweimal anzugeben" and `[2318]` "… CAV+MBW vorhanden, … genau
547/// einmal" on the Wandler's CAV+Z30. Their evaluators say whether one applies
548/// there (which also makes the segment required); the count is judged by
549/// [`breaks_repetition_limit`].
550pub fn repetition_limits(message_type: &str) -> &'static [(u32, usize, Option<usize>)] {
551    match message_type {
552        "UTILMD_Strom" => &[(2317, 2, None), (2318, 1, Some(1))],
553        _ => &[],
554    }
555}
556
557/// Whether `count` segments under `status` break a [`repetition_limits`]
558/// condition the status names and that applies where `ctx` is.
559pub fn breaks_repetition_limit<E: ConditionEvaluator + ?Sized>(
560    status: &str,
561    evaluator: &E,
562    ctx: &EvaluationContext,
563    count: usize,
564) -> bool {
565    repetition_limits(evaluator.message_type())
566        .iter()
567        .filter(|(id, _, _)| status.contains(&format!("[{id}]")))
568        .any(|&(id, min, max)| {
569            evaluator.evaluate(id, ctx) == ConditionResult::True
570                && (count < min || max.is_some_and(|max| count > max))
571        })
572}
573
574/// [`PresentTarget`], except that the message type's
575/// [`counted_repetitions`] are evaluated.
576struct CountTarget<'a, E: ConditionEvaluator + ?Sized>(&'a E);
577
578impl<E: ConditionEvaluator + ?Sized> ConditionEvaluator for CountTarget<'_, E> {
579    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
580        if counted_repetitions(self.0.message_type()).contains(&condition) {
581            return self.0.evaluate(condition, ctx);
582        }
583        PresentTarget(self.0).evaluate(condition, ctx)
584    }
585
586    fn is_external(&self, condition: u32) -> bool {
587        self.0.is_external(condition)
588    }
589
590    fn is_known(&self, condition: u32) -> bool {
591        self.0.is_known(condition)
592    }
593
594    fn message_type(&self) -> &str {
595        self.0.message_type()
596    }
597
598    fn format_version(&self) -> &str {
599        self.0.format_version()
600    }
601}
602
603/// Whether something present under `status` is sent more often than it allows
604/// where `ctx` is: [`refuses_presence`] does not refuse it, but does once the
605/// message type's [`counted_repetitions`] are evaluated. ORDERS 17009's SG34
606/// `Muss ([102] ∧ [2006]) ⊻ ([103] ∧ [2005])` holds for the second RFF+Z09
607/// of a Zähleinrichtung as far as presence goes; `[2005]` refuses it.
608pub fn refuses_count<E: ConditionEvaluator + ?Sized>(
609    status: &str,
610    evaluator: &E,
611    ctx: &EvaluationContext,
612    ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
613) -> bool {
614    let counted = counted_repetitions(evaluator.message_type());
615    let names_one = status.split('[').skip(1).any(|term| {
616        term.split(']')
617            .next()
618            .and_then(|id| id.parse::<u32>().ok())
619            .is_some_and(|id| counted.contains(&id))
620    });
621    names_one
622        && !refuses_presence(status, evaluator, ctx, ub_definitions)
623        && refuses(status, &CountTarget(evaluator), ctx, ub_definitions)
624}
625
626/// Whether a `Muss`/`X` `status` evaluates to false through `target`; see
627/// [`refuses_presence`] for what refuses nothing.
628fn refuses<E: ConditionEvaluator>(
629    status: &str,
630    target: &E,
631    ctx: &EvaluationContext,
632    ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
633) -> bool {
634    let status = status.trim();
635    let package = status.split('[').skip(1).any(|term| {
636        term.trim_start_matches(|c: char| c.is_ascii_digit())
637            .starts_with('P')
638    });
639    if status.contains('\n') || !status.contains('[') || package {
640        return false;
641    }
642    let keyword = &status[..status
643        .find(|c: char| !c.is_alphabetic())
644        .unwrap_or(status.len())];
645    if !matches!(keyword, "Muss" | "X") {
646        return false;
647    }
648    super::ConditionExprEvaluator::new(target).evaluate_status_with_ub(status, ctx, ub_definitions)
649        == ConditionResult::False
650}
651
652#[cfg(test)]
653mod tests {
654    use super::*;
655
656    struct AlwaysTrue(&'static str);
657
658    impl ConditionEvaluator for AlwaysTrue {
659        fn evaluate(&self, _: u32, _: &EvaluationContext) -> ConditionResult {
660            ConditionResult::True
661        }
662        fn is_external(&self, _: u32) -> bool {
663            false
664        }
665        fn message_type(&self) -> &str {
666            self.0
667        }
668        fn format_version(&self) -> &str {
669            "FV2604"
670        }
671    }
672
673    #[test]
674    fn once_per_transaction_binds_where_every_line_has_it() {
675        assert!(binds_once("Muss [483] ∧ [2061]", 2061));
676        assert!(binds_once("Muss [2061]", 2061));
677        assert!(binds_once(
678            "Muss ([77] ∧ [78]) ∧ [347] ∧ [2061]\r\nKann [2061]",
679            2061
680        ));
681        assert!(!binds_once("Soll [165] ∧ (([2061] ∧ [583]) ∨ [584])", 2061));
682        assert!(!binds_once(
683            "Muss ([32] ∧ [2061] ∧ [651]) ⊻ ([200] ∧ [601])",
684            2061
685        ));
686        assert!(!binds_once("Muss [483]", 2061));
687        assert!(!binds_once("", 2061));
688    }
689
690    #[test]
691    fn absent_target_answers_only_the_presence_condition_with_false() {
692        let external = NoOpExternalProvider;
693        let ctx = EvaluationContext::new("55043", &external, &[]);
694        let inner = AlwaysTrue("UTILMD_Strom");
695        let absent = AbsentTarget(&inner);
696        assert_eq!(absent.evaluate(166, &ctx), ConditionResult::False);
697        assert_eq!(absent.evaluate(674, &ctx), ConditionResult::True);
698        // [2003]: one group per ruhende Marktlokation the sender knows of.
699        assert_eq!(absent.evaluate(2003, &ctx), ConditionResult::False);
700
701        // [166] of another message type is an unrelated condition.
702        let invoic = AlwaysTrue("INVOIC");
703        assert_eq!(
704            AbsentTarget(&invoic).evaluate(166, &ctx),
705            ConditionResult::True
706        );
707        assert_eq!(
708            AbsentTarget(&invoic).evaluate(22, &ctx),
709            ConditionResult::False
710        );
711    }
712
713    #[test]
714    fn test_condition_result_is_methods() {
715        assert!(ConditionResult::True.is_true());
716        assert!(!ConditionResult::True.is_false());
717        assert!(!ConditionResult::True.is_unknown());
718
719        assert!(!ConditionResult::False.is_true());
720        assert!(ConditionResult::False.is_false());
721
722        assert!(ConditionResult::Unknown.is_unknown());
723    }
724
725    #[test]
726    fn three_valued_and_or_not() {
727        use ConditionResult::{False as F, True as T, Unknown as U};
728        assert_eq!(T.and(T), T);
729        assert_eq!(T.and(U), U);
730        assert_eq!(U.and(F), F);
731        assert_eq!(F.or(F), F);
732        assert_eq!(F.or(U), U);
733        assert_eq!(U.or(T), T);
734        assert_eq!(U.negate(), U);
735        assert_eq!(T.negate(), F);
736    }
737
738    #[test]
739    fn test_condition_result_to_option() {
740        assert_eq!(ConditionResult::True.to_option(), Some(true));
741        assert_eq!(ConditionResult::False.to_option(), Some(false));
742        assert_eq!(ConditionResult::Unknown.to_option(), None);
743    }
744
745    #[test]
746    fn test_condition_result_from_bool() {
747        assert_eq!(ConditionResult::from(true), ConditionResult::True);
748        assert_eq!(ConditionResult::from(false), ConditionResult::False);
749    }
750
751    #[test]
752    fn test_condition_result_display() {
753        assert_eq!(format!("{}", ConditionResult::True), "True");
754        assert_eq!(format!("{}", ConditionResult::False), "False");
755        assert_eq!(format!("{}", ConditionResult::Unknown), "Unknown");
756    }
757
758    #[test]
759    fn test_noop_external_provider() {
760        let provider = NoOpExternalProvider;
761        assert_eq!(
762            provider.evaluate("MessageSplitting"),
763            ConditionResult::Unknown
764        );
765        assert_eq!(provider.evaluate("anything"), ConditionResult::Unknown);
766    }
767}