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
166pub struct NoOpExternalProvider;
171
172impl ExternalConditionProvider for NoOpExternalProvider {
173 fn evaluate(&self, _condition_name: &str) -> ConditionResult {
174 ConditionResult::Unknown
175 }
176}
177
178pub fn presence_condition(message_type: &str) -> Option<u32> {
191 match message_type {
192 "UTILMD_Strom" | "UTILMD_Gas" => Some(166),
193 "INVOIC" => Some(22),
194 "ORDERS" => Some(12),
195 _ => None,
196 }
197}
198
199pub fn presence_conditions(message_type: &str) -> &'static [u32] {
210 match message_type {
211 "UTILMD_Strom" => &[166, 2003],
212 "UTILMD_Gas" => &[166],
213 "INVOIC" => &[22],
214 "ORDERS" => &[12],
215 _ => &[],
216 }
217}
218
219pub fn once_per_transaction(message_type: &str) -> Option<(u32, &'static str)> {
226 match message_type {
227 "UTILMD_Strom" | "UTILMD_Gas" => Some((2061, "SG4")),
228 _ => None,
229 }
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub struct Vorgang {
237 pub group: &'static str,
239 pub tag: &'static str,
241 pub element: usize,
243 pub component: usize,
244}
245
246pub fn transactions(message_type: &str) -> &'static [Vorgang] {
258 const IDE_SG4: Vorgang = Vorgang {
259 group: "SG4",
260 tag: "IDE",
261 element: 1,
262 component: 0,
263 };
264 const IDE_SG5: Vorgang = Vorgang {
265 group: "SG5",
266 tag: "IDE",
267 element: 1,
268 component: 0,
269 };
270 const DOC: Vorgang = Vorgang {
271 group: "SG3",
272 tag: "DOC",
273 element: 1,
274 component: 0,
275 };
276 const EQD: Vorgang = Vorgang {
277 group: "SG4",
278 tag: "EQD",
279 element: 1,
280 component: 0,
281 };
282 const CNI: Vorgang = Vorgang {
283 group: "SG14",
284 tag: "CNI",
285 element: 0,
286 component: 0,
287 };
288 match message_type.split('_').next().unwrap_or_default() {
289 "UTILMD" => &[IDE_SG4],
290 "UTILTS" => &[IDE_SG5],
291 "INSRPT" => &[DOC],
292 "IFTSTA" => &[EQD, CNI],
293 _ => &[],
294 }
295}
296
297pub fn is_transaction_group(message_type: &str, group_id: &str) -> bool {
302 transactions(message_type)
303 .iter()
304 .any(|v| v.group == group_id)
305}
306
307pub fn binds_once(status: &str, once: u32) -> bool {
312 fn conjunct(expr: &crate::expr::ConditionExpr, once: u32) -> bool {
313 match expr {
314 crate::expr::ConditionExpr::Ref(id) => *id == once,
315 crate::expr::ConditionExpr::And(exprs) => exprs.iter().any(|e| conjunct(e, once)),
316 _ => false,
317 }
318 }
319 let lines: Vec<&str> = status
320 .lines()
321 .map(str::trim)
322 .filter(|l| !l.is_empty())
323 .collect();
324 !lines.is_empty()
325 && lines.iter().all(|line| {
326 matches!(
327 crate::expr::ConditionParser::parse(line),
328 Ok(Some(expr)) if conjunct(&expr, once)
329 )
330 })
331}
332
333pub struct AbsentTarget<'a, E: ConditionEvaluator + ?Sized>(pub &'a E);
340
341impl<E: ConditionEvaluator + ?Sized> ConditionEvaluator for AbsentTarget<'_, E> {
342 fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
343 if presence_conditions(self.0.message_type()).contains(&condition) {
344 return ConditionResult::False;
345 }
346 self.0.evaluate(condition, ctx)
347 }
348
349 fn is_external(&self, condition: u32) -> bool {
350 self.0.is_external(condition)
351 }
352
353 fn is_known(&self, condition: u32) -> bool {
354 self.0.is_known(condition)
355 }
356
357 fn message_type(&self) -> &str {
358 self.0.message_type()
359 }
360
361 fn format_version(&self) -> &str {
362 self.0.format_version()
363 }
364}
365
366pub struct PresentTarget<'a, E: ConditionEvaluator + ?Sized>(pub &'a E);
376
377impl<E: ConditionEvaluator + ?Sized> ConditionEvaluator for PresentTarget<'_, E> {
378 fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
379 if presence_conditions(self.0.message_type()).contains(&condition) {
380 return ConditionResult::True;
381 }
382 if !(1..500).contains(&condition) {
383 return ConditionResult::Unknown;
384 }
385 self.0.evaluate(condition, ctx)
386 }
387
388 fn is_external(&self, condition: u32) -> bool {
389 self.0.is_external(condition)
390 }
391
392 fn is_known(&self, condition: u32) -> bool {
393 self.0.is_known(condition)
394 }
395
396 fn message_type(&self) -> &str {
397 self.0.message_type()
398 }
399
400 fn format_version(&self) -> &str {
401 self.0.format_version()
402 }
403}
404
405pub fn refuses_presence<E: ConditionEvaluator + ?Sized>(
411 status: &str,
412 evaluator: &E,
413 ctx: &EvaluationContext,
414 ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
415) -> bool {
416 refuses(status, &PresentTarget(evaluator), ctx, ub_definitions)
417}
418
419pub fn counted_repetitions(message_type: &str) -> &'static [u32] {
426 match message_type {
427 "ORDERS" => &[2005, 2006],
428 _ => &[],
429 }
430}
431
432pub fn repetition_limits(message_type: &str) -> &'static [(u32, usize, Option<usize>)] {
440 match message_type {
441 "UTILMD_Strom" => &[(2317, 2, None), (2318, 1, Some(1))],
442 _ => &[],
443 }
444}
445
446pub fn breaks_repetition_limit<E: ConditionEvaluator + ?Sized>(
449 status: &str,
450 evaluator: &E,
451 ctx: &EvaluationContext,
452 count: usize,
453) -> bool {
454 repetition_limits(evaluator.message_type())
455 .iter()
456 .filter(|(id, _, _)| status.contains(&format!("[{id}]")))
457 .any(|&(id, min, max)| {
458 evaluator.evaluate(id, ctx) == ConditionResult::True
459 && (count < min || max.is_some_and(|max| count > max))
460 })
461}
462
463struct CountTarget<'a, E: ConditionEvaluator + ?Sized>(&'a E);
466
467impl<E: ConditionEvaluator + ?Sized> ConditionEvaluator for CountTarget<'_, E> {
468 fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
469 if counted_repetitions(self.0.message_type()).contains(&condition) {
470 return self.0.evaluate(condition, ctx);
471 }
472 PresentTarget(self.0).evaluate(condition, ctx)
473 }
474
475 fn is_external(&self, condition: u32) -> bool {
476 self.0.is_external(condition)
477 }
478
479 fn is_known(&self, condition: u32) -> bool {
480 self.0.is_known(condition)
481 }
482
483 fn message_type(&self) -> &str {
484 self.0.message_type()
485 }
486
487 fn format_version(&self) -> &str {
488 self.0.format_version()
489 }
490}
491
492pub fn refuses_count<E: ConditionEvaluator + ?Sized>(
498 status: &str,
499 evaluator: &E,
500 ctx: &EvaluationContext,
501 ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
502) -> bool {
503 let counted = counted_repetitions(evaluator.message_type());
504 let names_one = status.split('[').skip(1).any(|term| {
505 term.split(']')
506 .next()
507 .and_then(|id| id.parse::<u32>().ok())
508 .is_some_and(|id| counted.contains(&id))
509 });
510 names_one
511 && !refuses_presence(status, evaluator, ctx, ub_definitions)
512 && refuses(status, &CountTarget(evaluator), ctx, ub_definitions)
513}
514
515fn refuses<E: ConditionEvaluator>(
518 status: &str,
519 target: &E,
520 ctx: &EvaluationContext,
521 ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
522) -> bool {
523 let status = status.trim();
524 let package = status.split('[').skip(1).any(|term| {
525 term.trim_start_matches(|c: char| c.is_ascii_digit())
526 .starts_with('P')
527 });
528 if status.contains('\n') || !status.contains('[') || package {
529 return false;
530 }
531 let keyword = &status[..status
532 .find(|c: char| !c.is_alphabetic())
533 .unwrap_or(status.len())];
534 if !matches!(keyword, "Muss" | "X") {
535 return false;
536 }
537 super::ConditionExprEvaluator::new(target).evaluate_status_with_ub(status, ctx, ub_definitions)
538 == ConditionResult::False
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544
545 struct AlwaysTrue(&'static str);
546
547 impl ConditionEvaluator for AlwaysTrue {
548 fn evaluate(&self, _: u32, _: &EvaluationContext) -> ConditionResult {
549 ConditionResult::True
550 }
551 fn is_external(&self, _: u32) -> bool {
552 false
553 }
554 fn message_type(&self) -> &str {
555 self.0
556 }
557 fn format_version(&self) -> &str {
558 "FV2604"
559 }
560 }
561
562 #[test]
563 fn once_per_transaction_binds_where_every_line_has_it() {
564 assert!(binds_once("Muss [483] ∧ [2061]", 2061));
565 assert!(binds_once("Muss [2061]", 2061));
566 assert!(binds_once(
567 "Muss ([77] ∧ [78]) ∧ [347] ∧ [2061]\r\nKann [2061]",
568 2061
569 ));
570 assert!(!binds_once("Soll [165] ∧ (([2061] ∧ [583]) ∨ [584])", 2061));
571 assert!(!binds_once(
572 "Muss ([32] ∧ [2061] ∧ [651]) ⊻ ([200] ∧ [601])",
573 2061
574 ));
575 assert!(!binds_once("Muss [483]", 2061));
576 assert!(!binds_once("", 2061));
577 }
578
579 #[test]
580 fn absent_target_answers_only_the_presence_condition_with_false() {
581 let external = NoOpExternalProvider;
582 let ctx = EvaluationContext::new("55043", &external, &[]);
583 let inner = AlwaysTrue("UTILMD_Strom");
584 let absent = AbsentTarget(&inner);
585 assert_eq!(absent.evaluate(166, &ctx), ConditionResult::False);
586 assert_eq!(absent.evaluate(674, &ctx), ConditionResult::True);
587 assert_eq!(absent.evaluate(2003, &ctx), ConditionResult::False);
589
590 let invoic = AlwaysTrue("INVOIC");
592 assert_eq!(
593 AbsentTarget(&invoic).evaluate(166, &ctx),
594 ConditionResult::True
595 );
596 assert_eq!(
597 AbsentTarget(&invoic).evaluate(22, &ctx),
598 ConditionResult::False
599 );
600 }
601
602 #[test]
603 fn test_condition_result_is_methods() {
604 assert!(ConditionResult::True.is_true());
605 assert!(!ConditionResult::True.is_false());
606 assert!(!ConditionResult::True.is_unknown());
607
608 assert!(!ConditionResult::False.is_true());
609 assert!(ConditionResult::False.is_false());
610
611 assert!(ConditionResult::Unknown.is_unknown());
612 }
613
614 #[test]
615 fn three_valued_and_or_not() {
616 use ConditionResult::{False as F, True as T, Unknown as U};
617 assert_eq!(T.and(T), T);
618 assert_eq!(T.and(U), U);
619 assert_eq!(U.and(F), F);
620 assert_eq!(F.or(F), F);
621 assert_eq!(F.or(U), U);
622 assert_eq!(U.or(T), T);
623 assert_eq!(U.negate(), U);
624 assert_eq!(T.negate(), F);
625 }
626
627 #[test]
628 fn test_condition_result_to_option() {
629 assert_eq!(ConditionResult::True.to_option(), Some(true));
630 assert_eq!(ConditionResult::False.to_option(), Some(false));
631 assert_eq!(ConditionResult::Unknown.to_option(), None);
632 }
633
634 #[test]
635 fn test_condition_result_from_bool() {
636 assert_eq!(ConditionResult::from(true), ConditionResult::True);
637 assert_eq!(ConditionResult::from(false), ConditionResult::False);
638 }
639
640 #[test]
641 fn test_condition_result_display() {
642 assert_eq!(format!("{}", ConditionResult::True), "True");
643 assert_eq!(format!("{}", ConditionResult::False), "False");
644 assert_eq!(format!("{}", ConditionResult::Unknown), "Unknown");
645 }
646
647 #[test]
648 fn test_noop_external_provider() {
649 let provider = NoOpExternalProvider;
650 assert_eq!(
651 provider.evaluate("MessageSplitting"),
652 ConditionResult::Unknown
653 );
654 assert_eq!(provider.evaluate("anything"), ConditionResult::Unknown);
655 }
656}