1use super::context::EvaluationContext;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum ConditionResult {
11 True,
13 False,
15 Unknown,
17}
18
19impl ConditionResult {
20 pub fn is_true(self) -> bool {
22 matches!(self, ConditionResult::True)
23 }
24
25 pub fn is_false(self) -> bool {
27 matches!(self, ConditionResult::False)
28 }
29
30 pub fn is_unknown(self) -> bool {
32 matches!(self, ConditionResult::Unknown)
33 }
34
35 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 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 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 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
92pub trait ConditionEvaluator: Send + Sync {
98 fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult;
103
104 fn is_external(&self, condition: u32) -> bool;
107
108 fn is_known(&self, _condition: u32) -> bool {
116 false
117 }
118
119 fn message_type(&self) -> &str;
121
122 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
148pub trait ExternalConditionProvider: Send + Sync {
158 fn evaluate(&self, condition_name: &str) -> ConditionResult;
164
165 fn now(&self) -> chrono::DateTime<chrono::Utc> {
170 chrono::Utc::now()
171 }
172}
173
174pub struct NoOpExternalProvider;
179
180impl ExternalConditionProvider for NoOpExternalProvider {
181 fn evaluate(&self, _condition_name: &str) -> ConditionResult {
182 ConditionResult::Unknown
183 }
184}
185
186pub 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
207pub 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
227pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244pub struct Vorgang {
245 pub group: &'static str,
247 pub tag: &'static str,
249 pub element: usize,
251 pub component: usize,
252}
253
254pub 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
305pub 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
315pub 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
341pub 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
374pub 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
413pub 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
427pub fn counted_repetitions(message_type: &str) -> &'static [u32] {
434 match message_type {
435 "ORDERS" => &[2005, 2006],
436 _ => &[],
437 }
438}
439
440pub fn reference_conditions(message_type: &str) -> &'static [u32] {
446 match message_type {
447 "UTILMD_Strom" => &[22],
448 "UTILTS" => &[59],
449 _ => &[],
450 }
451}
452
453pub fn repetition_limits(message_type: &str) -> &'static [(u32, usize, Option<usize>)] {
461 match message_type {
462 "UTILMD_Strom" => &[(2317, 2, None), (2318, 1, Some(1))],
463 _ => &[],
464 }
465}
466
467pub fn breaks_repetition_limit<E: ConditionEvaluator + ?Sized>(
470 status: &str,
471 evaluator: &E,
472 ctx: &EvaluationContext,
473 count: usize,
474) -> bool {
475 repetition_limits(evaluator.message_type())
476 .iter()
477 .filter(|(id, _, _)| status.contains(&format!("[{id}]")))
478 .any(|&(id, min, max)| {
479 evaluator.evaluate(id, ctx) == ConditionResult::True
480 && (count < min || max.is_some_and(|max| count > max))
481 })
482}
483
484struct CountTarget<'a, E: ConditionEvaluator + ?Sized>(&'a E);
487
488impl<E: ConditionEvaluator + ?Sized> ConditionEvaluator for CountTarget<'_, E> {
489 fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
490 if counted_repetitions(self.0.message_type()).contains(&condition) {
491 return self.0.evaluate(condition, ctx);
492 }
493 PresentTarget(self.0).evaluate(condition, ctx)
494 }
495
496 fn is_external(&self, condition: u32) -> bool {
497 self.0.is_external(condition)
498 }
499
500 fn is_known(&self, condition: u32) -> bool {
501 self.0.is_known(condition)
502 }
503
504 fn message_type(&self) -> &str {
505 self.0.message_type()
506 }
507
508 fn format_version(&self) -> &str {
509 self.0.format_version()
510 }
511}
512
513pub fn refuses_count<E: ConditionEvaluator + ?Sized>(
519 status: &str,
520 evaluator: &E,
521 ctx: &EvaluationContext,
522 ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
523) -> bool {
524 let counted = counted_repetitions(evaluator.message_type());
525 let names_one = status.split('[').skip(1).any(|term| {
526 term.split(']')
527 .next()
528 .and_then(|id| id.parse::<u32>().ok())
529 .is_some_and(|id| counted.contains(&id))
530 });
531 names_one
532 && !refuses_presence(status, evaluator, ctx, ub_definitions)
533 && refuses(status, &CountTarget(evaluator), ctx, ub_definitions)
534}
535
536fn refuses<E: ConditionEvaluator>(
539 status: &str,
540 target: &E,
541 ctx: &EvaluationContext,
542 ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
543) -> bool {
544 let status = status.trim();
545 let package = status.split('[').skip(1).any(|term| {
546 term.trim_start_matches(|c: char| c.is_ascii_digit())
547 .starts_with('P')
548 });
549 if status.contains('\n') || !status.contains('[') || package {
550 return false;
551 }
552 let keyword = &status[..status
553 .find(|c: char| !c.is_alphabetic())
554 .unwrap_or(status.len())];
555 if !matches!(keyword, "Muss" | "X") {
556 return false;
557 }
558 super::ConditionExprEvaluator::new(target).evaluate_status_with_ub(status, ctx, ub_definitions)
559 == ConditionResult::False
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565
566 struct AlwaysTrue(&'static str);
567
568 impl ConditionEvaluator for AlwaysTrue {
569 fn evaluate(&self, _: u32, _: &EvaluationContext) -> ConditionResult {
570 ConditionResult::True
571 }
572 fn is_external(&self, _: u32) -> bool {
573 false
574 }
575 fn message_type(&self) -> &str {
576 self.0
577 }
578 fn format_version(&self) -> &str {
579 "FV2604"
580 }
581 }
582
583 #[test]
584 fn once_per_transaction_binds_where_every_line_has_it() {
585 assert!(binds_once("Muss [483] ∧ [2061]", 2061));
586 assert!(binds_once("Muss [2061]", 2061));
587 assert!(binds_once(
588 "Muss ([77] ∧ [78]) ∧ [347] ∧ [2061]\r\nKann [2061]",
589 2061
590 ));
591 assert!(!binds_once("Soll [165] ∧ (([2061] ∧ [583]) ∨ [584])", 2061));
592 assert!(!binds_once(
593 "Muss ([32] ∧ [2061] ∧ [651]) ⊻ ([200] ∧ [601])",
594 2061
595 ));
596 assert!(!binds_once("Muss [483]", 2061));
597 assert!(!binds_once("", 2061));
598 }
599
600 #[test]
601 fn absent_target_answers_only_the_presence_condition_with_false() {
602 let external = NoOpExternalProvider;
603 let ctx = EvaluationContext::new("55043", &external, &[]);
604 let inner = AlwaysTrue("UTILMD_Strom");
605 let absent = AbsentTarget(&inner);
606 assert_eq!(absent.evaluate(166, &ctx), ConditionResult::False);
607 assert_eq!(absent.evaluate(674, &ctx), ConditionResult::True);
608 assert_eq!(absent.evaluate(2003, &ctx), ConditionResult::False);
610
611 let invoic = AlwaysTrue("INVOIC");
613 assert_eq!(
614 AbsentTarget(&invoic).evaluate(166, &ctx),
615 ConditionResult::True
616 );
617 assert_eq!(
618 AbsentTarget(&invoic).evaluate(22, &ctx),
619 ConditionResult::False
620 );
621 }
622
623 #[test]
624 fn test_condition_result_is_methods() {
625 assert!(ConditionResult::True.is_true());
626 assert!(!ConditionResult::True.is_false());
627 assert!(!ConditionResult::True.is_unknown());
628
629 assert!(!ConditionResult::False.is_true());
630 assert!(ConditionResult::False.is_false());
631
632 assert!(ConditionResult::Unknown.is_unknown());
633 }
634
635 #[test]
636 fn three_valued_and_or_not() {
637 use ConditionResult::{False as F, True as T, Unknown as U};
638 assert_eq!(T.and(T), T);
639 assert_eq!(T.and(U), U);
640 assert_eq!(U.and(F), F);
641 assert_eq!(F.or(F), F);
642 assert_eq!(F.or(U), U);
643 assert_eq!(U.or(T), T);
644 assert_eq!(U.negate(), U);
645 assert_eq!(T.negate(), F);
646 }
647
648 #[test]
649 fn test_condition_result_to_option() {
650 assert_eq!(ConditionResult::True.to_option(), Some(true));
651 assert_eq!(ConditionResult::False.to_option(), Some(false));
652 assert_eq!(ConditionResult::Unknown.to_option(), None);
653 }
654
655 #[test]
656 fn test_condition_result_from_bool() {
657 assert_eq!(ConditionResult::from(true), ConditionResult::True);
658 assert_eq!(ConditionResult::from(false), ConditionResult::False);
659 }
660
661 #[test]
662 fn test_condition_result_display() {
663 assert_eq!(format!("{}", ConditionResult::True), "True");
664 assert_eq!(format!("{}", ConditionResult::False), "False");
665 assert_eq!(format!("{}", ConditionResult::Unknown), "Unknown");
666 }
667
668 #[test]
669 fn test_noop_external_provider() {
670 let provider = NoOpExternalProvider;
671 assert_eq!(
672 provider.evaluate("MessageSplitting"),
673 ConditionResult::Unknown
674 );
675 assert_eq!(provider.evaluate("anything"), ConditionResult::Unknown);
676 }
677}