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 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 "IFTSTA" => &[(3, 4), (4, 5), (78, 79)],
468 "INVOIC" => &[(9, 10)],
470 "ORDERS" => &[(122, 123)],
472 "ORDRSP" => &[(76, 77)],
474 "QUOTES" if format_version != "FV2504" => &[(93, 94)],
476 "REQOTE" => &[(1, 2), (15, 16)],
478 "UTILMD_Gas" => &[(12, 18)],
480 "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
491pub 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
543pub 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
557pub 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
574struct 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
603pub 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
626fn 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 assert_eq!(absent.evaluate(2003, &ctx), ConditionResult::False);
700
701 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}