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