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 message_repetition_limits(
463 message_type: &str,
464) -> &'static [(u32, &'static str, Option<&'static str>)] {
465 const PER_LINE_SG29: [(u32, &str, Option<&str>); 5] = [
466 (2060, "SG29", Some("Z64")),
467 (2061, "SG29", Some("Z65")),
468 (2062, "SG29", Some("Z66")),
469 (2063, "SG29", Some("Z67")),
470 (2064, "SG29", Some("Z68")),
471 ];
472 const PER_LINE_SG27: [(u32, &str, Option<&str>); 5] = [
473 (2060, "SG27", Some("Z64")),
474 (2061, "SG27", Some("Z65")),
475 (2062, "SG27", Some("Z66")),
476 (2063, "SG27", Some("Z67")),
477 (2064, "SG27", Some("Z68")),
478 ];
479 const ORDERS: [(u32, &str, Option<&str>); 7] = [
480 (2050, "SG29", None),
481 (2092, "SG29", None),
482 PER_LINE_SG29[0],
483 PER_LINE_SG29[1],
484 PER_LINE_SG29[2],
485 PER_LINE_SG29[3],
486 PER_LINE_SG29[4],
487 ];
488 const REQOTE: [(u32, &str, Option<&str>); 6] = [
489 (2005, "SG27", None),
490 PER_LINE_SG27[0],
491 PER_LINE_SG27[1],
492 PER_LINE_SG27[2],
493 PER_LINE_SG27[3],
494 PER_LINE_SG27[4],
495 ];
496 match message_type {
497 "ORDERS" => &ORDERS,
498 "QUOTES" => &PER_LINE_SG27,
499 "REQOTE" => &REQOTE,
500 "ORDRSP" => &[(2036, "SG27", None)],
501 _ => &[],
502 }
503}
504
505pub fn repetition_limits(message_type: &str) -> &'static [(u32, usize, Option<usize>)] {
513 match message_type {
514 "UTILMD_Strom" => &[(2317, 2, None), (2318, 1, Some(1))],
515 _ => &[],
516 }
517}
518
519pub fn breaks_repetition_limit<E: ConditionEvaluator + ?Sized>(
522 status: &str,
523 evaluator: &E,
524 ctx: &EvaluationContext,
525 count: usize,
526) -> bool {
527 repetition_limits(evaluator.message_type())
528 .iter()
529 .filter(|(id, _, _)| status.contains(&format!("[{id}]")))
530 .any(|&(id, min, max)| {
531 evaluator.evaluate(id, ctx) == ConditionResult::True
532 && (count < min || max.is_some_and(|max| count > max))
533 })
534}
535
536struct CountTarget<'a, E: ConditionEvaluator + ?Sized>(&'a E);
539
540impl<E: ConditionEvaluator + ?Sized> ConditionEvaluator for CountTarget<'_, E> {
541 fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
542 if counted_repetitions(self.0.message_type()).contains(&condition) {
543 return self.0.evaluate(condition, ctx);
544 }
545 PresentTarget(self.0).evaluate(condition, ctx)
546 }
547
548 fn is_external(&self, condition: u32) -> bool {
549 self.0.is_external(condition)
550 }
551
552 fn is_known(&self, condition: u32) -> bool {
553 self.0.is_known(condition)
554 }
555
556 fn message_type(&self) -> &str {
557 self.0.message_type()
558 }
559
560 fn format_version(&self) -> &str {
561 self.0.format_version()
562 }
563}
564
565pub fn refuses_count<E: ConditionEvaluator + ?Sized>(
571 status: &str,
572 evaluator: &E,
573 ctx: &EvaluationContext,
574 ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
575) -> bool {
576 let counted = counted_repetitions(evaluator.message_type());
577 let names_one = status.split('[').skip(1).any(|term| {
578 term.split(']')
579 .next()
580 .and_then(|id| id.parse::<u32>().ok())
581 .is_some_and(|id| counted.contains(&id))
582 });
583 names_one
584 && !refuses_presence(status, evaluator, ctx, ub_definitions)
585 && refuses(status, &CountTarget(evaluator), ctx, ub_definitions)
586}
587
588fn refuses<E: ConditionEvaluator>(
591 status: &str,
592 target: &E,
593 ctx: &EvaluationContext,
594 ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
595) -> bool {
596 let status = status.trim();
597 let package = status.split('[').skip(1).any(|term| {
598 term.trim_start_matches(|c: char| c.is_ascii_digit())
599 .starts_with('P')
600 });
601 if status.contains('\n') || !status.contains('[') || package {
602 return false;
603 }
604 let keyword = &status[..status
605 .find(|c: char| !c.is_alphabetic())
606 .unwrap_or(status.len())];
607 if !matches!(keyword, "Muss" | "X") {
608 return false;
609 }
610 super::ConditionExprEvaluator::new(target).evaluate_status_with_ub(status, ctx, ub_definitions)
611 == ConditionResult::False
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617
618 struct AlwaysTrue(&'static str);
619
620 impl ConditionEvaluator for AlwaysTrue {
621 fn evaluate(&self, _: u32, _: &EvaluationContext) -> ConditionResult {
622 ConditionResult::True
623 }
624 fn is_external(&self, _: u32) -> bool {
625 false
626 }
627 fn message_type(&self) -> &str {
628 self.0
629 }
630 fn format_version(&self) -> &str {
631 "FV2604"
632 }
633 }
634
635 #[test]
636 fn once_per_transaction_binds_where_every_line_has_it() {
637 assert!(binds_once("Muss [483] ∧ [2061]", 2061));
638 assert!(binds_once("Muss [2061]", 2061));
639 assert!(binds_once(
640 "Muss ([77] ∧ [78]) ∧ [347] ∧ [2061]\r\nKann [2061]",
641 2061
642 ));
643 assert!(!binds_once("Soll [165] ∧ (([2061] ∧ [583]) ∨ [584])", 2061));
644 assert!(!binds_once(
645 "Muss ([32] ∧ [2061] ∧ [651]) ⊻ ([200] ∧ [601])",
646 2061
647 ));
648 assert!(!binds_once("Muss [483]", 2061));
649 assert!(!binds_once("", 2061));
650 }
651
652 #[test]
653 fn absent_target_answers_only_the_presence_condition_with_false() {
654 let external = NoOpExternalProvider;
655 let ctx = EvaluationContext::new("55043", &external, &[]);
656 let inner = AlwaysTrue("UTILMD_Strom");
657 let absent = AbsentTarget(&inner);
658 assert_eq!(absent.evaluate(166, &ctx), ConditionResult::False);
659 assert_eq!(absent.evaluate(674, &ctx), ConditionResult::True);
660 assert_eq!(absent.evaluate(2003, &ctx), ConditionResult::False);
662
663 let invoic = AlwaysTrue("INVOIC");
665 assert_eq!(
666 AbsentTarget(&invoic).evaluate(166, &ctx),
667 ConditionResult::True
668 );
669 assert_eq!(
670 AbsentTarget(&invoic).evaluate(22, &ctx),
671 ConditionResult::False
672 );
673 }
674
675 #[test]
676 fn test_condition_result_is_methods() {
677 assert!(ConditionResult::True.is_true());
678 assert!(!ConditionResult::True.is_false());
679 assert!(!ConditionResult::True.is_unknown());
680
681 assert!(!ConditionResult::False.is_true());
682 assert!(ConditionResult::False.is_false());
683
684 assert!(ConditionResult::Unknown.is_unknown());
685 }
686
687 #[test]
688 fn three_valued_and_or_not() {
689 use ConditionResult::{False as F, True as T, Unknown as U};
690 assert_eq!(T.and(T), T);
691 assert_eq!(T.and(U), U);
692 assert_eq!(U.and(F), F);
693 assert_eq!(F.or(F), F);
694 assert_eq!(F.or(U), U);
695 assert_eq!(U.or(T), T);
696 assert_eq!(U.negate(), U);
697 assert_eq!(T.negate(), F);
698 }
699
700 #[test]
701 fn test_condition_result_to_option() {
702 assert_eq!(ConditionResult::True.to_option(), Some(true));
703 assert_eq!(ConditionResult::False.to_option(), Some(false));
704 assert_eq!(ConditionResult::Unknown.to_option(), None);
705 }
706
707 #[test]
708 fn test_condition_result_from_bool() {
709 assert_eq!(ConditionResult::from(true), ConditionResult::True);
710 assert_eq!(ConditionResult::from(false), ConditionResult::False);
711 }
712
713 #[test]
714 fn test_condition_result_display() {
715 assert_eq!(format!("{}", ConditionResult::True), "True");
716 assert_eq!(format!("{}", ConditionResult::False), "False");
717 assert_eq!(format!("{}", ConditionResult::Unknown), "Unknown");
718 }
719
720 #[test]
721 fn test_noop_external_provider() {
722 let provider = NoOpExternalProvider;
723 assert_eq!(
724 provider.evaluate("MessageSplitting"),
725 ConditionResult::Unknown
726 );
727 assert_eq!(provider.evaluate("anything"), ConditionResult::Unknown);
728 }
729}