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