automapper_validation/generated/fv2504/mscons_conditions_fv2504.rs
1// <auto-generated>
2// Generated by automapper-generator generate-conditions
3// AHB: xml-migs-and-ahbs/FV2504/MSCONS_AHB_3_1f_Fehlerkorrektur_20250623.xml
4// Generated: 2026-03-12T10:12:37Z
5// </auto-generated>
6
7// LLM-produced bodies aren't idiomatic Rust — silence stylistic lints at
8// file scope so `cargo clippy -D warnings` stays green. Correctness,
9// suspicious, and perf categories stay on so real bugs still surface.
10#![allow(clippy::style, clippy::complexity)]
11
12#[allow(unused_imports)]
13use crate::eval::format_validators::*;
14use crate::eval::{ConditionEvaluator, ConditionResult, EvaluationContext};
15
16/// Generated condition evaluator for MSCONS FV2504.
17pub struct MsconsConditionEvaluatorFV2504 {
18 // External condition IDs that require runtime context.
19 external_conditions: std::collections::HashSet<u32>,
20}
21
22impl Default for MsconsConditionEvaluatorFV2504 {
23 fn default() -> Self {
24 let mut external_conditions = std::collections::HashSet::new();
25 external_conditions.insert(1);
26 external_conditions.insert(22);
27 external_conditions.insert(32);
28 external_conditions.insert(33);
29 external_conditions.insert(35);
30 external_conditions.insert(36);
31 external_conditions.insert(42);
32 external_conditions.insert(67);
33 external_conditions.insert(77);
34 external_conditions.insert(80);
35 external_conditions.insert(81);
36 external_conditions.insert(97);
37 external_conditions.insert(117);
38 external_conditions.insert(118);
39 external_conditions.insert(126);
40 external_conditions.insert(127);
41 external_conditions.insert(138);
42 external_conditions.insert(141);
43 external_conditions.insert(492);
44 external_conditions.insert(493);
45 Self {
46 external_conditions,
47 }
48 }
49}
50
51impl ConditionEvaluator for MsconsConditionEvaluatorFV2504 {
52 fn message_type(&self) -> &str {
53 "MSCONS"
54 }
55
56 fn format_version(&self) -> &str {
57 "FV2504"
58 }
59
60 fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
61 match condition {
62 1 => self.evaluate_1(ctx),
63 2 => self.evaluate_2(ctx),
64 11 => self.evaluate_11(ctx),
65 12 => self.evaluate_12(ctx),
66 17 => self.evaluate_17(ctx),
67 18 => self.evaluate_18(ctx),
68 22 => self.evaluate_22(ctx),
69 27 => self.evaluate_27(ctx),
70 28 => self.evaluate_28(ctx),
71 32 => self.evaluate_32(ctx),
72 33 => self.evaluate_33(ctx),
73 35 => self.evaluate_35(ctx),
74 36 => self.evaluate_36(ctx),
75 38 => self.evaluate_38(ctx),
76 42 => self.evaluate_42(ctx),
77 45 => self.evaluate_45(ctx),
78 46 => self.evaluate_46(ctx),
79 48 => self.evaluate_48(ctx),
80 49 => self.evaluate_49(ctx),
81 50 => self.evaluate_50(ctx),
82 51 => self.evaluate_51(ctx),
83 62 => self.evaluate_62(ctx),
84 67 => self.evaluate_67(ctx),
85 68 => self.evaluate_68(ctx),
86 69 => self.evaluate_69(ctx),
87 70 => self.evaluate_70(ctx),
88 71 => self.evaluate_71(ctx),
89 72 => self.evaluate_72(ctx),
90 73 => self.evaluate_73(ctx),
91 77 => self.evaluate_77(ctx),
92 78 => self.evaluate_78(ctx),
93 79 => self.evaluate_79(ctx),
94 80 => self.evaluate_80(ctx),
95 81 => self.evaluate_81(ctx),
96 83 => self.evaluate_83(ctx),
97 84 => self.evaluate_84(ctx),
98 85 => self.evaluate_85(ctx),
99 86 => self.evaluate_86(ctx),
100 87 => self.evaluate_87(ctx),
101 88 => self.evaluate_88(ctx),
102 90 => self.evaluate_90(ctx),
103 91 => self.evaluate_91(ctx),
104 92 => self.evaluate_92(ctx),
105 93 => self.evaluate_93(ctx),
106 94 => self.evaluate_94(ctx),
107 95 => self.evaluate_95(ctx),
108 96 => self.evaluate_96(ctx),
109 97 => self.evaluate_97(ctx),
110 98 => self.evaluate_98(ctx),
111 99 => self.evaluate_99(ctx),
112 100 => self.evaluate_100(ctx),
113 101 => self.evaluate_101(ctx),
114 108 => self.evaluate_108(ctx),
115 111 => self.evaluate_111(ctx),
116 113 => self.evaluate_113(ctx),
117 117 => self.evaluate_117(ctx),
118 118 => self.evaluate_118(ctx),
119 119 => self.evaluate_119(ctx),
120 121 => self.evaluate_121(ctx),
121 125 => self.evaluate_125(ctx),
122 126 => self.evaluate_126(ctx),
123 127 => self.evaluate_127(ctx),
124 128 => self.evaluate_128(ctx),
125 129 => self.evaluate_129(ctx),
126 130 => self.evaluate_130(ctx),
127 131 => self.evaluate_131(ctx),
128 132 => self.evaluate_132(ctx),
129 133 => self.evaluate_133(ctx),
130 134 => self.evaluate_134(ctx),
131 135 => self.evaluate_135(ctx),
132 138 => self.evaluate_138(ctx),
133 141 => self.evaluate_141(ctx),
134 142 => self.evaluate_142(ctx),
135 143 => self.evaluate_143(ctx),
136 144 => self.evaluate_144(ctx),
137 145 => self.evaluate_145(ctx),
138 146 => self.evaluate_146(ctx),
139 147 => self.evaluate_147(ctx),
140 148 => self.evaluate_148(ctx),
141 149 => self.evaluate_149(ctx),
142 150 => self.evaluate_150(ctx),
143 490 => self.evaluate_490(ctx),
144 491 => self.evaluate_491(ctx),
145 492 => self.evaluate_492(ctx),
146 493 => self.evaluate_493(ctx),
147 494 => self.evaluate_494(ctx),
148 495 => self.evaluate_495(ctx),
149 501 => self.evaluate_501(ctx),
150 502 => self.evaluate_502(ctx),
151 506 => self.evaluate_506(ctx),
152 509 => self.evaluate_509(ctx),
153 510 => self.evaluate_510(ctx),
154 511 => self.evaluate_511(ctx),
155 512 => self.evaluate_512(ctx),
156 513 => self.evaluate_513(ctx),
157 514 => self.evaluate_514(ctx),
158 515 => self.evaluate_515(ctx),
159 516 => self.evaluate_516(ctx),
160 518 => self.evaluate_518(ctx),
161 519 => self.evaluate_519(ctx),
162 520 => self.evaluate_520(ctx),
163 522 => self.evaluate_522(ctx),
164 523 => self.evaluate_523(ctx),
165 524 => self.evaluate_524(ctx),
166 525 => self.evaluate_525(ctx),
167 526 => self.evaluate_526(ctx),
168 528 => self.evaluate_528(ctx),
169 529 => self.evaluate_529(ctx),
170 530 => self.evaluate_530(ctx),
171 531 => self.evaluate_531(ctx),
172 532 => self.evaluate_532(ctx),
173 535 => self.evaluate_535(ctx),
174 538 => self.evaluate_538(ctx),
175 541 => self.evaluate_541(ctx),
176 543 => self.evaluate_543(ctx),
177 544 => self.evaluate_544(ctx),
178 545 => self.evaluate_545(ctx),
179 546 => self.evaluate_546(ctx),
180 547 => self.evaluate_547(ctx),
181 551 => self.evaluate_551(ctx),
182 553 => self.evaluate_553(ctx),
183 554 => self.evaluate_554(ctx),
184 556 => self.evaluate_556(ctx),
185 557 => self.evaluate_557(ctx),
186 558 => self.evaluate_558(ctx),
187 559 => self.evaluate_559(ctx),
188 560 => self.evaluate_560(ctx),
189 566 => self.evaluate_566(ctx),
190 567 => self.evaluate_567(ctx),
191 568 => self.evaluate_568(ctx),
192 569 => self.evaluate_569(ctx),
193 570 => self.evaluate_570(ctx),
194 571 => self.evaluate_571(ctx),
195 572 => self.evaluate_572(ctx),
196 573 => self.evaluate_573(ctx),
197 574 => self.evaluate_574(ctx),
198 575 => self.evaluate_575(ctx),
199 576 => self.evaluate_576(ctx),
200 577 => self.evaluate_577(ctx),
201 902 => self.evaluate_902(ctx),
202 904 => self.evaluate_904(ctx),
203 905 => self.evaluate_905(ctx),
204 906 => self.evaluate_906(ctx),
205 907 => self.evaluate_907(ctx),
206 908 => self.evaluate_908(ctx),
207 909 => self.evaluate_909(ctx),
208 910 => self.evaluate_910(ctx),
209 917 => self.evaluate_917(ctx),
210 918 => self.evaluate_918(ctx),
211 922 => self.evaluate_922(ctx),
212 925 => self.evaluate_925(ctx),
213 931 => self.evaluate_931(ctx),
214 932 => self.evaluate_932(ctx),
215 933 => self.evaluate_933(ctx),
216 934 => self.evaluate_934(ctx),
217 935 => self.evaluate_935(ctx),
218 937 => self.evaluate_937(ctx),
219 939 => self.evaluate_939(ctx),
220 940 => self.evaluate_940(ctx),
221 950 => self.evaluate_950(ctx),
222 951 => self.evaluate_951(ctx),
223 960 => self.evaluate_960(ctx),
224 2001 => self.evaluate_2001(ctx),
225 2002 => self.evaluate_2002(ctx),
226 2003 => self.evaluate_2003(ctx),
227 _ => ConditionResult::Unknown,
228 }
229 }
230
231 fn is_external(&self, condition: u32) -> bool {
232 self.external_conditions.contains(&condition)
233 }
234 fn is_known(&self, condition: u32) -> bool {
235 matches!(
236 condition,
237 1 | 2
238 | 11
239 | 12
240 | 17
241 | 18
242 | 22
243 | 27
244 | 28
245 | 32
246 | 33
247 | 35
248 | 36
249 | 38
250 | 42
251 | 45
252 | 46
253 | 48
254 | 49
255 | 50
256 | 51
257 | 62
258 | 67
259 | 68
260 | 69
261 | 70
262 | 71
263 | 72
264 | 73
265 | 77
266 | 78
267 | 79
268 | 80
269 | 81
270 | 83
271 | 84
272 | 85
273 | 86
274 | 87
275 | 88
276 | 90
277 | 91
278 | 92
279 | 93
280 | 94
281 | 95
282 | 96
283 | 97
284 | 98
285 | 99
286 | 100
287 | 101
288 | 108
289 | 111
290 | 113
291 | 117
292 | 118
293 | 119
294 | 121
295 | 125
296 | 126
297 | 127
298 | 128
299 | 129
300 | 130
301 | 131
302 | 132
303 | 133
304 | 134
305 | 135
306 | 138
307 | 141
308 | 142
309 | 143
310 | 144
311 | 145
312 | 146
313 | 147
314 | 148
315 | 149
316 | 150
317 | 490
318 | 491
319 | 492
320 | 493
321 | 494
322 | 495
323 | 501
324 | 502
325 | 506
326 | 509
327 | 510
328 | 511
329 | 512
330 | 513
331 | 514
332 | 515
333 | 516
334 | 518
335 | 519
336 | 520
337 | 522
338 | 523
339 | 524
340 | 525
341 | 526
342 | 528
343 | 529
344 | 530
345 | 531
346 | 532
347 | 535
348 | 538
349 | 541
350 | 543
351 | 544
352 | 545
353 | 546
354 | 547
355 | 551
356 | 553
357 | 554
358 | 556
359 | 557
360 | 558
361 | 559
362 | 560
363 | 566
364 | 567
365 | 568
366 | 569
367 | 570
368 | 571
369 | 572
370 | 573
371 | 574
372 | 575
373 | 576
374 | 577
375 | 902
376 | 904
377 | 905
378 | 906
379 | 907
380 | 908
381 | 909
382 | 910
383 | 917
384 | 918
385 | 922
386 | 925
387 | 931
388 | 932
389 | 933
390 | 934
391 | 935
392 | 937
393 | 939
394 | 940
395 | 950
396 | 951
397 | 960
398 | 2001
399 | 2002
400 | 2003
401 )
402 }
403}
404
405impl MsconsConditionEvaluatorFV2504 {
406 /// [81] Wenn MP-ID in SG2 NAD+MS in der Rolle ÜNB
407 /// EXTERNAL: Requires context from outside the message.
408 fn evaluate_81(&self, ctx: &EvaluationContext) -> ConditionResult {
409 ctx.external.evaluate("sender_is_uenb")
410 }
411
412 /// [87] Wenn der Wert in DTM+163 DE2380 derselben SG6 LOC+172 mit demselben Wert in SG9 PIA+5 DE7140 der früheste angegebene Zeitpunkt ist
413 // REVIEW: Checks that DTM+163 in SG6 (with LOC+172) equals the minimum DTM+163 across all SG10 instances beneath that SG6's SG9 children. Medium confidence because the SG9→SG10 child navigation uses a local child instance index (si) passed to the grandchild API, which may behave differently depending on navigator implementation details for two-level child traversal. (medium confidence)
414 fn evaluate_87(&self, ctx: &EvaluationContext) -> ConditionResult {
415 // HAND-EDITED: "DTM+163 … derselben SG6 LOC+172 mit demselben Wert in SG9 PIA+5 … der früheste Zeitpunkt" — compared with the other instances when the
416 // scope reaches an SG10; the message as before otherwise.
417 if let Some(scope) = ctx.enclosing_scope("SG10") {
418 let dtm = |path: &[(&str, usize)]| -> Vec<String> {
419 ctx.find_segments_at(path, "DTM")
420 .iter()
421 .filter(|s| s.get_element(0) == "163")
422 .map(|s| s.get_component(0, 1).to_string())
423 .filter(|v| !v.is_empty())
424 .collect()
425 };
426 let pia = |path: &[(&str, usize)]| -> Vec<String> {
427 ctx.find_segments_at(path, "PIA")
428 .iter()
429 .filter(|s| s.get_element(0) == "5")
430 .map(|s| s.get_component(1, 0).to_string())
431 .filter(|v| !v.is_empty())
432 .collect()
433 };
434 let (Some(own), Some(sg9)) =
435 (dtm(scope).into_iter().next(), ctx.enclosing_scope("SG9"))
436 else {
437 return ConditionResult::Unknown;
438 };
439 let products = pia(sg9);
440 // Every SG10 (the DTMs sit there) of the SG9s of this SG6 with the
441 // same PIA+5 product.
442 let all: Vec<String> = ctx
443 .scopes_under("SG6", "SG9")
444 .unwrap_or_default()
445 .iter()
446 .filter(|p| pia(p).iter().any(|v| products.contains(v)))
447 .flat_map(|p| dtm(p))
448 .collect();
449 return ConditionResult::from(all.iter().min() == Some(&own));
450 }
451 let nav = match ctx.navigator() {
452 Some(n) => n,
453 None => return ConditionResult::Unknown,
454 };
455 let sg6_count = nav.group_instance_count(&["SG5", "SG6"]);
456 for gi in 0..sg6_count {
457 let locs = nav.find_segments_in_group("LOC", &["SG5", "SG6"], gi);
458 let has_loc_172 = locs.iter().any(|s| {
459 s.elements
460 .first()
461 .and_then(|e| e.first())
462 .is_some_and(|v| v == "172")
463 });
464 if !has_loc_172 {
465 continue;
466 }
467 let sg6_dtms = nav.find_segments_in_group("DTM", &["SG5", "SG6"], gi);
468 let sg6_val = match sg6_dtms
469 .iter()
470 .find(|s| {
471 s.elements
472 .first()
473 .and_then(|e| e.first())
474 .is_some_and(|v| v == "163")
475 })
476 .and_then(|s| s.elements.first().and_then(|e| e.get(1)))
477 .filter(|v| !v.is_empty())
478 .cloned()
479 {
480 Some(v) => v,
481 None => continue,
482 };
483 let sg9_count = nav.child_group_instance_count(&["SG5", "SG6"], gi, "SG9");
484 let mut sg10_values: Vec<String> = Vec::new();
485 for si in 0..sg9_count {
486 let sg10_count = nav.child_group_instance_count(&["SG5", "SG6", "SG9"], si, "SG10");
487 for ti in 0..sg10_count {
488 let dtms = nav.find_segments_in_child_group(
489 "DTM",
490 &["SG5", "SG6", "SG9"],
491 si,
492 "SG10",
493 ti,
494 );
495 for dtm in &dtms {
496 if dtm
497 .elements
498 .first()
499 .and_then(|e| e.first())
500 .is_some_and(|v| v == "163")
501 {
502 if let Some(val) = dtm.elements.first().and_then(|e| e.get(1)) {
503 if !val.is_empty() {
504 sg10_values.push(val.clone());
505 }
506 }
507 }
508 }
509 }
510 }
511 if let Some(min_val) = sg10_values.iter().min() {
512 if &sg6_val != min_val {
513 return ConditionResult::False;
514 }
515 }
516 }
517 ConditionResult::True
518 }
519
520 /// [88] Wenn der Wert in DTM+164 DE2380 derselben SG6 LOC+172 mit demselben Wert in SG9 PIA+5 DE7140 der späteste angegebene Zeitpunkt ist
521 // REVIEW: Mirror of condition 87 but for DTM+164 (Ende Messperiode) — checks that the SG6 transmission period end equals the maximum DTM+164 across all SG10 measurement instances under that SG6. Same navigation caveat applies for two-level SG9→SG10 child traversal. (medium confidence)
522 fn evaluate_88(&self, ctx: &EvaluationContext) -> ConditionResult {
523 // HAND-EDITED: "DTM+164 … derselben SG6 LOC+172 mit demselben Wert in SG9 PIA+5 … der späteste Zeitpunkt" — compared with the other instances when the
524 // scope reaches an SG10; the message as before otherwise.
525 if let Some(scope) = ctx.enclosing_scope("SG10") {
526 let dtm = |path: &[(&str, usize)]| -> Vec<String> {
527 ctx.find_segments_at(path, "DTM")
528 .iter()
529 .filter(|s| s.get_element(0) == "164")
530 .map(|s| s.get_component(0, 1).to_string())
531 .filter(|v| !v.is_empty())
532 .collect()
533 };
534 let pia = |path: &[(&str, usize)]| -> Vec<String> {
535 ctx.find_segments_at(path, "PIA")
536 .iter()
537 .filter(|s| s.get_element(0) == "5")
538 .map(|s| s.get_component(1, 0).to_string())
539 .filter(|v| !v.is_empty())
540 .collect()
541 };
542 let (Some(own), Some(sg9)) =
543 (dtm(scope).into_iter().next(), ctx.enclosing_scope("SG9"))
544 else {
545 return ConditionResult::Unknown;
546 };
547 let products = pia(sg9);
548 // Every SG10 (the DTMs sit there) of the SG9s of this SG6 with the
549 // same PIA+5 product.
550 let all: Vec<String> = ctx
551 .scopes_under("SG6", "SG9")
552 .unwrap_or_default()
553 .iter()
554 .filter(|p| pia(p).iter().any(|v| products.contains(v)))
555 .flat_map(|p| dtm(p))
556 .collect();
557 return ConditionResult::from(all.iter().max() == Some(&own));
558 }
559 let nav = match ctx.navigator() {
560 Some(n) => n,
561 None => return ConditionResult::Unknown,
562 };
563 let sg6_count = nav.group_instance_count(&["SG5", "SG6"]);
564 for gi in 0..sg6_count {
565 let locs = nav.find_segments_in_group("LOC", &["SG5", "SG6"], gi);
566 let has_loc_172 = locs.iter().any(|s| {
567 s.elements
568 .first()
569 .and_then(|e| e.first())
570 .is_some_and(|v| v == "172")
571 });
572 if !has_loc_172 {
573 continue;
574 }
575 let sg6_dtms = nav.find_segments_in_group("DTM", &["SG5", "SG6"], gi);
576 let sg6_val = match sg6_dtms
577 .iter()
578 .find(|s| {
579 s.elements
580 .first()
581 .and_then(|e| e.first())
582 .is_some_and(|v| v == "164")
583 })
584 .and_then(|s| s.elements.first().and_then(|e| e.get(1)))
585 .filter(|v| !v.is_empty())
586 .cloned()
587 {
588 Some(v) => v,
589 None => continue,
590 };
591 let sg9_count = nav.child_group_instance_count(&["SG5", "SG6"], gi, "SG9");
592 let mut sg10_values: Vec<String> = Vec::new();
593 for si in 0..sg9_count {
594 let sg10_count = nav.child_group_instance_count(&["SG5", "SG6", "SG9"], si, "SG10");
595 for ti in 0..sg10_count {
596 let dtms = nav.find_segments_in_child_group(
597 "DTM",
598 &["SG5", "SG6", "SG9"],
599 si,
600 "SG10",
601 ti,
602 );
603 for dtm in &dtms {
604 if dtm
605 .elements
606 .first()
607 .and_then(|e| e.first())
608 .is_some_and(|v| v == "164")
609 {
610 if let Some(val) = dtm.elements.first().and_then(|e| e.get(1)) {
611 if !val.is_empty() {
612 sg10_values.push(val.clone());
613 }
614 }
615 }
616 }
617 }
618 }
619 if let Some(max_val) = sg10_values.iter().max() {
620 if &sg6_val != max_val {
621 return ConditionResult::False;
622 }
623 }
624 }
625 ConditionResult::True
626 }
627
628 /// [126] wenn Plausibilisierungshinweise vorliegen
629 /// EXTERNAL: Requires context from outside the message.
630 fn evaluate_126(&self, ctx: &EvaluationContext) -> ConditionResult {
631 ctx.external.evaluate("plausibility_notes_present")
632 }
633
634 /// [127] wenn ein Korrekturgrund anzugeben ist
635 /// EXTERNAL: Requires context from outside the message.
636 fn evaluate_127(&self, ctx: &EvaluationContext) -> ConditionResult {
637 ctx.external.evaluate("correction_reason_required")
638 }
639
640 /// [130] Wenn innerhalb desselben LIN-Segments neben diesem Segment (SG10 DTM+7 Nutzungszeitpunkt) noch das SG10 DTM+60 (Ausführungs- / Änderungszeitpunkt) oder das SG10 DTM+9 (Ablesedatum) vorhanden, dar...
641 // REVIEW: Within each SG5.SG6.SG10 (LIN) instance, if DTM+7 co-occurs with DTM+60 or DTM+9 (all format 303 CCYYMMDDHHMM, UTC), the absolute difference in minutes must be < 24 h (1440 min). If an EU DST transition falls between the two instants, the bound is relaxed to < 25 h (1500 min, summer→winter) or tightened to < 23 h (1380 min, winter→summer). Uses parse_dtm303 + dst_transitions_between helpers.
642 fn evaluate_130(&self, ctx: &EvaluationContext) -> ConditionResult {
643 let nav = match ctx.navigator() {
644 Some(n) => n,
645 None => return ConditionResult::Unknown,
646 };
647 let sg10_count = nav.group_instance_count(&["SG5", "SG6", "SG9", "SG10"]);
648 if sg10_count == 0 {
649 return ConditionResult::Unknown;
650 }
651 let dtm_value = |dtms: &[mig_types::segment::OwnedSegment], qual: &str| -> Option<String> {
652 dtms.iter()
653 .find(|s| {
654 s.elements
655 .first()
656 .and_then(|e| e.first())
657 .is_some_and(|v| v == qual)
658 })
659 .and_then(|s| s.elements.first())
660 .and_then(|e| e.get(1))
661 .filter(|v| !v.is_empty())
662 .cloned()
663 };
664 let mut observed = false;
665 for i in 0..sg10_count {
666 let dtms = nav.find_segments_in_group("DTM", &["SG5", "SG6", "SG9", "SG10"], i);
667 let Some(dtm7_raw) = dtm_value(&dtms, "7") else {
668 continue;
669 };
670 let Some(dtm7_min) = parse_dtm303(&dtm7_raw) else {
671 continue;
672 };
673 for qual in ["60", "9"] {
674 let Some(other_raw) = dtm_value(&dtms, qual) else {
675 continue;
676 };
677 let Some(other_min) = parse_dtm303(&other_raw) else {
678 continue;
679 };
680 observed = true;
681 let diff_min = (dtm7_min - other_min).abs();
682 let dst_delta = dst_transitions_between(dtm7_min, other_min);
683 let bound_min: i64 = match dst_delta.signum() {
684 -1 => 1500,
685 1 => 1380,
686 _ => 1440,
687 };
688 if diff_min >= bound_min {
689 return ConditionResult::False;
690 }
691 }
692 }
693 if observed {
694 ConditionResult::True
695 } else {
696 ConditionResult::Unknown
697 }
698 }
699
700 /// [133] Wenn innerhalb desselben LIN-Segments neben diesem Segment (SG10 DTM+7 Nutzungszeitpunkt) noch das SG10 DTM+9 (Ablesedatum) mit dem Code 102 im DE2379 vorhanden ist, darf der Wert der Differenz zwi...
701 // REVIEW: Checks that when DTM+7 (Nutzungszeitpunkt, format 303 CCYYMMDDHHMM) and DTM+9 (Ablesedatum, format 102 CCYYMMDD) co-exist in the same SG10 instance, the CCYYMMDD date part of DTM+7 must equal or be at most 1 calendar day different from DTM+9. Uses a Julian Day Number formula for correct cross-month/year date arithmetic. MSCONS SG10 path assumed as SG5.SG6.SG10 based on the tx_group=SG5 and SG6 being the measurement period container; medium confidence due to path uncertainty. (medium confidence)
702 fn evaluate_133(&self, ctx: &EvaluationContext) -> ConditionResult {
703 let nav = match ctx.navigator() {
704 Some(n) => n,
705 None => return ConditionResult::Unknown,
706 };
707 fn date_to_days(s: &str) -> Option<i64> {
708 if s.len() < 8 {
709 return None;
710 }
711 let y: i64 = s[..4].parse().ok()?;
712 let m: i64 = s[4..6].parse().ok()?;
713 let d: i64 = s[6..8].parse().ok()?;
714 if m < 1 || m > 12 || d < 1 || d > 31 {
715 return None;
716 }
717 let a = (14 - m) / 12;
718 let yy = y + 4800 - a;
719 let mm = m + 12 * a - 3;
720 Some(d + (153 * mm + 2) / 5 + 365 * yy + yy / 4 - yy / 100 + yy / 400 - 32045)
721 }
722 let sg10_count = nav.group_instance_count(&["SG5", "SG6", "SG9", "SG10"]);
723 if sg10_count == 0 {
724 return ConditionResult::Unknown;
725 }
726 for i in 0..sg10_count {
727 let dtms = nav.find_segments_in_group("DTM", &["SG5", "SG6", "SG9", "SG10"], i);
728 let dtm7_val = dtms
729 .iter()
730 .find(|s| {
731 s.elements
732 .first()
733 .and_then(|e| e.first())
734 .is_some_and(|v| v == "7")
735 })
736 .and_then(|s| s.elements.first())
737 .and_then(|e| e.get(1))
738 .cloned();
739 let dtm9_val = dtms
740 .iter()
741 .find(|s| {
742 s.elements
743 .first()
744 .and_then(|e| e.first())
745 .is_some_and(|v| v == "9")
746 && s.elements
747 .first()
748 .and_then(|e| e.get(2))
749 .is_some_and(|v| v == "102")
750 })
751 .and_then(|s| s.elements.first())
752 .and_then(|e| e.get(1))
753 .cloned();
754 let (v7, v9) = match (dtm7_val, dtm9_val) {
755 (Some(v7), Some(v9)) if !v7.is_empty() && !v9.is_empty() => (v7, v9),
756 _ => continue,
757 };
758 let date7 = if v7.len() >= 8 { &v7[..8] } else { continue };
759 let date9 = if v9.len() >= 8 { &v9[..8] } else { v9.as_str() };
760 let days7 = match date_to_days(date7) {
761 Some(d) => d,
762 None => return ConditionResult::Unknown,
763 };
764 let days9 = match date_to_days(date9) {
765 Some(d) => d,
766 None => return ConditionResult::Unknown,
767 };
768 let diff = (days7 - days9).abs();
769 if diff > 1 {
770 return ConditionResult::False;
771 }
772 }
773 ConditionResult::True
774 }
775
776 /// [138] Wenn es sich um eine Korrekturenergiemenge auf einen Wert aus einem iMS handelt
777 /// EXTERNAL: Requires context from outside the message.
778 fn evaluate_138(&self, ctx: &EvaluationContext) -> ConditionResult {
779 ctx.external.evaluate("is_correction_energy_from_ims")
780 }
781
782 /// [141] Wenn MP-ID in SG2 NAD+MR in der Rolle MGV
783 /// EXTERNAL: Requires context from outside the message.
784 fn evaluate_141(&self, ctx: &EvaluationContext) -> ConditionResult {
785 ctx.external.evaluate("recipient_is_mgv")
786 }
787
788 /// [490] wenn Wert in diesem DE, an der Stelle CCYYMMDDHHMM ein Zeitpunkt aus dem angegeben Zeitraum der Tabelle Kapitel 3.5 „Übersicht gesetzliche deutsche Sommerzeit (MESZ)“ der Spalten: „Sommerzei...
789 fn evaluate_490(&self, ctx: &EvaluationContext) -> ConditionResult {
790 match ctx.resolved_value {
791 Some(val) => is_mesz_utc(val),
792 None => ConditionResult::False,
793 }
794 }
795
796 /// [491] wenn Wert in diesem DE, an der Stelle CCYYMMDDHHMM ein Zeitpunkt aus dem angegeben Zeitraum der Tabelle Kapitel 3.6 „Übersicht gesetzliche deutsche Zeit (MEZ)“ der Spalten: „Winterzeit (MEZ)...
797 // REVIEW: Checks whether the datetime value falls within German winter time (MEZ, UTC+1) using the is_mez_utc helper. Medium confidence because 'diesem DE' is context-dependent — without knowing the exact field the condition is applied to in the AHB table, we check the first DTM value available. The is_mez_utc helper applies the EU DST rule to determine MEZ vs MESZ. (medium confidence)
798 fn evaluate_491(&self, ctx: &EvaluationContext) -> ConditionResult {
799 match ctx.resolved_value {
800 Some(val) => is_mez_utc(val),
801 None => ConditionResult::False,
802 }
803 }
804
805 /// [494] Das hier genannte Datum muss der Zeitpunkt sein, zu dem das Dokument erstellt wurde, oder ein Zeitpunkt, der davor liegt.
806 // HAND-EDITED: subject is the current DTM's DE2380 (via ctx.resolved_value), not a
807 // guessed other qualifier. Lexicographic compare is valid for fixed-width
808 // CCYYMMDDHHMM+ZZ (format 303) and CCYYMMDD (format 102) when compared prefix-wise.
809 fn evaluate_494(&self, ctx: &EvaluationContext) -> ConditionResult {
810 let current = match ctx.resolved_value {
811 Some(v) if !v.is_empty() => v,
812 _ => return ConditionResult::Unknown,
813 };
814 let threshold = match ctx
815 .find_segments_with_qualifier("DTM", 0, "137")
816 .first()
817 .and_then(|s| s.elements.first())
818 .and_then(|e| e.get(1))
819 {
820 Some(v) if !v.is_empty() => v.clone(),
821 _ => return ConditionResult::Unknown,
822 };
823 ConditionResult::from(current <= threshold.as_str())
824 }
825
826 /// [495] Der Zeitpunkt muss ≤ dem Wert im DE2380 des DTM+137 sein
827 // HAND-EDITED: subject is the current DTM's DE2380 (via ctx.resolved_value), not a
828 // guessed other qualifier. Matches the invoic_conditions pattern.
829 fn evaluate_495(&self, ctx: &EvaluationContext) -> ConditionResult {
830 let current = match ctx.resolved_value {
831 Some(v) if !v.is_empty() => v,
832 _ => return ConditionResult::Unknown,
833 };
834 let threshold = match ctx
835 .find_segments_with_qualifier("DTM", 0, "137")
836 .first()
837 .and_then(|s| s.elements.first())
838 .and_then(|e| e.get(1))
839 {
840 Some(v) if !v.is_empty() => v.clone(),
841 _ => return ConditionResult::Unknown,
842 };
843 ConditionResult::from(current <= threshold.as_str())
844 }
845
846 /// [922] Format: TR-ID
847 // REVIEW: Format: TR-ID — validates that the value is a valid Transaktionsreferenz-ID (1–35 alphanumeric characters) using validate_tr_id. In MSCONS, the document/transaction ID is typically found in BGM elements[1][0] (the document number composite). Medium confidence because the exact DE this format condition is applied to depends on the AHB field context, which could alternatively be RFF or another reference segment. (medium confidence)
848 fn evaluate_922(&self, ctx: &EvaluationContext) -> ConditionResult {
849 ctx.format_check("BGM", 1, 0, validate_tr_id)
850 }
851
852 /// [1] Sofern per ORDERS angefordert
853 /// EXTERNAL: Requires context from outside the message.
854 fn evaluate_1(&self, ctx: &EvaluationContext) -> ConditionResult {
855 ctx.external.evaluate("requested_via_orders")
856 }
857
858 /// [2] Wenn das Zeitintervall zwischen ersten SG10 DTM+163 und letzten SG10 DTM+164 mindestens einen Monat umfasst
859 // REVIEW: Parses year+month from DTM+163 (first) and DTM+164 (last) in format 303 (YYYYMMDDHHmm). Computes calendar-month difference. 'Mindestens einen Monat' interpreted as end month > start month (calendar boundary). Medium confidence due to ambiguity between calendar-month vs. 30-day interpretation. (medium confidence)
860 fn evaluate_2(&self, ctx: &EvaluationContext) -> ConditionResult {
861 let dtm_163 = ctx.find_segments_with_qualifier("DTM", 0, "163");
862 let dtm_164 = ctx.find_segments_with_qualifier("DTM", 0, "164");
863 let first_start = dtm_163
864 .first()
865 .and_then(|s| s.elements.first())
866 .and_then(|e| e.get(1))
867 .map(|s| s.as_str());
868 let last_end = dtm_164
869 .last()
870 .and_then(|s| s.elements.first())
871 .and_then(|e| e.get(1))
872 .map(|s| s.as_str());
873 match (first_start, last_end) {
874 (Some(start), Some(end)) if start.len() >= 6 && end.len() >= 6 => {
875 let start_year: u32 = start[0..4].parse().unwrap_or(0);
876 let start_month: u32 = start[4..6].parse().unwrap_or(0);
877 let end_year: u32 = end[0..4].parse().unwrap_or(0);
878 let end_month: u32 = end[4..6].parse().unwrap_or(0);
879 let start_total = start_year * 12 + start_month;
880 let end_total = end_year * 12 + end_month;
881 ConditionResult::from(end_total >= start_total + 1)
882 }
883 _ => ConditionResult::Unknown,
884 }
885 }
886
887 /// [11] Wenn SG9 PIA+5+7-0?:52.0.22/7-0?:54.0.16/7-0?:54.0.20/7-0?:54.0.22
888 fn evaluate_11(&self, ctx: &EvaluationContext) -> ConditionResult {
889 let pia_segments = ctx.find_segments_with_qualifier("PIA", 0, "5");
890 let obis_values = ["7-0:52.0.22", "7-0:54.0.16", "7-0:54.0.20", "7-0:54.0.22"];
891 ConditionResult::from(pia_segments.iter().any(|s| {
892 s.elements
893 .get(1)
894 .and_then(|e| e.first())
895 .is_some_and(|v| obis_values.contains(&v.as_str()))
896 }))
897 }
898
899 /// [12] Wenn nicht SG9 PIA+5+7-0?:52.0.22/7-0?:54.0.16/7-0?:54.0.20/7-0?:54.0.22
900 fn evaluate_12(&self, ctx: &EvaluationContext) -> ConditionResult {
901 let pia_segments = ctx.find_segments_with_qualifier("PIA", 0, "5");
902 let obis_values = ["7-0:52.0.22", "7-0:54.0.16", "7-0:54.0.20", "7-0:54.0.22"];
903 ConditionResult::from(!pia_segments.iter().any(|s| {
904 s.elements
905 .get(1)
906 .and_then(|e| e.first())
907 .is_some_and(|v| obis_values.contains(&v.as_str()))
908 }))
909 }
910
911 /// [17] Wenn nicht SG9 PIA+5+1-b?:9.99.0 (b= Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien)
912 // REVIEW: Checks absence of PIA+5 with OBIS matching pattern '1-{channel}:9.99.0'. The 'b' is a variable channel, so pattern-matching on prefix '1-' and suffix ':9.99.0' is used. Negated because condition is 'Wenn NICHT'. Medium confidence due to pattern-based matching of OBIS code. (medium confidence)
913 fn evaluate_17(&self, ctx: &EvaluationContext) -> ConditionResult {
914 let pia_segments = ctx.find_segments_with_qualifier("PIA", 0, "5");
915 let has_match = pia_segments.iter().any(|s| {
916 s.elements
917 .get(1)
918 .and_then(|e| e.first())
919 .is_some_and(|v| v.starts_with("1-") && v.ends_with(":9.99.0"))
920 });
921 ConditionResult::from(!has_match)
922 }
923
924 /// [18] Wenn SG9 PIA+5+1-b?:9.99.0 (b= Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien)
925 // REVIEW: Affirmative version of condition 17. True when any PIA+5 has OBIS code matching the '1-{channel}:9.99.0' pattern (combined heat/power or special metering). Medium confidence due to pattern matching. (medium confidence)
926 fn evaluate_18(&self, ctx: &EvaluationContext) -> ConditionResult {
927 let pia_segments = ctx.find_segments_with_qualifier("PIA", 0, "5");
928 ConditionResult::from(pia_segments.iter().any(|s| {
929 s.elements
930 .get(1)
931 .and_then(|e| e.first())
932 .is_some_and(|v| v.starts_with("1-") && v.ends_with(":9.99.0"))
933 }))
934 }
935
936 /// [22] Wenn Aufteilung vorhanden
937 /// EXTERNAL: Requires context from outside the message.
938 fn evaluate_22(&self, ctx: &EvaluationContext) -> ConditionResult {
939 ctx.external.evaluate("message_splitting")
940 }
941
942 /// [27] Wenn SG9 PIA+5+1-1?:1.9.0 vorhanden
943 fn evaluate_27(&self, ctx: &EvaluationContext) -> ConditionResult {
944 let pia_segments = ctx.find_segments_with_qualifier("PIA", 0, "5");
945 ConditionResult::from(pia_segments.iter().any(|s| {
946 s.elements
947 .get(1)
948 .and_then(|e| e.first())
949 .is_some_and(|v| v == "1-1:1.9.0")
950 }))
951 }
952
953 /// [28] Wenn SG9 PIA+5+1-1?:1.9.0 nicht vorhanden
954 fn evaluate_28(&self, ctx: &EvaluationContext) -> ConditionResult {
955 let pia_segments = ctx.find_segments_with_qualifier("PIA", 0, "5");
956 ConditionResult::from(!pia_segments.iter().any(|s| {
957 s.elements
958 .get(1)
959 .and_then(|e| e.first())
960 .is_some_and(|v| v == "1-1:1.9.0")
961 }))
962 }
963
964 /// [32] wenn MP-ID in SG2 NAD+MS in der Rolle NB
965 /// EXTERNAL: Requires context from outside the message.
966 fn evaluate_32(&self, ctx: &EvaluationContext) -> ConditionResult {
967 ctx.external.evaluate("sender_is_nb")
968 }
969
970 /// [33] wenn MP-ID in SG2 NAD+MR in der Rolle LF
971 /// EXTERNAL: Requires context from outside the message.
972 fn evaluate_33(&self, ctx: &EvaluationContext) -> ConditionResult {
973 ctx.external.evaluate("recipient_is_lf")
974 }
975
976 /// [35] wenn MP-ID in SG2 NAD+MS in der Rolle MSB
977 /// EXTERNAL: Requires context from outside the message.
978 fn evaluate_35(&self, ctx: &EvaluationContext) -> ConditionResult {
979 ctx.external.evaluate("sender_is_msb")
980 }
981
982 /// [36] wenn MP-ID in SG2 NAD+MR in der Rolle NB
983 /// EXTERNAL: Requires context from outside the message.
984 fn evaluate_36(&self, ctx: &EvaluationContext) -> ConditionResult {
985 ctx.external.evaluate("recipient_is_nb")
986 }
987
988 /// [38] wenn in SG6 LOC+172 DE3225 die ID der Messlokation angegeben ist
989 fn evaluate_38(&self, ctx: &EvaluationContext) -> ConditionResult {
990 // Messlokation = 33-char alphanumeric Zählpunktbezeichnung. A non-empty
991 // value that doesn't match (e.g. an 11-digit MaLo-ID) must be False so
992 // the AHB's XOR branches against condition [119] stay mutually exclusive.
993 let loc_segments = ctx.find_segments_with_qualifier("LOC", 0, "172");
994 match loc_segments.first() {
995 Some(loc) => match loc.elements.get(1).and_then(|e| e.first()) {
996 Some(v) if !v.is_empty() => validate_zahlpunkt(v),
997 _ => ConditionResult::False,
998 },
999 None => ConditionResult::False,
1000 }
1001 }
1002
1003 /// [42] Wenn MP-ID in SG2 NAD+MR in der Rolle MSB
1004 /// EXTERNAL: Requires context from outside the message.
1005 fn evaluate_42(&self, ctx: &EvaluationContext) -> ConditionResult {
1006 ctx.external.evaluate("recipient_is_msb")
1007 }
1008
1009 /// [45] Wenn SG9 PIA+5+7-b:99.41.16 (b=Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien) vorhanden
1010 fn evaluate_45(&self, ctx: &EvaluationContext) -> ConditionResult {
1011 let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
1012 ConditionResult::from(pias.iter().any(|s| {
1013 s.elements
1014 .get(1)
1015 .and_then(|e| e.first())
1016 .is_some_and(|v| v.starts_with("7-") && v.ends_with(":99.41.16"))
1017 }))
1018 }
1019
1020 /// [46] Wenn Wert in SG6 LOC+172 DE3225 genau 11 Stellen
1021 fn evaluate_46(&self, ctx: &EvaluationContext) -> ConditionResult {
1022 let locs = ctx.find_segments_with_qualifier("LOC", 0, "172");
1023 if locs.is_empty() {
1024 return ConditionResult::Unknown;
1025 }
1026 ConditionResult::from(locs.iter().any(|s| {
1027 s.elements
1028 .get(1)
1029 .and_then(|e| e.first())
1030 .is_some_and(|v| v.len() == 11)
1031 }))
1032 }
1033
1034 /// [48] Wenn SG9 PIA+5+7-0?:52.0.22
1035 fn evaluate_48(&self, ctx: &EvaluationContext) -> ConditionResult {
1036 let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
1037 ConditionResult::from(pias.iter().any(|s| {
1038 s.elements
1039 .get(1)
1040 .and_then(|e| e.first())
1041 .is_some_and(|v| v == "7-0:52.0.22")
1042 }))
1043 }
1044
1045 /// [49] Wenn SG9 PIA+5+7-b?:70.16.16/7-b?:70.16.20/7-b?:70.16.22 (b=Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien) vorhanden
1046 fn evaluate_49(&self, ctx: &EvaluationContext) -> ConditionResult {
1047 let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
1048 ConditionResult::from(pias.iter().any(|s| {
1049 s.elements.get(1).and_then(|e| e.first()).is_some_and(|v| {
1050 v.starts_with("7-")
1051 && (v.ends_with(":70.16.16")
1052 || v.ends_with(":70.16.20")
1053 || v.ends_with(":70.16.22"))
1054 })
1055 }))
1056 }
1057
1058 /// [50] Wenn SG9 PIA+5+7-b?:70.18.16/7-b?:70.18.20/7-b?:70.18.22 (b=Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien) vorhanden
1059 fn evaluate_50(&self, ctx: &EvaluationContext) -> ConditionResult {
1060 let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
1061 ConditionResult::from(pias.iter().any(|s| {
1062 s.elements.get(1).and_then(|e| e.first()).is_some_and(|v| {
1063 v.starts_with("7-")
1064 && (v.ends_with(":70.18.16")
1065 || v.ends_with(":70.18.20")
1066 || v.ends_with(":70.18.22"))
1067 })
1068 }))
1069 }
1070
1071 /// [51] Wenn SG9 PIA+5+7-0?:33.86.0 vorhanden ist, darf mittels Wiederholung SG9 LIN in derselben Nachricht das SG9 PIA+5+7-0?:52.0.22/7-0?:54.0.16/7-0?:54.0.20/7-0?:54.0.22 nicht mehr angegeben werden
1072 // REVIEW: Mutual-exclusion constraint: if 7-0:33.86.0 is present, none of the listed 52/54.x codes may appear. Returns True when constraint is satisfied (i.e. 33.86.0 absent, or 33.86.0 present but excluded codes absent). Returns True when 33.86.0 is not present because the constraint simply does not apply. (medium confidence)
1073 fn evaluate_51(&self, ctx: &EvaluationContext) -> ConditionResult {
1074 let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
1075 let has_3386 = pias.iter().any(|s| {
1076 s.elements
1077 .get(1)
1078 .and_then(|e| e.first())
1079 .is_some_and(|v| v == "7-0:33.86.0")
1080 });
1081 if !has_3386 {
1082 return ConditionResult::True;
1083 }
1084 let excluded = ["7-0:52.0.22", "7-0:54.0.16", "7-0:54.0.20", "7-0:54.0.22"];
1085 ConditionResult::from(!pias.iter().any(|s| {
1086 s.elements
1087 .get(1)
1088 .and_then(|e| e.first())
1089 .is_some_and(|v| excluded.contains(&v.as_str()))
1090 }))
1091 }
1092
1093 /// [62] Wenn Wert in SG6 LOC+172 DE3225 genau 33 Stellen
1094 fn evaluate_62(&self, ctx: &EvaluationContext) -> ConditionResult {
1095 let locs = ctx.find_segments_with_qualifier("LOC", 0, "172");
1096 if locs.is_empty() {
1097 return ConditionResult::Unknown;
1098 }
1099 ConditionResult::from(locs.iter().any(|s| {
1100 s.elements
1101 .get(1)
1102 .and_then(|e| e.first())
1103 .is_some_and(|v| v.len() == 33)
1104 }))
1105 }
1106
1107 /// [67] Wenn es sich um die Referenz auf eine ORDERS handelt
1108 /// EXTERNAL: Requires context from outside the message.
1109 // REVIEW: Checks whether the current MSCONS transaction references an ORDERS message. While RFF segments can carry cross-message references, the specific qualifier that marks an ORDERS reference is not provided in the segment structure reference, and determining whether a reference points to an ORDERS vs. another message type typically requires external business context about the originating transaction. (medium confidence)
1110 fn evaluate_67(&self, ctx: &EvaluationContext) -> ConditionResult {
1111 ctx.external.evaluate("is_orders_reference")
1112 }
1113
1114 /// [68] Wenn BGM+7 vorhanden
1115 fn evaluate_68(&self, ctx: &EvaluationContext) -> ConditionResult {
1116 ctx.has_qualifier("BGM", 0, "7")
1117 }
1118
1119 /// [69] Wenn BGM+Z28 vorhanden
1120 fn evaluate_69(&self, ctx: &EvaluationContext) -> ConditionResult {
1121 ctx.has_qualifier("BGM", 0, "Z28")
1122 }
1123
1124 /// [70] Wenn BGM+BK vorhanden
1125 fn evaluate_70(&self, ctx: &EvaluationContext) -> ConditionResult {
1126 ctx.has_qualifier("BGM", 0, "BK")
1127 }
1128
1129 /// [71] Wenn BGM+Z39 vorhanden
1130 fn evaluate_71(&self, ctx: &EvaluationContext) -> ConditionResult {
1131 ctx.has_qualifier("BGM", 0, "Z39")
1132 }
1133
1134 /// [72] Wenn SG9 PIA+5+1-b?:1.6.0/1-b?:3.6.0/1-b?:4.6.0/1-66?:13.6.0/1-66?:14.6.0 (b=Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien) vorhanden
1135 fn evaluate_72(&self, ctx: &EvaluationContext) -> ConditionResult {
1136 let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
1137 ConditionResult::from(pias.iter().any(|s| {
1138 s.elements.get(1).and_then(|e| e.first()).is_some_and(|v| {
1139 (v.starts_with("1-")
1140 && (v.ends_with(":1.6.0") || v.ends_with(":3.6.0") || v.ends_with(":4.6.0")))
1141 || v == "1-66:13.6.0"
1142 || v == "1-66:14.6.0"
1143 })
1144 }))
1145 }
1146
1147 /// [73] Wenn SG9 PIA+5+1-b?:1.9.e/1-b?:3.9.0/1-b?:4.9.0/1-66?:13.9.0/1-66?:14.9.0 (b=Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien, e=Tarif: Wert gemäß Codeliste der OBIS-Kennzahlen und Me...
1148 fn evaluate_73(&self, ctx: &EvaluationContext) -> ConditionResult {
1149 let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
1150 ConditionResult::from(pias.iter().any(|s| {
1151 s.elements.get(1).and_then(|e| e.first()).is_some_and(|v| {
1152 (v.starts_with("1-") && v.contains(":1.9."))
1153 || (v.starts_with("1-") && v.ends_with(":3.9.0"))
1154 || (v.starts_with("1-") && v.ends_with(":4.9.0"))
1155 || v == "1-66:13.9.0"
1156 || v == "1-66:14.9.0"
1157 })
1158 }))
1159 }
1160
1161 /// [77] Wenn MP-ID in SG2 NAD+MR der RB HKN-R
1162 /// EXTERNAL: Requires context from outside the message.
1163 fn evaluate_77(&self, ctx: &EvaluationContext) -> ConditionResult {
1164 ctx.external
1165 .evaluate("recipient_is_hkn_registration_authority")
1166 }
1167
1168 /// [78] Wenn SG9 PIA+5+1-66?:13.6.0/1-66?:14.6.0/1-66?:13.9.0/1-66?:14.9.0 vorhanden
1169 // REVIEW: PIA+5 means elements[0][0]='5'. DE7140 (OBIS code) is elements[1][0]. EDIFACT '?:' escape sequences are resolved by the parser to literal ':', so '1-66?:13.6.0' becomes '1-66:13.6.0'. Checks any SG9 group instance for PIA+5 with one of the four OBIS codes. Group path SG6->SG9 is approximate based on MSCONS structure. (medium confidence)
1170 fn evaluate_78(&self, ctx: &EvaluationContext) -> ConditionResult {
1171 ctx.any_group_has_qualified_value(
1172 "PIA",
1173 0,
1174 "5",
1175 1,
1176 0,
1177 &["1-66:13.6.0", "1-66:14.6.0", "1-66:13.9.0", "1-66:14.9.0"],
1178 &["SG6", "SG9"],
1179 )
1180 }
1181
1182 /// [79] Wenn SG9 PIA+5+1-66?:13.6.0/1-66?:14.6.0/1-66?:13.9.0/1-66?:14.9.0 nicht vorhanden
1183 // REVIEW: Logical negation of condition 78. True when none of the specified OBIS codes appears in any SG9 PIA+5 segment. (medium confidence)
1184 fn evaluate_79(&self, ctx: &EvaluationContext) -> ConditionResult {
1185 match ctx.any_group_has_qualified_value(
1186 "PIA",
1187 0,
1188 "5",
1189 1,
1190 0,
1191 &["1-66:13.6.0", "1-66:14.6.0", "1-66:13.9.0", "1-66:14.9.0"],
1192 &["SG6", "SG9"],
1193 ) {
1194 ConditionResult::True => ConditionResult::False,
1195 ConditionResult::False => ConditionResult::True,
1196 ConditionResult::Unknown => ConditionResult::Unknown,
1197 }
1198 }
1199
1200 /// [80] Wenn MP-ID in SG2 NAD+MR in der Rolle ÜNB
1201 /// EXTERNAL: Requires context from outside the message.
1202 fn evaluate_80(&self, ctx: &EvaluationContext) -> ConditionResult {
1203 ctx.external.evaluate("recipient_is_uenb")
1204 }
1205
1206 /// [83] Wenn in derselben SG9 LIN die Angabe STS+10+Z38 nicht vorhanden
1207 // REVIEW: STS 'Grundlage der Energiemenge' has elements[0][0]='10' (Statuskategorie) and elements[1][0]=status code. STS lives in SG10 (child of SG9). Condition is True when NO STS with category 10 and status Z38 exists within SG9/SG10 scope. Using any_group_has_qualified_value on SG9->SG10 path then negating. Group scope is approximate — a precise check would traverse SG9 instances and their SG10 children. (medium confidence)
1208 fn evaluate_83(&self, ctx: &EvaluationContext) -> ConditionResult {
1209 // HAND-EDITED: "in derselben SG9 LIN die Angabe STS+10+Z38 nicht vorhanden; the STS sits in its SG10" — the enclosing SG9 only, when the scope
1210 // reaches one; the message as before otherwise.
1211 if let Some(scope) = ctx.enclosing_scope("SG9") {
1212 return ConditionResult::from(
1213 !ctx.find_segments_at(scope, "STS")
1214 .iter()
1215 .any(|s| s.get_element(0) == "10" && s.get_component(1, 0) == "Z38"),
1216 );
1217 }
1218 match ctx.any_group_has_qualified_value("STS", 0, "10", 1, 0, &["Z38"], &["SG9", "SG10"]) {
1219 ConditionResult::True => ConditionResult::False,
1220 ConditionResult::False => ConditionResult::True,
1221 ConditionResult::Unknown => ConditionResult::Unknown,
1222 }
1223 }
1224
1225 /// [84] Wenn in derselben SG9 LIN die Angabe STS+10+Z39 nicht vorhanden
1226 // REVIEW: Same pattern as condition 83 but for status code Z39. True when no STS+10+Z39 is present in any SG10 within the SG9 scope. (medium confidence)
1227 fn evaluate_84(&self, ctx: &EvaluationContext) -> ConditionResult {
1228 // HAND-EDITED: "in derselben SG9 LIN die Angabe STS+10+Z39 nicht vorhanden; the STS sits in its SG10" — the enclosing SG9 only, when the scope
1229 // reaches one; the message as before otherwise.
1230 if let Some(scope) = ctx.enclosing_scope("SG9") {
1231 return ConditionResult::from(
1232 !ctx.find_segments_at(scope, "STS")
1233 .iter()
1234 .any(|s| s.get_element(0) == "10" && s.get_component(1, 0) == "Z39"),
1235 );
1236 }
1237 match ctx.any_group_has_qualified_value("STS", 0, "10", 1, 0, &["Z39"], &["SG9", "SG10"]) {
1238 ConditionResult::True => ConditionResult::False,
1239 ConditionResult::False => ConditionResult::True,
1240 ConditionResult::Unknown => ConditionResult::Unknown,
1241 }
1242 }
1243
1244 /// [85] Wenn in derselben SG9 LIN die Angabe STS+10+Z36 nicht vorhanden
1245 // REVIEW: Same pattern as condition 83 but for status code Z36. True when no STS+10+Z36 is present in any SG10 within the SG9 scope. (medium confidence)
1246 fn evaluate_85(&self, ctx: &EvaluationContext) -> ConditionResult {
1247 // HAND-EDITED: "in derselben SG9 LIN die Angabe STS+10+Z36 nicht vorhanden; the STS sits in its SG10" — the enclosing SG9 only, when the scope
1248 // reaches one; the message as before otherwise.
1249 if let Some(scope) = ctx.enclosing_scope("SG9") {
1250 return ConditionResult::from(
1251 !ctx.find_segments_at(scope, "STS")
1252 .iter()
1253 .any(|s| s.get_element(0) == "10" && s.get_component(1, 0) == "Z36"),
1254 );
1255 }
1256 match ctx.any_group_has_qualified_value("STS", 0, "10", 1, 0, &["Z36"], &["SG9", "SG10"]) {
1257 ConditionResult::True => ConditionResult::False,
1258 ConditionResult::False => ConditionResult::True,
1259 ConditionResult::Unknown => ConditionResult::Unknown,
1260 }
1261 }
1262
1263 /// [86] Wenn in derselben SG9 LIN die Angabe STS+10+Z37 nicht vorhanden
1264 // REVIEW: Same pattern as condition 83 but for status code Z37. True when no STS+10+Z37 is present in any SG10 within the SG9 scope. (medium confidence)
1265 fn evaluate_86(&self, ctx: &EvaluationContext) -> ConditionResult {
1266 // HAND-EDITED: "in derselben SG9 LIN die Angabe STS+10+Z37 nicht vorhanden; the STS sits in its SG10" — the enclosing SG9 only, when the scope
1267 // reaches one; the message as before otherwise.
1268 if let Some(scope) = ctx.enclosing_scope("SG9") {
1269 return ConditionResult::from(
1270 !ctx.find_segments_at(scope, "STS")
1271 .iter()
1272 .any(|s| s.get_element(0) == "10" && s.get_component(1, 0) == "Z37"),
1273 );
1274 }
1275 match ctx.any_group_has_qualified_value("STS", 0, "10", 1, 0, &["Z37"], &["SG9", "SG10"]) {
1276 ConditionResult::True => ConditionResult::False,
1277 ConditionResult::False => ConditionResult::True,
1278 ConditionResult::Unknown => ConditionResult::Unknown,
1279 }
1280 }
1281
1282 /// [90] Wenn BGM+Z41 vorhanden
1283 fn evaluate_90(&self, ctx: &EvaluationContext) -> ConditionResult {
1284 ctx.has_qualifier("BGM", 0, "Z41")
1285 }
1286
1287 /// [91] Wenn BGM+Z42 vorhanden
1288 fn evaluate_91(&self, ctx: &EvaluationContext) -> ConditionResult {
1289 ctx.has_qualifier("BGM", 0, "Z42")
1290 }
1291
1292 /// [92] Wenn SG10 QTY DE6063 mit Wert 67 vorhanden
1293 fn evaluate_92(&self, ctx: &EvaluationContext) -> ConditionResult {
1294 // HAND-EDITED (#334): the QTY of this SG10 — STS combines [92]/[93]/[94]
1295 // with `⊻`, which holds only per SG10.
1296 ctx.this_group_has_qualifier("QTY", 0, "67", &["SG5", "SG6", "SG9", "SG10"])
1297 }
1298
1299 /// [93] Wenn SG10 QTY DE6063 mit Wert 220 vorhanden
1300 fn evaluate_93(&self, ctx: &EvaluationContext) -> ConditionResult {
1301 // HAND-EDITED (#334): the QTY of this SG10 — STS combines [92]/[93]/[94]
1302 // with `⊻`, which holds only per SG10.
1303 ctx.this_group_has_qualifier("QTY", 0, "220", &["SG5", "SG6", "SG9", "SG10"])
1304 }
1305
1306 /// [94] Wenn SG10 QTY DE6063 mit Wert 201 vorhanden
1307 fn evaluate_94(&self, ctx: &EvaluationContext) -> ConditionResult {
1308 // HAND-EDITED (#334): the QTY of this SG10 — STS combines [92]/[93]/[94]
1309 // with `⊻`, which holds only per SG10.
1310 ctx.this_group_has_qualifier("QTY", 0, "201", &["SG5", "SG6", "SG9", "SG10"])
1311 }
1312
1313 /// [95] Wenn SG10 QTY DE6063 mit Wert 20 vorhanden
1314 fn evaluate_95(&self, ctx: &EvaluationContext) -> ConditionResult {
1315 ctx.any_group_has_qualifier("QTY", 0, "20", &["SG5", "SG10"])
1316 }
1317
1318 /// [96] Wenn SG10 QTY DE6063 mit Wert Z18 vorhanden
1319 fn evaluate_96(&self, ctx: &EvaluationContext) -> ConditionResult {
1320 ctx.any_group_has_qualifier("QTY", 0, "Z18", &["SG5", "SG10"])
1321 }
1322
1323 /// [97] Wenn es sich um die Übermittlung eines Wertes aufgrund der Umstellung der Gasqualität handelt
1324 /// EXTERNAL: Requires context from outside the message.
1325 // REVIEW: Whether this transmission is caused by a gas quality changeover (H-Gas/L-Gas conversion) cannot be determined from EDIFACT message content alone — it is a business context condition. (medium confidence)
1326 fn evaluate_97(&self, ctx: &EvaluationContext) -> ConditionResult {
1327 ctx.external.evaluate("gas_quality_change_transmission")
1328 }
1329
1330 /// [98] Wenn SG9 PIA+5+SOL:Z08 vorhanden
1331 fn evaluate_98(&self, ctx: &EvaluationContext) -> ConditionResult {
1332 let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
1333 ConditionResult::from(pias.iter().any(|s| {
1334 s.elements
1335 .get(1)
1336 .and_then(|e| e.first())
1337 .is_some_and(|v| v == "SOL")
1338 && s.elements
1339 .get(1)
1340 .and_then(|e| e.get(1))
1341 .is_some_and(|v| v == "Z08")
1342 }))
1343 }
1344
1345 /// [99] Wenn SG9 PIA+5+WID:Z08 vorhanden
1346 fn evaluate_99(&self, ctx: &EvaluationContext) -> ConditionResult {
1347 let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
1348 ConditionResult::from(pias.iter().any(|s| {
1349 s.elements
1350 .get(1)
1351 .and_then(|e| e.first())
1352 .is_some_and(|v| v == "WID")
1353 && s.elements
1354 .get(1)
1355 .and_then(|e| e.get(1))
1356 .is_some_and(|v| v == "Z08")
1357 }))
1358 }
1359
1360 /// [100] Wenn in derselben SG9 LIN das PIA+5+AUA:Z08 vorhanden
1361 fn evaluate_100(&self, ctx: &EvaluationContext) -> ConditionResult {
1362 // HAND-EDITED: "in dieser/derselben SG9" — scoped to the enclosing group.
1363 ctx.this_group_has_qualified_value("PIA", 0, "5", 1, 0, &["AUA"], &["SG5", "SG9"])
1364 }
1365
1366 /// [101] Wenn in derselben SG9 LIN das PIA+5+FPA:Z08 vorhanden
1367 fn evaluate_101(&self, ctx: &EvaluationContext) -> ConditionResult {
1368 // HAND-EDITED: "in dieser/derselben SG9" — scoped to the enclosing group.
1369 ctx.this_group_has_qualified_value("PIA", 0, "5", 1, 0, &["FPA"], &["SG5", "SG9"])
1370 }
1371
1372 /// [108] wenn SG9 PIA+5+7-b?:99.41.16/7-b?:99.42.16 (b=Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien) vorhanden
1373 // REVIEW: Checks PIA+5 for OBIS codes matching pattern '7-{channel}:99.41.16' or '7-{channel}:99.42.16' (Wirkenergie-Kennzahlen). The '?' in AHB notation is the EDIFACT release character escaping the colon; parsed segments will contain literal colon. Channel variable 'b' matched via starts_with("7-"). (medium confidence)
1374 fn evaluate_108(&self, ctx: &EvaluationContext) -> ConditionResult {
1375 let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
1376 ConditionResult::from(pias.iter().any(|s| {
1377 let obis = match s.elements.get(1).and_then(|e| e.first()) {
1378 Some(v) => v.as_str(),
1379 None => return false,
1380 };
1381 let code_list = s
1382 .elements
1383 .get(1)
1384 .and_then(|e| e.get(1))
1385 .map(|v| v.as_str())
1386 .unwrap_or("");
1387 code_list == "Z08"
1388 && obis.starts_with("7-")
1389 && (obis.contains(":99.41.16") || obis.contains(":99.42.16"))
1390 }))
1391 }
1392
1393 /// [111] Wenn SG10 DTM+9 DE2379 in demselben Segment mit Wert 303 vorhanden
1394 fn evaluate_111(&self, ctx: &EvaluationContext) -> ConditionResult {
1395 ConditionResult::from(ctx.self_segment_value_equals("DTM", 0, 2, "303"))
1396 }
1397
1398 /// [113] wenn SG7 RFF+AGK (Konfigurations-ID) vorhanden
1399 fn evaluate_113(&self, ctx: &EvaluationContext) -> ConditionResult {
1400 ctx.has_qualifier("RFF", 0, "AGK")
1401 }
1402
1403 /// [117] Nur MP-ID aus Sparte Strom
1404 /// EXTERNAL: Requires context from outside the message.
1405 fn evaluate_117(&self, ctx: &EvaluationContext) -> ConditionResult {
1406 ctx.external.evaluate("marktpartner_is_strom")
1407 }
1408
1409 /// [118] Nur MP-ID aus Sparte Gas
1410 /// EXTERNAL: Requires context from outside the message.
1411 fn evaluate_118(&self, ctx: &EvaluationContext) -> ConditionResult {
1412 ctx.external.evaluate("marktpartner_is_gas")
1413 }
1414
1415 /// [119] wenn in SG6 LOC+172 DE3225 die ID der Marktlokation angegeben ist
1416 fn evaluate_119(&self, ctx: &EvaluationContext) -> ConditionResult {
1417 // Marktlokation = 11-digit MaLo-ID with BDEW check digit. A non-empty
1418 // value that doesn't match (e.g. a 33-char Zählpunkt) must be False so
1419 // the AHB's XOR branches against condition [38] stay mutually exclusive.
1420 let locs = ctx.find_segments_with_qualifier("LOC", 0, "172");
1421 match locs.first() {
1422 Some(loc) => match loc.elements.get(1).and_then(|e| e.first()) {
1423 Some(v) if !v.is_empty() => validate_malo_id(v),
1424 _ => ConditionResult::False,
1425 },
1426 None => ConditionResult::False,
1427 }
1428 }
1429
1430 /// [121] wenn BGM+Z43 (Redispatch Ausfallarbeitüberführungszeitreihe) vorhanden
1431 fn evaluate_121(&self, ctx: &EvaluationContext) -> ConditionResult {
1432 ctx.has_qualifier("BGM", 0, "Z43")
1433 }
1434
1435 /// [125] wenn SG9 PIA+5+7-0?:52.0.22/7-b?:53.0.16/7-b?:55.0.16/7-b?:55.0.20/7-b?:55.0.22 (b=Kanal: Wert gemäß Codeliste der OBIS-Kennzahlen und Medien) vorhanden
1436 // REVIEW: Checks PIA+5 for specific OBIS code patterns: '7-0:52.0.22' (fixed channel 0), '7-{b}:53.0.16', '7-{b}:55.0.16', '7-{b}:55.0.20', '7-{b}:55.0.22' (variable channel). These are Blindenergie/Leistungsmessungs-Kennzahlen. Uses contains() to match the OBIS suffix after the channel variable. (medium confidence)
1437 fn evaluate_125(&self, ctx: &EvaluationContext) -> ConditionResult {
1438 let pias = ctx.find_segments_with_qualifier("PIA", 0, "5");
1439 ConditionResult::from(pias.iter().any(|s| {
1440 let obis = match s.elements.get(1).and_then(|e| e.first()) {
1441 Some(v) => v.as_str(),
1442 None => return false,
1443 };
1444 let code_list = s
1445 .elements
1446 .get(1)
1447 .and_then(|e| e.get(1))
1448 .map(|v| v.as_str())
1449 .unwrap_or("");
1450 code_list == "Z08"
1451 && obis.starts_with("7-")
1452 && (obis.contains(":52.0.22")
1453 || obis.contains(":53.0.16")
1454 || obis.contains(":55.0.16")
1455 || obis.contains(":55.0.20")
1456 || obis.contains(":55.0.22"))
1457 }))
1458 }
1459
1460 /// [128] Wenn es sich um eine Ablesung handelt, welche keine Ablesung aufgrund der Änderung an der Messtechnik oder deren Konfiguration ist (z.B. Kundenablesung).
1461 // REVIEW: A regular reading (e.g. Kundenablesung) is distinguished from a meter-change reading by the absence of DTM+60 (Ausführungs-/Änderungszeitpunkt) in the same SG10. DTM+9 presence confirms it is a reading at all; DTM+60 absence confirms it is not due to a meter/config change. Group path SG9→SG10 matches MSCONS LIN-group context; fallback to message-wide if no navigator. (medium confidence)
1462 fn evaluate_128(&self, ctx: &EvaluationContext) -> ConditionResult {
1463 // True when SG10 has a reading date (DTM+9, Ablesedatum) but no device change timestamp (DTM+60),
1464 // indicating a regular reading not caused by meter change or configuration change
1465 ctx.any_group_has_qualifier_without("DTM", 0, "9", "DTM", 0, "60", &["SG9", "SG10"])
1466 }
1467
1468 /// [129] Wenn es sich um eine Ablesung aufgrund der Änderung an der Messtechnik oder deren Konfiguration handelt (z.B. Gerätewechsel).
1469 // REVIEW: A meter-change reading is identified by the co-occurrence of DTM+9 (Ablesedatum) and DTM+60 (Ausführungs-/Änderungszeitpunkt) within the same SG10 instance. Uses direct navigator iteration to check co-occurrence within a single group instance (any_group_has_co_occurrence would have ambiguity for same-tag different qualifiers). Falls back to message-wide check without navigator. (medium confidence)
1470 fn evaluate_129(&self, ctx: &EvaluationContext) -> ConditionResult {
1471 // True when SG10 contains both DTM+9 (Ablesedatum) and DTM+60 (Ausführungs-/Änderungszeitpunkt),
1472 // indicating a reading caused by a meter change or configuration change (e.g. Gerätewechsel)
1473 let nav = match ctx.navigator() {
1474 Some(n) => n,
1475 None => {
1476 let has9 = ctx.has_qualifier("DTM", 0, "9");
1477 let has60 = ctx.has_qualifier("DTM", 0, "60");
1478 return match (has9, has60) {
1479 (ConditionResult::True, ConditionResult::True) => ConditionResult::True,
1480 (ConditionResult::Unknown, _) | (_, ConditionResult::Unknown) => {
1481 ConditionResult::Unknown
1482 }
1483 _ => ConditionResult::False,
1484 };
1485 }
1486 };
1487 let sg10_count = nav.group_instance_count(&["SG9", "SG10"]);
1488 for i in 0..sg10_count {
1489 let dtms = nav.find_segments_in_group("DTM", &["SG9", "SG10"], i);
1490 let has9 = dtms.iter().any(|s| {
1491 s.elements
1492 .first()
1493 .and_then(|e| e.first())
1494 .is_some_and(|v| v == "9")
1495 });
1496 let has60 = dtms.iter().any(|s| {
1497 s.elements
1498 .first()
1499 .and_then(|e| e.first())
1500 .is_some_and(|v| v == "60")
1501 });
1502 if has9 && has60 {
1503 return ConditionResult::True;
1504 }
1505 }
1506 ConditionResult::False
1507 }
1508
1509 /// [131] wenn RFF+AGK (Konfigurations-ID) nicht vorhanden
1510 fn evaluate_131(&self, ctx: &EvaluationContext) -> ConditionResult {
1511 ctx.lacks_qualifier("RFF", 0, "AGK")
1512 }
1513
1514 /// [132] wenn LOC+172 (Identifikationsangabe) DE3225 nicht vorhanden
1515 // REVIEW: Checks if LOC+172 is absent entirely, or if DE3225 (elements[1][0], the Bezeichnung) is empty/missing within the LOC+172 segment. Returns True in either case as the condition is 'DE3225 nicht vorhanden'. (medium confidence)
1516 fn evaluate_132(&self, ctx: &EvaluationContext) -> ConditionResult {
1517 let locs = ctx.find_segments_with_qualifier("LOC", 0, "172");
1518 match locs.first() {
1519 None => ConditionResult::True,
1520 Some(loc) => {
1521 let de3225_absent = loc
1522 .elements
1523 .get(1)
1524 .and_then(|e| e.first())
1525 .map(|v| v.is_empty())
1526 .unwrap_or(true);
1527 ConditionResult::from(de3225_absent)
1528 }
1529 }
1530 }
1531
1532 /// [134] Wenn SG10 DTM+9 DE2379 in demselben Segment mit Wert 102 vorhanden
1533 fn evaluate_134(&self, ctx: &EvaluationContext) -> ConditionResult {
1534 ConditionResult::from(ctx.self_segment_value_equals("DTM", 0, 2, "102"))
1535 }
1536
1537 /// [135] Der Wert an der Stelle CCYYMMDD muss ≤ dem Wert an der Stelle CCYYMMDD im DE2380 des DTM+137 sein
1538 // HAND-EDITED: subject is the current DTM's DE2380 (via ctx.resolved_value), not a
1539 // guessed DTM+9. Compares the CCYYMMDD prefix (first 8 chars) lexicographically,
1540 // which matches both format 303 (CCYYMMDDhhmm+ZZ) and 102 (CCYYMMDD) prefixes.
1541 fn evaluate_135(&self, ctx: &EvaluationContext) -> ConditionResult {
1542 let current = match ctx.resolved_value {
1543 Some(v) if v.len() >= 8 => &v[..8],
1544 _ => return ConditionResult::Unknown,
1545 };
1546 let threshold: String = match ctx
1547 .find_segments_with_qualifier("DTM", 0, "137")
1548 .first()
1549 .and_then(|s| s.elements.first())
1550 .and_then(|e| e.get(1))
1551 .filter(|v| v.len() >= 8)
1552 {
1553 Some(v) => v.chars().take(8).collect(),
1554 None => return ConditionResult::Unknown,
1555 };
1556 ConditionResult::from(current <= threshold.as_str())
1557 }
1558
1559 /// [142] wenn im DE3155 in demselben COM der Code EM vorhanden ist
1560 fn evaluate_142(&self, ctx: &EvaluationContext) -> ConditionResult {
1561 let coms = ctx.find_segments("COM");
1562 ConditionResult::from(coms.iter().any(|s| {
1563 s.elements
1564 .first()
1565 .and_then(|e| e.get(1))
1566 .is_some_and(|v| v == "EM")
1567 }))
1568 }
1569
1570 /// [143] wenn im DE3155 in demselben COM der Code TE / FX / AJ / AL vorhanden ist
1571 fn evaluate_143(&self, ctx: &EvaluationContext) -> ConditionResult {
1572 let coms = ctx.find_segments("COM");
1573 ConditionResult::from(coms.iter().any(|s| {
1574 s.elements
1575 .first()
1576 .and_then(|e| e.get(1))
1577 .is_some_and(|v| matches!(v.as_str(), "TE" | "FX" | "AJ" | "AL"))
1578 }))
1579 }
1580
1581 /// [144] Wenn Wert in SG7 RFF+AGK DE1154 (Konfigurations-ID) vorhanden
1582 fn evaluate_144(&self, ctx: &EvaluationContext) -> ConditionResult {
1583 let rffs = ctx.find_segments_with_qualifier("RFF", 0, "AGK");
1584 match rffs.first() {
1585 None => ConditionResult::False,
1586 Some(rff) => {
1587 let has_value = rff
1588 .elements
1589 .first()
1590 .and_then(|e| e.get(1))
1591 .map(|v| !v.is_empty())
1592 .unwrap_or(false);
1593 ConditionResult::from(has_value)
1594 }
1595 }
1596 }
1597
1598 /// [145] Wenn in derselben S9 LIN das SG10 DTM+163 (Beginn Messperiode) nicht vorhanden ist.
1599 // REVIEW: Checks absence of DTM+163 (Beginn Messperiode) in the message. The condition references the same SG10 context within the enclosing LIN group; without the exact MSCONS group path this is a message-wide approximation. Note: DTM+163 also appears in SG6 (Übertragungszeitraum), so this may yield false-negatives for the SG10-scoped intent. (medium confidence)
1600 fn evaluate_145(&self, ctx: &EvaluationContext) -> ConditionResult {
1601 ctx.lacks_qualifier("DTM", 0, "163")
1602 }
1603
1604 /// [146] Wenn es bei dem zu übermittelnden Wert um einen Wert zu einem Zeitpunkt handelt.
1605 // REVIEW: A point-in-time value (Wert zu einem Zeitpunkt) is signalled by the presence of DTM+7 (Nutzungszeitpunkt) in SG10. If this DTM qualifier is present, the transmitted value represents a single point in time rather than an interval. (medium confidence)
1606 fn evaluate_146(&self, ctx: &EvaluationContext) -> ConditionResult {
1607 ctx.has_qualifier("DTM", 0, "7")
1608 }
1609
1610 /// [147] Wenn in derselben S9 LIN das SG10 DTM+7 (Nutzungszeitpunkt) nicht vorhanden ist.
1611 // REVIEW: Checks absence of DTM+7 (Nutzungszeitpunkt) in the message, approximating the SG10-scoped check within the enclosing LIN group. Absence means the value is not a point-in-time value. (medium confidence)
1612 fn evaluate_147(&self, ctx: &EvaluationContext) -> ConditionResult {
1613 ctx.lacks_qualifier("DTM", 0, "7")
1614 }
1615
1616 /// [148] Wenn es bei dem zu übermittelnden Wert um einen Wert in einem Zeitintervall handelt.
1617 // REVIEW: An interval value (Wert in einem Zeitintervall) is signalled by the presence of DTM+163 (Beginn Messperiode) in SG10. If Beginn Messperiode is present, the value spans a time interval rather than being a single point. (medium confidence)
1618 fn evaluate_148(&self, ctx: &EvaluationContext) -> ConditionResult {
1619 ctx.has_qualifier("DTM", 0, "163")
1620 }
1621
1622 /// [149] Wenn in derselben S9 LIN das SG10 DTM+163 (Beginn Messperiode) vorhanden ist.
1623 // REVIEW: Checks presence of DTM+163 (Beginn Messperiode) — the direct inverse of condition 145. Message-wide approximation of the SG10-scoped intent within the enclosing LIN group. (medium confidence)
1624 fn evaluate_149(&self, ctx: &EvaluationContext) -> ConditionResult {
1625 ctx.has_qualifier("DTM", 0, "163")
1626 }
1627
1628 /// [150] Wenn BGM+Z69 (Redispatch tägliche Ausfallarbeitsüberführungszeitreihe) vorhanden.
1629 fn evaluate_150(&self, ctx: &EvaluationContext) -> ConditionResult {
1630 ctx.has_qualifier("BGM", 0, "Z69")
1631 }
1632
1633 /// [492] wenn MP-ID in NAD+MR aus Sparte Strom
1634 /// EXTERNAL: Requires context from outside the message.
1635 fn evaluate_492(&self, ctx: &EvaluationContext) -> ConditionResult {
1636 ctx.external.evaluate("recipient_is_electricity_sector")
1637 }
1638
1639 /// [493] wenn MP-ID in NAD+MR aus Sparte Gas
1640 /// EXTERNAL: Requires context from outside the message.
1641 fn evaluate_493(&self, ctx: &EvaluationContext) -> ConditionResult {
1642 ctx.external.evaluate("recipient_is_gas_sector")
1643 }
1644
1645 /// [501] Hinweis: Es sind nur die Werte erlaubt, die in der EDI@Energy Codeliste der OBIS-Kennzahlen und Medien mit dem entsprechenden Prüfidentifikator versehen sind.
1646 fn evaluate_501(&self, _ctx: &EvaluationContext) -> ConditionResult {
1647 // Hinweis: Informational note about allowed OBIS codes — always applies unconditionally
1648 ConditionResult::True
1649 }
1650
1651 /// [502] Hinweis: Einmal für die Energiemenge von Beginn des Kalenderjahres bis zum Lieferbeginn und bis zu zweimal für die zwei höchsten Monatsleistungswerte (wegen KAV) von Beginn des Kalenderjahres bi...
1652 fn evaluate_502(&self, _ctx: &EvaluationContext) -> ConditionResult {
1653 // Hinweis: Informational note about cardinality (once for energy from start of calendar year,
1654 // up to twice for two highest monthly peak values due to KAV) — always applies unconditionally
1655 ConditionResult::True
1656 }
1657
1658 /// [506] Hinweis: Nur bei Einspeisemengen und bei Gas zur stündlichen Energiedatenübermittlung
1659 fn evaluate_506(&self, _ctx: &EvaluationContext) -> ConditionResult {
1660 // Hinweis: Informational note — only for feed-in quantities and gas hourly energy data transmission
1661 // This is an informational annotation, not a boolean predicate — always applies
1662 ConditionResult::True
1663 }
1664
1665 /// [509] Hinweis: Falls es sich um eine Korrekturenergiemenge handelt, ist hier die Referenz auf die MSCONS anzugeben, in der der Zählerstand vorab übermittelt wurde.
1666 fn evaluate_509(&self, _ctx: &EvaluationContext) -> ConditionResult {
1667 // Hinweis: Informational note — for correction energy amounts, reference the prior MSCONS
1668 // in which the meter reading was transmitted. Informational annotation, always applies.
1669 ConditionResult::True
1670 }
1671
1672 /// [510] Hinweis: Verwendung der ID der Messlokation
1673 fn evaluate_510(&self, _ctx: &EvaluationContext) -> ConditionResult {
1674 // Hinweis: Verwendung der ID der Messlokation — informational note, always applies
1675 ConditionResult::True
1676 }
1677
1678 /// [511] Hinweis: Verwendung der ID des MaBiS-ZP
1679 fn evaluate_511(&self, _ctx: &EvaluationContext) -> ConditionResult {
1680 // Hinweis: Verwendung der ID des MaBiS-ZP — informational note, always applies
1681 ConditionResult::True
1682 }
1683
1684 /// [512] Hinweis: Verwendung der Bilanzkreisbezeichnung
1685 fn evaluate_512(&self, _ctx: &EvaluationContext) -> ConditionResult {
1686 // Hinweis: Verwendung der Bilanzkreisbezeichnung — informational note, always applies
1687 ConditionResult::True
1688 }
1689
1690 /// [513] Hinweis: Verwendung der Bezeichnung des Bilanzierungsgebietes
1691 fn evaluate_513(&self, _ctx: &EvaluationContext) -> ConditionResult {
1692 // Hinweis: Verwendung der Bezeichnung des Bilanzierungsgebietes — informational note, always applies
1693 ConditionResult::True
1694 }
1695
1696 /// [514] Hinweis: Verwendung der ID der Marktlokation
1697 fn evaluate_514(&self, _ctx: &EvaluationContext) -> ConditionResult {
1698 // Hinweis: Verwendung der ID der Marktlokation — informational note, always applies
1699 ConditionResult::True
1700 }
1701
1702 /// [515] Hinweis: Verwendung der Profilbezeichnung
1703 fn evaluate_515(&self, _ctx: &EvaluationContext) -> ConditionResult {
1704 // Hinweis: Verwendung der Profilbezeichnung — informational note, always applies
1705 ConditionResult::True
1706 }
1707
1708 /// [516] Hinweis: Verwendung der Bezeichnung der Profilschar
1709 fn evaluate_516(&self, _ctx: &EvaluationContext) -> ConditionResult {
1710 // Hinweis: Verwendung der Bezeichnung der Profilschar — informational note, always applies
1711 ConditionResult::True
1712 }
1713
1714 /// [518] Hinweis: Verwendung der ID der Tranche
1715 fn evaluate_518(&self, _ctx: &EvaluationContext) -> ConditionResult {
1716 // Hinweis: Verwendung der ID der Tranche — informational note, always applies
1717 ConditionResult::True
1718 }
1719
1720 /// [519] Hinweis: Nur wenn der gemessene Lastgang der Messlokation nicht dem Lastgang der Marktlokation 1:1 entspricht.
1721 fn evaluate_519(&self, _ctx: &EvaluationContext) -> ConditionResult {
1722 // Hinweis: Nur wenn der gemessene Lastgang der Messlokation nicht dem Lastgang der Marktlokation 1:1 entspricht — informational note, always applies
1723 ConditionResult::True
1724 }
1725
1726 /// [520] Hinweis: Wenn es sich um eine 1:1 Beziehung zwischen Messlokation und Marktlokation handelt und der gemessene Lastgang der Messlokation dem Lastgang der Marktlokation 1:1 entspricht, oder wenn der ...
1727 fn evaluate_520(&self, _ctx: &EvaluationContext) -> ConditionResult {
1728 // Hinweis: Wenn es sich um eine 1:1 Beziehung zwischen Messlokation und Marktlokation handelt... — informational note, always applies
1729 ConditionResult::True
1730 }
1731
1732 /// [522] Hinweis: Nur für die Übermittlung der Korrekturenergiemengen im Zeitintervall zwischen zwei Messwerten.
1733 fn evaluate_522(&self, _ctx: &EvaluationContext) -> ConditionResult {
1734 // Hinweis: Nur für die Übermittlung der Korrekturenergiemengen im Zeitintervall zwischen zwei Messwerten.
1735 ConditionResult::True
1736 }
1737
1738 /// [523] Hinweis: Nur für die Übermittlung der Energiemenge im Zeitintervall zwischen zwei Messwerten vor der Netznutzungsabrechnung.
1739 fn evaluate_523(&self, _ctx: &EvaluationContext) -> ConditionResult {
1740 // Hinweis: Nur für die Übermittlung der Energiemenge im Zeitintervall zwischen zwei Messwerten vor der Netznutzungsabrechnung.
1741 ConditionResult::True
1742 }
1743
1744 /// [524] Hinweis: Nur, wenn es sich um die Übermittlung von Abrechnungsbrennwert und Z-Zahl für den vom Lieferanten über eine Geschäftsdatenanfrage angeforderten Zeitraum handelt.
1745 fn evaluate_524(&self, _ctx: &EvaluationContext) -> ConditionResult {
1746 // Hinweis: Nur, wenn es sich um die Übermittlung von Abrechnungsbrennwert und Z-Zahl für den vom Lieferanten über eine Geschäftsdatenanfrage angeforderten Zeitraum handelt.
1747 ConditionResult::True
1748 }
1749
1750 /// [525] Hinweis: Nur für die Übermittlung der Energiemenge im Zeitintervall für eine Marktlokation ohne Messlokation (Pauschalanlage) wenn eines der Ereignisse aus Kapitel 4.2 eingetreten ist.
1751 fn evaluate_525(&self, _ctx: &EvaluationContext) -> ConditionResult {
1752 // Hinweis: Nur für die Übermittlung der Energiemenge im Zeitintervall für eine Marktlokation ohne Messlokation (Pauschalanlage) wenn eines der Ereignisse aus Kapitel 4.2 eingetreten ist.
1753 ConditionResult::True
1754 }
1755
1756 /// [526] Hinweis: Wert aus BGM+Z24 DE1004 der ORDERS mit der die Allokationsliste bestellt wurde.
1757 fn evaluate_526(&self, _ctx: &EvaluationContext) -> ConditionResult {
1758 // Hinweis: Wert aus BGM+Z24 DE1004 der ORDERS mit der die Allokationsliste bestellt wurde.
1759 ConditionResult::True
1760 }
1761
1762 /// [528] Hinweis: Wert aus BGM+Z28 DE1004 der ORDERS mit der die Anforderung von Messwerten erfolgt ist.
1763 fn evaluate_528(&self, _ctx: &EvaluationContext) -> ConditionResult {
1764 // Hinweis: Wert aus BGM+Z28 DE1004 der ORDERS mit der die Anforderung von Messwerten erfolgt ist.
1765 // Informational note, always applies
1766 ConditionResult::True
1767 }
1768
1769 /// [529] Hinweis: Wert aus BGM+7 DE1004 der ORDERS mit der die Anforderung von Messwerten erfolgt ist.
1770 fn evaluate_529(&self, _ctx: &EvaluationContext) -> ConditionResult {
1771 // Hinweis: Wert aus BGM+7 DE1004 der ORDERS mit der die Anforderung von Messwerten erfolgt ist.
1772 // Informational note, always applies
1773 ConditionResult::True
1774 }
1775
1776 /// [530] Hinweis: Wert aus SG4 IDE+24 DE7402 der UTILMD mit dem der Sender der MSCONS die vorherigen Stammdaten mittels UTILMD übermittelt hat.
1777 fn evaluate_530(&self, _ctx: &EvaluationContext) -> ConditionResult {
1778 // Hinweis: Wert aus SG4 IDE+24 DE7402 der UTILMD mit dem der Sender der MSCONS die vorherigen Stammdaten mittels UTILMD übermittelt hat.
1779 // Informational note, always applies
1780 ConditionResult::True
1781 }
1782
1783 /// [531] Hinweis: Wert aus BGM+7 DE1004 der MSCONS mit der der Zählerstand übermittelt wurde.
1784 fn evaluate_531(&self, _ctx: &EvaluationContext) -> ConditionResult {
1785 // Hinweis: Wert aus BGM+7 DE1004 der MSCONS mit der der Zählerstand übermittelt wurde.
1786 // Informational note, always applies
1787 ConditionResult::True
1788 }
1789
1790 /// [532] Hinweis: Wert aus BGM+7/Z27/Z28/270/Z41/Z42/Z85 DE1004 der MSCONS Nachricht, die storniert wird
1791 fn evaluate_532(&self, _ctx: &EvaluationContext) -> ConditionResult {
1792 // Hinweis: Wert aus BGM+7/Z27/Z28/270/Z41/Z42/Z85 DE1004 der MSCONS Nachricht, die storniert wird
1793 // Informational note, always applies
1794 ConditionResult::True
1795 }
1796
1797 /// [535] Hinweis: Verwendung der ID des Netzkoppelpunktes Strom/Gas
1798 fn evaluate_535(&self, _ctx: &EvaluationContext) -> ConditionResult {
1799 // Hinweis: Verwendung der ID des Netzkoppelpunktes Strom/Gas — informational note, always applies
1800 ConditionResult::True
1801 }
1802
1803 /// [538] Hinweis: Die Referenz auf die ORDERS ist nur dann anzugeben, wenn diese Werte vom Empfänger auch ursprünglich mittels ORDERS angefragt wurden.
1804 fn evaluate_538(&self, _ctx: &EvaluationContext) -> ConditionResult {
1805 // Hinweis: Die Referenz auf die ORDERS ist nur dann anzugeben, wenn diese Werte vom Empfänger auch ursprünglich mittels ORDERS angefragt wurden — informational note, always applies
1806 ConditionResult::True
1807 }
1808
1809 /// [541] Hinweis: Ein Korrekturgrund ist anzugeben, wenn: 1. ein bereits an den MP übermittelter vorläufiger Wert nach Stornierung durch einen Ersatzwert ersetzt wird, oder 2. ein bereits an den MP über...
1810 fn evaluate_541(&self, _ctx: &EvaluationContext) -> ConditionResult {
1811 // Hinweis: Ein Korrekturgrund ist anzugeben in bestimmten Ersatzwert/Stornierung-Szenarien — informational note, always applies
1812 ConditionResult::True
1813 }
1814
1815 /// [543] Hinweis: Wert aus BGM+Z23 DE1004 der ORDERS mit der die bilanzierte Menge bestellt wurde.
1816 fn evaluate_543(&self, _ctx: &EvaluationContext) -> ConditionResult {
1817 // Hinweis: Wert aus BGM+Z23 DE1004 der ORDERS mit der die bilanzierte Menge bestellt wurde — informational note, always applies
1818 ConditionResult::True
1819 }
1820
1821 /// [544] Hinweis: Bei einer Mengenaufteilung (z. B. Aufgrund einer Abgrenzung) für SG6 LOC+172 muss für den frühesten angegebenen Zeitpunkt zum Beginn des Zeitintervalls (über alle Wiederholungen der L...
1822 fn evaluate_544(&self, _ctx: &EvaluationContext) -> ConditionResult {
1823 // Hinweis: Bei einer Mengenaufteilung für SG6 LOC+172 muss für den frühesten Zeitpunkt zu jeder OBIS-Kennziffer ein Zählerstand vorhanden sein — informational note, always applies
1824 ConditionResult::True
1825 }
1826
1827 /// [545] Hinweis: Bei einer Mengenaufteilung (z. B. Aufgrund einer Abgrenzung) für SG6 LOC+172 muss für den spätesten angegebenen Zeitpunkt zum Ende des Zeitintervalls (über alle Wiederholungen der LIN-...
1828 fn evaluate_545(&self, _ctx: &EvaluationContext) -> ConditionResult {
1829 // Hinweis: informational note about Mengenaufteilung and Zählerstand requirement — always applies
1830 ConditionResult::True
1831 }
1832
1833 /// [546] Hinweis: Eine Referenz auf die Stammdatenänderung des Gerätewechsels ist immer anzugeben, wenn diese dem Sender vorliegt.
1834 fn evaluate_546(&self, _ctx: &EvaluationContext) -> ConditionResult {
1835 // Hinweis: informational note about referencing master data changes for device replacements — always applies
1836 ConditionResult::True
1837 }
1838
1839 /// [547] Hinweis: Der Code 270 ist nur zu nutzen, wenn ein Lieferschein, der vor dem 1.4.2021 erstellt wurde, storniert wird.
1840 fn evaluate_547(&self, _ctx: &EvaluationContext) -> ConditionResult {
1841 // Hinweis: informational note — code 270 only for cancelling delivery notes created before 1.4.2021 — always applies
1842 ConditionResult::True
1843 }
1844
1845 /// [551] Hinweis: Ein Korrekturgrund ist anzugeben, wenn: 1. ein bereits an den MP übermittelter vorläufiger Wert durch einen Ersatzwert ersetzt wird, oder 2. ein bereits an den MP übermittelter Ersatzw...
1846 fn evaluate_551(&self, _ctx: &EvaluationContext) -> ConditionResult {
1847 // Hinweis: informational note describing when a Korrekturgrund must be provided — always applies
1848 ConditionResult::True
1849 }
1850
1851 /// [553] Hinweis: Wert aus BGM+Z34 DE1004 der ORDERS mit der die Reklamation von Werten erfolgt ist
1852 fn evaluate_553(&self, _ctx: &EvaluationContext) -> ConditionResult {
1853 // Hinweis: informational note — value from BGM+Z34 DE1004 of the ORDERS used for value complaint — always applies
1854 ConditionResult::True
1855 }
1856
1857 /// [554] Hinweis: Verwendung der ID der Technischen Ressource
1858 fn evaluate_554(&self, _ctx: &EvaluationContext) -> ConditionResult {
1859 // Hinweis: Verwendung der ID der Technischen Ressource — informational note, always applies
1860 ConditionResult::True
1861 }
1862
1863 /// [556] Hinweis: Wert aus BGM+Z45 DE1004 der ORDERS mit der die Anforderung der Ausfallarbeit durch den anfNB erfolgt ist.
1864 fn evaluate_556(&self, _ctx: &EvaluationContext) -> ConditionResult {
1865 // Hinweis: Wert aus BGM+Z45 DE1004 der ORDERS — informational note, always applies
1866 ConditionResult::True
1867 }
1868
1869 /// [557] Hinweis: Die Referenz auf die ursprüngliche MSCONS ist anzugeben, wenn es sich um die Übermittlung eines Gegenvorschlags durch den BTR handelt.
1870 fn evaluate_557(&self, _ctx: &EvaluationContext) -> ConditionResult {
1871 // Hinweis: Referenz auf ursprüngliche MSCONS bei Gegenvorschlag — informational note, always applies
1872 ConditionResult::True
1873 }
1874
1875 /// [558] Hinweis: Wert aus BGM+Z45 DE1004 der MSCONS auf die sich die Übermittlung des Gegenvorschlags durch den BTR bezieht.
1876 fn evaluate_558(&self, _ctx: &EvaluationContext) -> ConditionResult {
1877 // Hinweis: Wert aus BGM+Z45 DE1004 der MSCONS bei Gegenvorschlag — informational note, always applies
1878 ConditionResult::True
1879 }
1880
1881 /// [559] Hinweis: Ein Korrekturgrund ist anzugeben, wenn: 1. ein bereits an den MP übermittelter vorläufiger Wert nach Stornierung durch einen Ersatzwert ersetzt wird, oder 2. ein bereits an den MP über...
1882 fn evaluate_559(&self, _ctx: &EvaluationContext) -> ConditionResult {
1883 // Hinweis: Korrekturgrund bei Ersatz/Stornierung von Werten — informational note, always applies
1884 ConditionResult::True
1885 }
1886
1887 /// [560] Hinweis: Ein Korrekturgrund ist anzugeben, wenn: 1. ein bereits an den MP übermittelter vorläufiger Wert durch einen Ersatzwert ersetzt wird, oder 2. ein bereits an den MP übermittelter Ersatzw...
1888 fn evaluate_560(&self, _ctx: &EvaluationContext) -> ConditionResult {
1889 // Hinweis: Ein Korrekturgrund ist anzugeben, wenn ein vorläufiger/Ersatz/wahrer Wert ersetzt wird — informational note, always applies
1890 ConditionResult::True
1891 }
1892
1893 /// [566] Hinweis: Es sind nur die Werte erlaubt, die im vorherigen Stammdatenaustausch zu diesem Meldepunkt vom MSB zum Zeitpunkt übermittelt wurden.
1894 fn evaluate_566(&self, _ctx: &EvaluationContext) -> ConditionResult {
1895 // Hinweis: Es sind nur die Werte erlaubt, die im vorherigen Stammdatenaustausch übermittelt wurden — informational note, always applies
1896 ConditionResult::True
1897 }
1898
1899 /// [567] Hinweis: Es ist die Konfigurations-ID anzugeben, die im vorherigen Stammdatenaustausch kommuniziert wurde.
1900 fn evaluate_567(&self, _ctx: &EvaluationContext) -> ConditionResult {
1901 // Hinweis: Es ist die Konfigurations-ID anzugeben, die im vorherigen Stammdatenaustausch kommuniziert wurde — informational note, always applies
1902 ConditionResult::True
1903 }
1904
1905 /// [568] Hinweis: Verwendung ist nur zulässig, wenn es sich um 1:n Beziehung zwischen Markt- und Messlokation handelt und auf Ebene der Messlokation unterschiedliche Ersatzwertbildungsverfahren verwendet u...
1906 fn evaluate_568(&self, _ctx: &EvaluationContext) -> ConditionResult {
1907 // Hinweis: Verwendung nur zulässig bei 1:n Beziehung zwischen Markt- und Messlokation mit unterschiedlichen Ersatzwertbildungsverfahren — informational note, always applies
1908 ConditionResult::True
1909 }
1910
1911 /// [569] Hinweis: Bei mehreren Zählerständen einer Messlokation (z. B. HT/NT) ist diese Zeitangabe zu nutzen und eine Wiederholung das SG9 LIN durchzuführen.
1912 fn evaluate_569(&self, _ctx: &EvaluationContext) -> ConditionResult {
1913 // Hinweis: Bei mehreren Zählerständen einer Messlokation (z. B. HT/NT) ist diese Zeitangabe zu nutzen und SG9 LIN zu wiederholen — informational note, always applies
1914 ConditionResult::True
1915 }
1916
1917 /// [570] Hinweis: Verwendung ist nur zulässig, wenn es sich um 1:n Beziehung zwischen Markt- und Messlokation handelt und auf Ebene der Messlokation unterschiedliche Gründe für die Ersatzwertbildung vorl...
1918 fn evaluate_570(&self, _ctx: &EvaluationContext) -> ConditionResult {
1919 // Hinweis: Verwendung ist nur zulässig, wenn es sich um 1:n Beziehung zwischen Markt- und Messlokation handelt und auf Ebene der Messlokation unterschiedliche Gründe für die Ersatzwertbildung vorliegen und kommuniziert wurden.
1920 ConditionResult::True
1921 }
1922
1923 /// [571] Hinweis: Verwendung ist nur zulässig, wenn es sich um 1:n Beziehung handelt und auf Ebene der Netzkopplungspunkte unterschiedliche Gründe für die Ersatzwertbildung vorliegen und kommuniziert wur...
1924 fn evaluate_571(&self, _ctx: &EvaluationContext) -> ConditionResult {
1925 // Hinweis: Verwendung ist nur zulässig, wenn es sich um 1:n Beziehung handelt und auf Ebene der Netzkopplungspunkte unterschiedliche Gründe für die Ersatzwertbildung vorliegen und kommuniziert wurden.
1926 ConditionResult::True
1927 }
1928
1929 /// [572] Hinweis: Verwendung ist nur zulässig, wenn es sich um 1:n Beziehung handelt und auf Ebene der Netzkopplungspunkte unterschiedliche Ersatzwertbildungsverfahren vorliegen und kommuniziert wurden.
1930 fn evaluate_572(&self, _ctx: &EvaluationContext) -> ConditionResult {
1931 // Hinweis: Verwendung ist nur zulässig, wenn es sich um 1:n Beziehung handelt und auf Ebene der Netzkopplungspunkte unterschiedliche Ersatzwertbildungsverfahren vorliegen und kommuniziert wurden.
1932 ConditionResult::True
1933 }
1934
1935 /// [573] Hinweis: Eine Energiemenge in der Sparte Gas ist gemäß DVGW G685 Arbeitsblatt 4 Kapitel 5.3 auf ganze Kilowattstunden zu runden.
1936 fn evaluate_573(&self, _ctx: &EvaluationContext) -> ConditionResult {
1937 // Hinweis: Eine Energiemenge in der Sparte Gas ist gemäß DVGW G685 Arbeitsblatt 4 Kapitel 5.3 auf ganze Kilowattstunden zu runden.
1938 ConditionResult::True
1939 }
1940
1941 /// [574] Hinweis: Wert aus BGM DE1004 der ORDERS mit der die Bestellung der Werte nach Typ 2 erfolg ist
1942 fn evaluate_574(&self, _ctx: &EvaluationContext) -> ConditionResult {
1943 // Hinweis: Wert aus BGM DE1004 der ORDERS mit der die Bestellung der Werte nach Typ 2 erfolg ist
1944 ConditionResult::True
1945 }
1946
1947 /// [575] Hinweis: Verwendung der ID der Netzlokation
1948 fn evaluate_575(&self, _ctx: &EvaluationContext) -> ConditionResult {
1949 // Hinweis: Verwendung der ID der Netzlokation — informational note, always applies
1950 ConditionResult::True
1951 }
1952
1953 /// [576] Hinweis: Es darf nur eine Information im DE3148 übermittelt werden
1954 fn evaluate_576(&self, _ctx: &EvaluationContext) -> ConditionResult {
1955 // Hinweis: Es darf nur eine Information im DE3148 übermittelt werden — informational note, always applies
1956 ConditionResult::True
1957 }
1958
1959 /// [577] Hinweis: Dieser Code ist auch zu verwenden, wenn aufgrund der Beendigung einer Messlokation (Stilllegung) die Beendigung der Marktlokation (Stilllegung) zu unterschiedlichen Zeitpunkten erfolgt, da...
1960 fn evaluate_577(&self, _ctx: &EvaluationContext) -> ConditionResult {
1961 // Hinweis: Informational note about Messlokation/Marktlokation termination timing — always applies
1962 ConditionResult::True
1963 }
1964
1965 /// [902] Format: Möglicher Wert: ≥ 0
1966 fn evaluate_902(&self, ctx: &EvaluationContext) -> ConditionResult {
1967 ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, ">=", 0.0))
1968 }
1969
1970 /// [904] Format: genau 16 Stellen
1971 // REVIEW: Format condition requiring exactly 16 characters. Applied to QTY value (most common numeric format condition target in MSCONS). Confidence is medium because the exact target segment/element is not specified in the condition text — QTY is the most likely candidate in MSCONS context. (medium confidence)
1972 fn evaluate_904(&self, ctx: &EvaluationContext) -> ConditionResult {
1973 ctx.format_check("QTY", 0, 1, |val| validate_exact_length(val, 16))
1974 }
1975
1976 /// [905] Format: max. 3 Stellen
1977 fn evaluate_905(&self, ctx: &EvaluationContext) -> ConditionResult {
1978 ctx.format_check("QTY", 0, 1, |val| validate_max_integer_digits(val, 3))
1979 }
1980
1981 /// [906] Format: max. 3 Nachkommastellen
1982 fn evaluate_906(&self, ctx: &EvaluationContext) -> ConditionResult {
1983 ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 3))
1984 }
1985
1986 /// [907] Format: max. 4 Nachkommastellen
1987 fn evaluate_907(&self, ctx: &EvaluationContext) -> ConditionResult {
1988 ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 4))
1989 }
1990
1991 /// [908] Format: Mögliche Werte: 1 bis n
1992 fn evaluate_908(&self, ctx: &EvaluationContext) -> ConditionResult {
1993 ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, ">=", 1.0))
1994 }
1995
1996 /// [909] Format: Mögliche Werte: 0 bis n
1997 fn evaluate_909(&self, ctx: &EvaluationContext) -> ConditionResult {
1998 ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, ">=", 0.0))
1999 }
2000
2001 /// [910] Format: Möglicher Wert: < 0 oder ≥ 0
2002 fn evaluate_910(&self, ctx: &EvaluationContext) -> ConditionResult {
2003 let qty_segments = ctx.find_segments("QTY");
2004 let val = qty_segments
2005 .first()
2006 .and_then(|s| s.elements.first())
2007 .and_then(|e| e.get(1))
2008 .map(|s| s.as_str())
2009 .unwrap_or("");
2010 if val.is_empty() {
2011 return ConditionResult::Unknown;
2012 }
2013 match val.parse::<f64>() {
2014 Ok(_) => ConditionResult::True,
2015 Err(_) => ConditionResult::False,
2016 }
2017 }
2018
2019 /// [917] Format: max. 4 Vorkommastellen
2020 fn evaluate_917(&self, ctx: &EvaluationContext) -> ConditionResult {
2021 ctx.format_check("QTY", 0, 1, |val| validate_max_integer_digits(val, 4))
2022 }
2023
2024 /// [918] Format: Zeichen aus dem über UNOC definierten Zeichensatz, wobei von den Buchstaben nur Großbuchstaben erlaubt sind.
2025 // REVIEW: UNOC charset requires uppercase-only alphabetics. Checks all alphabetic characters in FTX text fields (C108, elements[3], components 0..4) are uppercase. Non-alphabetic characters (digits, punctuation) are permitted. Medium confidence — UNOC validation approximated as uppercase-only alphabetic check without full ISO 8859-1 codepoint enumeration. (medium confidence)
2026 fn evaluate_918(&self, ctx: &EvaluationContext) -> ConditionResult {
2027 let ftx_segments = ctx.find_segments("FTX");
2028 for seg in &ftx_segments {
2029 if let Some(c108) = seg.elements.get(3) {
2030 for component in c108 {
2031 let s = component.as_str();
2032 if s.is_empty() {
2033 continue;
2034 }
2035 if !s.chars().all(|c| !c.is_alphabetic() || c.is_uppercase()) {
2036 return ConditionResult::False;
2037 }
2038 }
2039 }
2040 }
2041 ConditionResult::True
2042 }
2043
2044 /// [925] Format: max. 5 Nachkommastellen
2045 fn evaluate_925(&self, ctx: &EvaluationContext) -> ConditionResult {
2046 ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 5))
2047 }
2048
2049 /// [931] Format: ZZZ = +00
2050 fn evaluate_931(&self, ctx: &EvaluationContext) -> ConditionResult {
2051 ctx.format_check("DTM", 0, 1, validate_timezone_utc)
2052 }
2053
2054 /// [932] Format: HHMM = 2200
2055 fn evaluate_932(&self, ctx: &EvaluationContext) -> ConditionResult {
2056 ctx.format_check("DTM", 0, 1, |val| validate_hhmm_equals(val, "2200"))
2057 }
2058
2059 /// [933] Format: HHMM = 2300
2060 fn evaluate_933(&self, ctx: &EvaluationContext) -> ConditionResult {
2061 ctx.format_check("DTM", 0, 1, |val| validate_hhmm_equals(val, "2300"))
2062 }
2063
2064 /// [934] Format: HHMM = 0400
2065 fn evaluate_934(&self, ctx: &EvaluationContext) -> ConditionResult {
2066 ctx.format_check("DTM", 0, 1, |val| validate_hhmm_equals(val, "0400"))
2067 }
2068
2069 /// [935] Format: HHMM = 0500
2070 fn evaluate_935(&self, ctx: &EvaluationContext) -> ConditionResult {
2071 ctx.format_check("DTM", 0, 1, |val| validate_hhmm_equals(val, "0500"))
2072 }
2073
2074 /// [937] Format: keine Nachkommastelle
2075 fn evaluate_937(&self, ctx: &EvaluationContext) -> ConditionResult {
2076 ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 0))
2077 }
2078
2079 /// [939] Format: Die Zeichenkette muss die Zeichen @ und . enthalten
2080 fn evaluate_939(&self, ctx: &EvaluationContext) -> ConditionResult {
2081 ctx.format_check("COM", 0, 0, validate_email)
2082 }
2083
2084 /// [940] Format: Die Zeichenkette muss mit dem Zeichen + beginnen und danach dürfen nur noch Ziffern folgen
2085 fn evaluate_940(&self, ctx: &EvaluationContext) -> ConditionResult {
2086 ctx.format_check("COM", 0, 0, validate_phone)
2087 }
2088
2089 /// [950] Format: Marktlokations-ID
2090 fn evaluate_950(&self, ctx: &EvaluationContext) -> ConditionResult {
2091 ctx.format_check_qualified("LOC", 0, "Z16", 1, 0, validate_malo_id)
2092 }
2093
2094 /// [951] Format: Zählpunktbezeichnung
2095 // REVIEW: Format: Zählpunktbezeichnung — 33 alphanumeric characters. In MSCONS, Zählpunktbezeichnung typically appears in LOC+Z19. Using validate_zahlpunkt helper. (medium confidence)
2096 fn evaluate_951(&self, ctx: &EvaluationContext) -> ConditionResult {
2097 ctx.format_check_qualified("LOC", 0, "Z19", 1, 0, validate_zahlpunkt)
2098 }
2099
2100 /// [960] Format: Netzlokations-ID
2101 // HAND-EDITED: a Netzlokations-ID is "E" + ten alphanumerics (`E1688117482`),
2102 // not MaLo-shaped; `validate_malo_id` rejected every real one (#173).
2103 fn evaluate_960(&self, ctx: &EvaluationContext) -> ConditionResult {
2104 ctx.format_check_qualified("LOC", 0, "Z17", 1, 0, validate_nelo_id)
2105 }
2106
2107 /// [2001] Segmentgruppe ist nur einmal je UNH anzugeben
2108 fn evaluate_2001(&self, _ctx: &EvaluationContext) -> ConditionResult {
2109 // Hinweis: Segmentgruppe ist nur einmal je UNH anzugeben — cardinality annotation, always applies
2110 ConditionResult::True
2111 }
2112
2113 /// [2002] Segmentgruppe ist bis zu drei Mal je SG5 NAD+DP anzugeben
2114 fn evaluate_2002(&self, _ctx: &EvaluationContext) -> ConditionResult {
2115 // Hinweis: Segmentgruppe ist bis zu drei Mal je SG5 NAD+DP anzugeben — cardinality annotation, always applies
2116 ConditionResult::True
2117 }
2118
2119 /// [2003] Segmentgruppe ist genau zwei Mal je SG9 LIN anzugeben
2120 fn evaluate_2003(&self, _ctx: &EvaluationContext) -> ConditionResult {
2121 // Hinweis: Segmentgruppe ist genau zwei Mal je SG9 LIN anzugeben — cardinality annotation, always applies
2122 ConditionResult::True
2123 }
2124}