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 repetition_limits(message_type: &str) -> &'static [(u32, usize, Option<usize>)] {
448 match message_type {
449 "UTILMD_Strom" => &[(2317, 2, None), (2318, 1, Some(1))],
450 _ => &[],
451 }
452}
453
454pub fn breaks_repetition_limit<E: ConditionEvaluator + ?Sized>(
457 status: &str,
458 evaluator: &E,
459 ctx: &EvaluationContext,
460 count: usize,
461) -> bool {
462 repetition_limits(evaluator.message_type())
463 .iter()
464 .filter(|(id, _, _)| status.contains(&format!("[{id}]")))
465 .any(|&(id, min, max)| {
466 evaluator.evaluate(id, ctx) == ConditionResult::True
467 && (count < min || max.is_some_and(|max| count > max))
468 })
469}
470
471struct CountTarget<'a, E: ConditionEvaluator + ?Sized>(&'a E);
474
475impl<E: ConditionEvaluator + ?Sized> ConditionEvaluator for CountTarget<'_, E> {
476 fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
477 if counted_repetitions(self.0.message_type()).contains(&condition) {
478 return self.0.evaluate(condition, ctx);
479 }
480 PresentTarget(self.0).evaluate(condition, ctx)
481 }
482
483 fn is_external(&self, condition: u32) -> bool {
484 self.0.is_external(condition)
485 }
486
487 fn is_known(&self, condition: u32) -> bool {
488 self.0.is_known(condition)
489 }
490
491 fn message_type(&self) -> &str {
492 self.0.message_type()
493 }
494
495 fn format_version(&self) -> &str {
496 self.0.format_version()
497 }
498}
499
500pub fn refuses_count<E: ConditionEvaluator + ?Sized>(
506 status: &str,
507 evaluator: &E,
508 ctx: &EvaluationContext,
509 ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
510) -> bool {
511 let counted = counted_repetitions(evaluator.message_type());
512 let names_one = status.split('[').skip(1).any(|term| {
513 term.split(']')
514 .next()
515 .and_then(|id| id.parse::<u32>().ok())
516 .is_some_and(|id| counted.contains(&id))
517 });
518 names_one
519 && !refuses_presence(status, evaluator, ctx, ub_definitions)
520 && refuses(status, &CountTarget(evaluator), ctx, ub_definitions)
521}
522
523fn refuses<E: ConditionEvaluator>(
526 status: &str,
527 target: &E,
528 ctx: &EvaluationContext,
529 ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
530) -> bool {
531 let status = status.trim();
532 let package = status.split('[').skip(1).any(|term| {
533 term.trim_start_matches(|c: char| c.is_ascii_digit())
534 .starts_with('P')
535 });
536 if status.contains('\n') || !status.contains('[') || package {
537 return false;
538 }
539 let keyword = &status[..status
540 .find(|c: char| !c.is_alphabetic())
541 .unwrap_or(status.len())];
542 if !matches!(keyword, "Muss" | "X") {
543 return false;
544 }
545 super::ConditionExprEvaluator::new(target).evaluate_status_with_ub(status, ctx, ub_definitions)
546 == ConditionResult::False
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552
553 struct AlwaysTrue(&'static str);
554
555 impl ConditionEvaluator for AlwaysTrue {
556 fn evaluate(&self, _: u32, _: &EvaluationContext) -> ConditionResult {
557 ConditionResult::True
558 }
559 fn is_external(&self, _: u32) -> bool {
560 false
561 }
562 fn message_type(&self) -> &str {
563 self.0
564 }
565 fn format_version(&self) -> &str {
566 "FV2604"
567 }
568 }
569
570 #[test]
571 fn once_per_transaction_binds_where_every_line_has_it() {
572 assert!(binds_once("Muss [483] ∧ [2061]", 2061));
573 assert!(binds_once("Muss [2061]", 2061));
574 assert!(binds_once(
575 "Muss ([77] ∧ [78]) ∧ [347] ∧ [2061]\r\nKann [2061]",
576 2061
577 ));
578 assert!(!binds_once("Soll [165] ∧ (([2061] ∧ [583]) ∨ [584])", 2061));
579 assert!(!binds_once(
580 "Muss ([32] ∧ [2061] ∧ [651]) ⊻ ([200] ∧ [601])",
581 2061
582 ));
583 assert!(!binds_once("Muss [483]", 2061));
584 assert!(!binds_once("", 2061));
585 }
586
587 #[test]
588 fn absent_target_answers_only_the_presence_condition_with_false() {
589 let external = NoOpExternalProvider;
590 let ctx = EvaluationContext::new("55043", &external, &[]);
591 let inner = AlwaysTrue("UTILMD_Strom");
592 let absent = AbsentTarget(&inner);
593 assert_eq!(absent.evaluate(166, &ctx), ConditionResult::False);
594 assert_eq!(absent.evaluate(674, &ctx), ConditionResult::True);
595 assert_eq!(absent.evaluate(2003, &ctx), ConditionResult::False);
597
598 let invoic = AlwaysTrue("INVOIC");
600 assert_eq!(
601 AbsentTarget(&invoic).evaluate(166, &ctx),
602 ConditionResult::True
603 );
604 assert_eq!(
605 AbsentTarget(&invoic).evaluate(22, &ctx),
606 ConditionResult::False
607 );
608 }
609
610 #[test]
611 fn test_condition_result_is_methods() {
612 assert!(ConditionResult::True.is_true());
613 assert!(!ConditionResult::True.is_false());
614 assert!(!ConditionResult::True.is_unknown());
615
616 assert!(!ConditionResult::False.is_true());
617 assert!(ConditionResult::False.is_false());
618
619 assert!(ConditionResult::Unknown.is_unknown());
620 }
621
622 #[test]
623 fn three_valued_and_or_not() {
624 use ConditionResult::{False as F, True as T, Unknown as U};
625 assert_eq!(T.and(T), T);
626 assert_eq!(T.and(U), U);
627 assert_eq!(U.and(F), F);
628 assert_eq!(F.or(F), F);
629 assert_eq!(F.or(U), U);
630 assert_eq!(U.or(T), T);
631 assert_eq!(U.negate(), U);
632 assert_eq!(T.negate(), F);
633 }
634
635 #[test]
636 fn test_condition_result_to_option() {
637 assert_eq!(ConditionResult::True.to_option(), Some(true));
638 assert_eq!(ConditionResult::False.to_option(), Some(false));
639 assert_eq!(ConditionResult::Unknown.to_option(), None);
640 }
641
642 #[test]
643 fn test_condition_result_from_bool() {
644 assert_eq!(ConditionResult::from(true), ConditionResult::True);
645 assert_eq!(ConditionResult::from(false), ConditionResult::False);
646 }
647
648 #[test]
649 fn test_condition_result_display() {
650 assert_eq!(format!("{}", ConditionResult::True), "True");
651 assert_eq!(format!("{}", ConditionResult::False), "False");
652 assert_eq!(format!("{}", ConditionResult::Unknown), "Unknown");
653 }
654
655 #[test]
656 fn test_noop_external_provider() {
657 let provider = NoOpExternalProvider;
658 assert_eq!(
659 provider.evaluate("MessageSplitting"),
660 ConditionResult::Unknown
661 );
662 assert_eq!(provider.evaluate("anything"), ConditionResult::Unknown);
663 }
664}