automapper_validation/generated/fv2504/utilts_conditions_fv2504.rs
1// <auto-generated>
2// Generated by automapper-generator generate-conditions
3// AHB: xml-migs-and-ahbs/FV2504/UTILTS_AHB_1_0_Fehlerkorrektur_20250218.xml
4// Generated: 2026-03-12T10:04:46Z
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::{Cluster, ConditionEvaluator, ConditionResult, EvaluationContext};
15// HAND-EDITED: shared cluster helper for condition [61].
16// Preserve across regeneration — see CLAUDE.md "Codelist Providers".
17use crate::eval::ebd_cluster::any_sts_e01_in_cluster;
18
19/// Generated condition evaluator for UTILTS FV2504.
20pub struct UtiltsConditionEvaluatorFV2504 {
21 // External condition IDs that require runtime context.
22 external_conditions: std::collections::HashSet<u32>,
23}
24
25impl Default for UtiltsConditionEvaluatorFV2504 {
26 fn default() -> Self {
27 let mut external_conditions = std::collections::HashSet::new();
28 external_conditions.insert(1);
29 external_conditions.insert(22);
30 external_conditions.insert(25);
31 external_conditions.insert(26);
32 external_conditions.insert(37);
33 // HAND-EDITED: [61] is no longer external — EBD cluster lookup handles it.
34 // external_conditions.insert(61);
35 external_conditions.insert(62);
36 external_conditions.insert(494);
37 Self {
38 external_conditions,
39 }
40 }
41}
42
43impl ConditionEvaluator for UtiltsConditionEvaluatorFV2504 {
44 fn message_type(&self) -> &str {
45 "UTILTS"
46 }
47
48 fn format_version(&self) -> &str {
49 "FV2504"
50 }
51
52 fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
53 match condition {
54 1 => self.evaluate_1(ctx),
55 2 => self.evaluate_2(ctx),
56 5 => self.evaluate_5(ctx),
57 6 => self.evaluate_6(ctx),
58 7 => self.evaluate_7(ctx),
59 8 => self.evaluate_8(ctx),
60 9 => self.evaluate_9(ctx),
61 10 => self.evaluate_10(ctx),
62 11 => self.evaluate_11(ctx),
63 12 => self.evaluate_12(ctx),
64 13 => self.evaluate_13(ctx),
65 14 => self.evaluate_14(ctx),
66 15 => self.evaluate_15(ctx),
67 21 => self.evaluate_21(ctx),
68 22 => self.evaluate_22(ctx),
69 24 => self.evaluate_24(ctx),
70 25 => self.evaluate_25(ctx),
71 26 => self.evaluate_26(ctx),
72 27 => self.evaluate_27(ctx),
73 29 => self.evaluate_29(ctx),
74 30 => self.evaluate_30(ctx),
75 31 => self.evaluate_31(ctx),
76 32 => self.evaluate_32(ctx),
77 33 => self.evaluate_33(ctx),
78 34 => self.evaluate_34(ctx),
79 36 => self.evaluate_36(ctx),
80 37 => self.evaluate_37(ctx),
81 41 => self.evaluate_41(ctx),
82 42 => self.evaluate_42(ctx),
83 43 => self.evaluate_43(ctx),
84 44 => self.evaluate_44(ctx),
85 46 => self.evaluate_46(ctx),
86 47 => self.evaluate_47(ctx),
87 48 => self.evaluate_48(ctx),
88 49 => self.evaluate_49(ctx),
89 50 => self.evaluate_50(ctx),
90 53 => self.evaluate_53(ctx),
91 54 => self.evaluate_54(ctx),
92 55 => self.evaluate_55(ctx),
93 56 => self.evaluate_56(ctx),
94 57 => self.evaluate_57(ctx),
95 58 => self.evaluate_58(ctx),
96 59 => self.evaluate_59(ctx),
97 61 => self.evaluate_61(ctx),
98 62 => self.evaluate_62(ctx),
99 490 => self.evaluate_490(ctx),
100 491 => self.evaluate_491(ctx),
101 494 => self.evaluate_494(ctx),
102 501 => self.evaluate_501(ctx),
103 502 => self.evaluate_502(ctx),
104 504 => self.evaluate_504(ctx),
105 505 => self.evaluate_505(ctx),
106 506 => self.evaluate_506(ctx),
107 507 => self.evaluate_507(ctx),
108 508 => self.evaluate_508(ctx),
109 509 => self.evaluate_509(ctx),
110 510 => self.evaluate_510(ctx),
111 511 => self.evaluate_511(ctx),
112 512 => self.evaluate_512(ctx),
113 513 => self.evaluate_513(ctx),
114 514 => self.evaluate_514(ctx),
115 515 => self.evaluate_515(ctx),
116 516 => self.evaluate_516(ctx),
117 517 => self.evaluate_517(ctx),
118 518 => self.evaluate_518(ctx),
119 519 => self.evaluate_519(ctx),
120 520 => self.evaluate_520(ctx),
121 521 => self.evaluate_521(ctx),
122 522 => self.evaluate_522(ctx),
123 523 => self.evaluate_523(ctx),
124 524 => self.evaluate_524(ctx),
125 525 => self.evaluate_525(ctx),
126 526 => self.evaluate_526(ctx),
127 527 => self.evaluate_527(ctx),
128 528 => self.evaluate_528(ctx),
129 529 => self.evaluate_529(ctx),
130 530 => self.evaluate_530(ctx),
131 531 => self.evaluate_531(ctx),
132 532 => self.evaluate_532(ctx),
133 533 => self.evaluate_533(ctx),
134 534 => self.evaluate_534(ctx),
135 912 => self.evaluate_912(ctx),
136 913 => self.evaluate_913(ctx),
137 914 => self.evaluate_914(ctx),
138 915 => self.evaluate_915(ctx),
139 930 => self.evaluate_930(ctx),
140 931 => self.evaluate_931(ctx),
141 932 => self.evaluate_932(ctx),
142 933 => self.evaluate_933(ctx),
143 937 => self.evaluate_937(ctx),
144 939 => self.evaluate_939(ctx),
145 940 => self.evaluate_940(ctx),
146 947 => self.evaluate_947(ctx),
147 950 => self.evaluate_950(ctx),
148 951 => self.evaluate_951(ctx),
149 960 => self.evaluate_960(ctx),
150 963 => self.evaluate_963(ctx),
151 964 => self.evaluate_964(ctx),
152 965 => self.evaluate_965(ctx),
153 969 => self.evaluate_969(ctx),
154 2001 => self.evaluate_2001(ctx),
155 2002 => self.evaluate_2002(ctx),
156 2004 => self.evaluate_2004(ctx),
157 2005 => self.evaluate_2005(ctx),
158 2006 => self.evaluate_2006(ctx),
159 2007 => self.evaluate_2007(ctx),
160 _ => ConditionResult::Unknown,
161 }
162 }
163
164 fn is_external(&self, condition: u32) -> bool {
165 self.external_conditions.contains(&condition)
166 }
167 fn is_known(&self, condition: u32) -> bool {
168 matches!(
169 condition,
170 1 | 2
171 | 5
172 | 6
173 | 7
174 | 8
175 | 9
176 | 10
177 | 11
178 | 12
179 | 13
180 | 14
181 | 15
182 | 21
183 | 22
184 | 24
185 | 25
186 | 26
187 | 27
188 | 29
189 | 30
190 | 31
191 | 32
192 | 33
193 | 34
194 | 36
195 | 37
196 | 41
197 | 42
198 | 43
199 | 44
200 | 46
201 | 47
202 | 48
203 | 49
204 | 50
205 | 53
206 | 54
207 | 55
208 | 56
209 | 57
210 | 58
211 | 59
212 | 61
213 | 62
214 | 490
215 | 491
216 | 494
217 | 501
218 | 502
219 | 504
220 | 505
221 | 506
222 | 507
223 | 508
224 | 509
225 | 510
226 | 511
227 | 512
228 | 513
229 | 514
230 | 515
231 | 516
232 | 517
233 | 518
234 | 519
235 | 520
236 | 521
237 | 522
238 | 523
239 | 524
240 | 525
241 | 526
242 | 527
243 | 528
244 | 529
245 | 530
246 | 531
247 | 532
248 | 533
249 | 534
250 | 912
251 | 913
252 | 914
253 | 915
254 | 930
255 | 931
256 | 932
257 | 933
258 | 937
259 | 939
260 | 940
261 | 947
262 | 950
263 | 951
264 | 960
265 | 963
266 | 964
267 | 965
268 | 969
269 | 2001
270 | 2002
271 | 2004
272 | 2005
273 | 2006
274 | 2007
275 )
276 }
277}
278
279impl UtiltsConditionEvaluatorFV2504 {
280 /// [8] Rechenschrittidentifikator aus einem SG8 SEQ+Z37 (Bestandteil des Rechenschritts) DE1050 desselben SG5 IDE+24 und derselben Zeitraum-ID wie bei diesem SG8
281 // REVIEW: Validates that RFF+Z23 references in SG8 SEQ+Z37 instances point to existing SEQ+Z37 DE1050 values. Follows the Example 27 pattern with SG5/SG8 group path. The Zeitraum-ID same-scope constraint and same-SG5 IDE+24 scoping are not fully enforced — cross-SG5 step IDs could be accepted — but within a single-transaction UTILTS message this is typically equivalent. RFF+Z23 is the standard Rechenschrittidentifikator reference qualifier based on Example 27. (medium confidence)
282 fn evaluate_8(&self, ctx: &EvaluationContext) -> ConditionResult {
283 let nav = match ctx.navigator() {
284 Some(n) => n,
285 None => return ConditionResult::Unknown,
286 };
287 let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
288 let mut valid_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
289 for i in 0..sg8_count {
290 let seq_segs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
291 for seq in &seq_segs {
292 if seq
293 .elements
294 .first()
295 .and_then(|e| e.first())
296 .is_some_and(|v| v == "Z37")
297 {
298 if let Some(val) = seq.elements.get(1).and_then(|e| e.first()) {
299 if !val.is_empty() {
300 valid_ids.insert(val.clone());
301 }
302 }
303 }
304 }
305 }
306 if valid_ids.is_empty() {
307 return ConditionResult::Unknown;
308 }
309 for i in 0..sg8_count {
310 let rff_segs = nav.find_segments_in_group("RFF", &["SG5", "SG8"], i);
311 for rff in &rff_segs {
312 if rff
313 .elements
314 .first()
315 .and_then(|e| e.first())
316 .is_some_and(|v| v == "Z23")
317 {
318 if let Some(ref_val) = rff.elements.first().and_then(|e| e.get(1)) {
319 if !ref_val.is_empty() && !valid_ids.contains(ref_val) {
320 return ConditionResult::False;
321 }
322 }
323 }
324 }
325 }
326 ConditionResult::True
327 }
328
329 /// [9] Der hier angegebene Rechenschrittidentifikator darf nicht identisch mit dem Rechenschrittidentifikator aus diesem SG8 SEQ+Z37 DE1050 sein
330 // REVIEW: Checks that within each SG8 SEQ+Z37 instance, the RFF+Z23 reference value does not equal the current SG8's own SEQ DE1050 Rechenschrittidentifikator (no self-reference). Iterates all SG8 instances globally and returns False on first self-referential cycle detected. (medium confidence)
331 fn evaluate_9(&self, ctx: &EvaluationContext) -> ConditionResult {
332 let nav = match ctx.navigator() {
333 Some(n) => n,
334 None => return ConditionResult::Unknown,
335 };
336 let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
337 for i in 0..sg8_count {
338 let seq_segs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
339 let own_id = seq_segs
340 .iter()
341 .find(|s| {
342 s.elements
343 .first()
344 .and_then(|e| e.first())
345 .is_some_and(|v| v == "Z37")
346 })
347 .and_then(|s| s.elements.get(1))
348 .and_then(|e| e.first())
349 .filter(|v| !v.is_empty())
350 .cloned();
351 let Some(own_id) = own_id else {
352 continue;
353 };
354 let rff_segs = nav.find_segments_in_group("RFF", &["SG5", "SG8"], i);
355 for rff in &rff_segs {
356 if rff
357 .elements
358 .first()
359 .and_then(|e| e.first())
360 .is_some_and(|v| v == "Z23")
361 {
362 if let Some(ref_val) = rff.elements.first().and_then(|e| e.get(1)) {
363 if ref_val == &own_id {
364 return ConditionResult::False;
365 }
366 }
367 }
368 }
369 }
370 ConditionResult::True
371 }
372
373 /// [11] Wenn in SG8 SEQ+Z37 SG9 CCI+++Z86 CAV+Z69/Z70 (Addition / Subtraktion) vorhanden, darf es in dem Vorgang beliebig viele weitere SG8 SEQ+Z37 mit identischem Rechenschrittidentifikator mit derselben ...
374 // REVIEW: When multiple SG8 SEQ+Z37 share the same Rechenschrittidentifikator (duplicates), all operator CAV codes across those instances must be exclusively Z69 (Addition) or Z70 (Subtraktion). Navigates SG9 children of each SG8 to collect operator codes, groups by step ID, and checks the constraint for duplicates. Zeitraum-ID same-scope matching is not enforced due to API limitations — treats global step_id grouping as approximation. (medium confidence)
375 fn evaluate_11(&self, ctx: &EvaluationContext) -> ConditionResult {
376 let nav = match ctx.navigator() {
377 Some(n) => n,
378 None => return ConditionResult::Unknown,
379 };
380 let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
381 if sg8_count == 0 {
382 return ConditionResult::Unknown;
383 }
384 // Map step_id -> (instance_count, all_operator_codes_across_all_instances)
385 let mut step_data: std::collections::HashMap<String, (usize, Vec<String>)> =
386 std::collections::HashMap::new();
387 for i in 0..sg8_count {
388 let seq_segs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
389 let step_id = seq_segs
390 .iter()
391 .find(|s| {
392 s.elements
393 .first()
394 .and_then(|e| e.first())
395 .is_some_and(|v| v == "Z37")
396 })
397 .and_then(|s| s.elements.get(1))
398 .and_then(|e| e.first())
399 .filter(|v| !v.is_empty())
400 .cloned();
401 let Some(step_id) = step_id else {
402 continue;
403 };
404 let sg9_count = nav.child_group_instance_count(&["SG5", "SG8"], i, "SG9");
405 let mut ops: Vec<String> = Vec::new();
406 for j in 0..sg9_count {
407 let cavs = nav.find_segments_in_child_group("CAV", &["SG5", "SG8"], i, "SG9", j);
408 for cav in &cavs {
409 if let Some(code) = cav.elements.first().and_then(|e| e.first()) {
410 if matches!(code.as_str(), "Z69" | "Z70" | "Z80" | "Z81" | "Z82" | "Z83") {
411 ops.push(code.clone());
412 }
413 }
414 }
415 }
416 let entry = step_data.entry(step_id).or_insert((0, Vec::new()));
417 entry.0 += 1;
418 entry.1.extend(ops);
419 }
420 // Rule: for any step_id with duplicate SG8 instances, ALL operators must be Z69 or Z70
421 for (_step_id, (count, operators)) in &step_data {
422 if *count > 1 && operators.iter().any(|op| op != "Z69" && op != "Z70") {
423 return ConditionResult::False;
424 }
425 }
426 ConditionResult::True
427 }
428
429 /// [12] Wenn in SG8 SEQ+Z37 SG9 CCI+++Z86 CAV+Z83 (Positivwert) vorhanden, darf es in dem Vorgang keine weitere SG8 SEQ+Z37 mit identischem Rechenschrittidentifikator und derselben Zeitraum-ID geben
430 // REVIEW: When a calculation step SG8 SEQ+Z37 contains the Z83 (Positivwert) operator in its SG9 CAV, no other SG8 SEQ+Z37 may share the same Rechenschrittidentifikator within the Vorgang. Groups step IDs by count and checks that any step with Z83 appears exactly once. Zeitraum-ID scoping approximated by global step_id grouping. (medium confidence)
431 fn evaluate_12(&self, ctx: &EvaluationContext) -> ConditionResult {
432 let nav = match ctx.navigator() {
433 Some(n) => n,
434 None => return ConditionResult::Unknown,
435 };
436 let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
437 if sg8_count == 0 {
438 return ConditionResult::Unknown;
439 }
440 // Map step_id -> (instance_count, has_z83_operator)
441 let mut step_data: std::collections::HashMap<String, (usize, bool)> =
442 std::collections::HashMap::new();
443 for i in 0..sg8_count {
444 let seq_segs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
445 let step_id = seq_segs
446 .iter()
447 .find(|s| {
448 s.elements
449 .first()
450 .and_then(|e| e.first())
451 .is_some_and(|v| v == "Z37")
452 })
453 .and_then(|s| s.elements.get(1))
454 .and_then(|e| e.first())
455 .filter(|v| !v.is_empty())
456 .cloned();
457 let Some(step_id) = step_id else {
458 continue;
459 };
460 let sg9_count = nav.child_group_instance_count(&["SG5", "SG8"], i, "SG9");
461 let mut has_z83 = false;
462 for j in 0..sg9_count {
463 let cavs = nav.find_segments_in_child_group("CAV", &["SG5", "SG8"], i, "SG9", j);
464 if cavs.iter().any(|c| {
465 c.elements
466 .first()
467 .and_then(|e| e.first())
468 .is_some_and(|v| v == "Z83")
469 }) {
470 has_z83 = true;
471 }
472 }
473 let entry = step_data.entry(step_id).or_insert((0, false));
474 entry.0 += 1;
475 entry.1 |= has_z83;
476 }
477 // Rule: if Z83 (Positivwert) operator is present for a step_id, that step_id must be unique
478 for (_step_id, (count, has_z83)) in &step_data {
479 if *has_z83 && *count > 1 {
480 return ConditionResult::False;
481 }
482 }
483 ConditionResult::True
484 }
485
486 /// [25] Wenn MP-ID in SG2 NAD+MR (Nachrichtenempfänger) in der Rolle LF
487 /// EXTERNAL: Requires context from outside the message.
488 fn evaluate_25(&self, ctx: &EvaluationContext) -> ConditionResult {
489 ctx.external.evaluate("recipient_is_lf")
490 }
491
492 /// [26] sofern per ORDERS reklamiert
493 /// EXTERNAL: Requires context from outside the message.
494 // REVIEW: Cannot be determined from EDIFACT message content alone — requires external knowledge of whether a prior ORDERS message was used to make a claim. Depends on business process context outside the current message. (medium confidence)
495 fn evaluate_26(&self, ctx: &EvaluationContext) -> ConditionResult {
496 ctx.external.evaluate("claimed_via_orders")
497 }
498
499 /// [37] Wenn ein Gültigkeitsende bereits angegeben werden kann.
500 /// EXTERNAL: Requires context from outside the message.
501 // REVIEW: Whether a validity end date can already be specified is an organizational business decision at message creation time. Checking DTM+Z35 presence would be circular (condition guards whether to include that very field). Must be resolved from business context external to the message. (medium confidence)
502 fn evaluate_37(&self, ctx: &EvaluationContext) -> ConditionResult {
503 ctx.external.evaluate("validity_end_known")
504 }
505
506 /// [42] Der in diesem Datenlement angegebene Code der Schaltzeitdefinition muss innerhalb eines Vorgangs (IDE) eindeutig sein.
507 // REVIEW: Uniqueness of Schaltzeitdefinition codes within an IDE (SG5 group). The only Z44-keyed element in the provided SG8 schema is DTM+Z44 (Schaltzeitänderungszeitpunkt), so uniqueness is enforced on the DTM value within each SG5 instance. Medium confidence because the schema reference only shows DTM segments for SG8 — the actual 'Code' data element could reside in a CCI/SEQ segment not shown here. (medium confidence)
508 fn evaluate_42(&self, ctx: &EvaluationContext) -> ConditionResult {
509 let nav = match ctx.navigator() {
510 Some(n) => n,
511 None => return ConditionResult::Unknown,
512 };
513 let sg5_count = nav.group_instance_count(&["SG5"]);
514 for i in 0..sg5_count {
515 let sg8_count = nav.child_group_instance_count(&["SG5"], i, "SG8");
516 let mut seen = std::collections::HashSet::new();
517 for j in 0..sg8_count {
518 let dtms = nav.find_segments_in_child_group("DTM", &["SG5"], i, "SG8", j);
519 for dtm in &dtms {
520 if dtm
521 .elements
522 .first()
523 .and_then(|e| e.first())
524 .is_some_and(|v| v == "Z44")
525 {
526 if let Some(val) = dtm.elements.first().and_then(|e| e.get(1)) {
527 if !val.is_empty() && !seen.insert(val.clone()) {
528 return ConditionResult::False;
529 }
530 }
531 }
532 }
533 }
534 }
535 ConditionResult::True
536 }
537
538 /// [43] Der in diesem Datenlement angegebene Code der Leistungskurvendefinition muss innerhalb eines Vorgangs (IDE) eindeutig sein.
539 // REVIEW: Uniqueness of Leistungskurvendefinition codes within an IDE (SG5 group). Mirrors condition 42 but targets DTM+Z45 (Leistungskurvenänderungszeitpunkt). Same caveat: the actual 'Code' element might live in a CCI/SEQ segment absent from the provided schema reference — medium confidence. (medium confidence)
540 fn evaluate_43(&self, ctx: &EvaluationContext) -> ConditionResult {
541 let nav = match ctx.navigator() {
542 Some(n) => n,
543 None => return ConditionResult::Unknown,
544 };
545 let sg5_count = nav.group_instance_count(&["SG5"]);
546 for i in 0..sg5_count {
547 let sg8_count = nav.child_group_instance_count(&["SG5"], i, "SG8");
548 let mut seen = std::collections::HashSet::new();
549 for j in 0..sg8_count {
550 let dtms = nav.find_segments_in_child_group("DTM", &["SG5"], i, "SG8", j);
551 for dtm in &dtms {
552 if dtm
553 .elements
554 .first()
555 .and_then(|e| e.first())
556 .is_some_and(|v| v == "Z45")
557 {
558 if let Some(val) = dtm.elements.first().and_then(|e| e.get(1)) {
559 if !val.is_empty() && !seen.insert(val.clone()) {
560 return ConditionResult::False;
561 }
562 }
563 }
564 }
565 }
566 }
567 ConditionResult::True
568 }
569
570 /// [56] Wenn dieses DTM+Z25 (Verwendung der Daten ab) im SG6 RFF (Verwendungszeitraum der Daten) mit der Zeitraum ID "1" im DE1156 ist, muss das Datum der darauffolgende oder ein älterer Tag 0:00 Uhr deut...
571 // REVIEW: Checks that DTM+Z25 (Verwendung der Daten ab) in any SG6 whose associated RFF has Zeitraum-ID '1' in DE1156 (C506 component [0][2]) is at most the day following DTM+137 (message date) at 0:00. Date arithmetic is implemented inline without external crates. Timezone nuance ('0:00 Uhr deutscher Zeit' = CET/CEST midnight, not UTC midnight) is approximated — threshold uses CCYYMMDD+10000 of the next calendar day in UTC, which introduces up to 2-hour margin of error around DST transitions. The Zeitraum-ID '1' is checked at elements[0][2] per the explicit DE1156 reference in the condition text. (medium confidence)
572 fn evaluate_56(&self, ctx: &EvaluationContext) -> ConditionResult {
573 let msg_dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "137");
574 let msg_date_val = match msg_dtm_segs
575 .first()
576 .and_then(|s| s.elements.first())
577 .and_then(|e| e.get(1))
578 {
579 Some(v) => v.clone(),
580 None => return ConditionResult::Unknown,
581 };
582 if msg_date_val.len() < 8 {
583 return ConditionResult::Unknown;
584 }
585 let year: u32 = match msg_date_val[..4].parse::<u32>() {
586 Ok(v) => v,
587 Err(_) => return ConditionResult::Unknown,
588 };
589 let month: u32 = match msg_date_val[4..6].parse::<u32>() {
590 Ok(v) => v,
591 Err(_) => return ConditionResult::Unknown,
592 };
593 let day: u32 = match msg_date_val[6..8].parse::<u32>() {
594 Ok(v) => v,
595 Err(_) => return ConditionResult::Unknown,
596 };
597 let days_in_month: u32 = match month {
598 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
599 4 | 6 | 9 | 11 => 30,
600 2 => {
601 if year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) {
602 29
603 } else {
604 28
605 }
606 }
607 _ => return ConditionResult::Unknown,
608 };
609 let (ny, nm, nd): (u32, u32, u32) = if day >= days_in_month {
610 if month == 12 {
611 (year + 1, 1, 1)
612 } else {
613 (year, month + 1, 1)
614 }
615 } else {
616 (year, month, day + 1)
617 };
618 let threshold = format!("{:04}{:02}{:02}0000", ny, nm, nd);
619 let nav = match ctx.navigator() {
620 Some(n) => n,
621 None => return ConditionResult::Unknown,
622 };
623 let sg5_count = nav.group_instance_count(&["SG5"]);
624 for i in 0..sg5_count {
625 let sg6_count = nav.child_group_instance_count(&["SG5"], i, "SG6");
626 for j in 0..sg6_count {
627 let rffs = nav.find_segments_in_child_group("RFF", &["SG5"], i, "SG6", j);
628 let has_zeitraum_1 = rffs.iter().any(|rff| {
629 rff.elements
630 .first()
631 .and_then(|e| e.get(2))
632 .is_some_and(|v| v == "1")
633 });
634 if has_zeitraum_1 {
635 let dtms = nav.find_segments_in_child_group("DTM", &["SG5"], i, "SG6", j);
636 for dtm in &dtms {
637 if dtm
638 .elements
639 .first()
640 .and_then(|e| e.first())
641 .is_some_and(|v| v == "Z25")
642 {
643 if let Some(dtm_val) = dtm.elements.first().and_then(|e| e.get(1)) {
644 if dtm_val.as_str() > threshold.as_str() {
645 return ConditionResult::False;
646 }
647 }
648 }
649 }
650 }
651 }
652 }
653 ConditionResult::True
654 }
655
656 /// [57] Wenn dieses DTM+Z25 (Verwendung der Daten ab) nicht im SG6 RFF+Z49/ Z53 (Verwendungszeitraum der Daten: Gültige Daten/ Keine Daten) mit der Zeitraum ID "1" im DE1156 ist, muss das Datum dem DTM+Z2...
657 // REVIEW: Complex cross-SG6 ordering condition: for each SG6 with Zeitraum-ID != 1, its DTM+Z25 must equal the DTM+Z26 of the SG6 with the next lower Zeitraum-ID. Implemented via navigator collecting (id, z25, z26) tuples per SG6 instance, then verifying the sequential boundary constraint. (medium confidence)
658 fn evaluate_57(&self, ctx: &EvaluationContext) -> ConditionResult {
659 let nav = match ctx.navigator() {
660 Some(n) => n,
661 None => return ConditionResult::Unknown,
662 };
663 let sg6_path: &[&str] = &["SG5", "SG6"];
664 let sg6_count = nav.group_instance_count(sg6_path);
665 if sg6_count == 0 {
666 return ConditionResult::Unknown;
667 }
668 // Build (zeitraum_id, dtm_z25, dtm_z26) per SG6
669 let mut entries: Vec<(u32, Option<String>, Option<String>)> = Vec::new();
670 for i in 0..sg6_count {
671 let rff_segs = nav.find_segments_in_group("RFF", sg6_path, i);
672 let mut zeitraum_id: Option<u32> = None;
673 for rff in &rff_segs {
674 let qual = rff
675 .elements
676 .first()
677 .and_then(|e| e.first())
678 .map(|s| s.as_str());
679 if matches!(qual, Some("Z49") | Some("Z53")) {
680 if let Some(id_str) = rff.elements.first().and_then(|e| e.get(2)) {
681 if let Ok(id) = id_str.parse::<u32>() {
682 zeitraum_id = Some(id);
683 }
684 }
685 }
686 }
687 let id = match zeitraum_id {
688 Some(id) => id,
689 None => continue,
690 };
691 let dtm_segs = nav.find_segments_in_group("DTM", sg6_path, i);
692 let mut dtm_z25: Option<String> = None;
693 let mut dtm_z26: Option<String> = None;
694 for dtm in &dtm_segs {
695 let qual = dtm
696 .elements
697 .first()
698 .and_then(|e| e.first())
699 .map(|s| s.as_str());
700 let val = dtm
701 .elements
702 .first()
703 .and_then(|e| e.get(1))
704 .filter(|v| !v.is_empty())
705 .cloned();
706 match qual {
707 Some("Z25") => dtm_z25 = val,
708 Some("Z26") => dtm_z26 = val,
709 _ => {}
710 }
711 }
712 entries.push((id, dtm_z25, dtm_z26));
713 }
714 // For non-ID-1 entries, check DTM+Z25 == DTM+Z26 of next-lower Zeitraum-ID
715 for (id, dtm_z25, _) in &entries {
716 if *id == 1 {
717 continue;
718 }
719 let current_z25 = match dtm_z25 {
720 Some(v) => v,
721 None => continue,
722 };
723 let next_lower = entries
724 .iter()
725 .filter(|(other_id, _, _)| *other_id < *id)
726 .map(|(other_id, _, _)| *other_id)
727 .max();
728 let lower_id = match next_lower {
729 Some(lid) => lid,
730 None => return ConditionResult::Unknown,
731 };
732 match entries.iter().find(|(eid, _, _)| *eid == lower_id) {
733 Some((_, _, Some(lower_z26))) => {
734 if current_z25 != lower_z26 {
735 return ConditionResult::False;
736 }
737 }
738 Some((_, _, None)) => return ConditionResult::Unknown,
739 None => return ConditionResult::Unknown,
740 }
741 }
742 ConditionResult::True
743 }
744
745 /// [490] wenn Wert in diesem DE, an der Stelle CCYYMMDD ein Datum aus dem angegeben Zeitraum der Tabelle Kapitel 3.5 „Prozesszeitpunkt bei MESZ mit UTC“ ist
746 fn evaluate_490(&self, ctx: &EvaluationContext) -> ConditionResult {
747 let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "137");
748 match dtm_segs
749 .first()
750 .and_then(|s| s.elements.first())
751 .and_then(|e| e.get(1))
752 {
753 Some(val) => is_mesz_utc(val),
754 None => ConditionResult::False, // segment absent → condition not applicable
755 }
756 }
757
758 /// [491] wenn Wert in diesem DE, an der Stelle CCYYMMDD ein Datum aus dem angegeben Zeitraum der Tabelle Kapitel 3.6 „Prozesszeitpunkt bei MEZ mit UTC“ ist
759 fn evaluate_491(&self, ctx: &EvaluationContext) -> ConditionResult {
760 let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "137");
761 match dtm_segs
762 .first()
763 .and_then(|s| s.elements.first())
764 .and_then(|e| e.get(1))
765 {
766 Some(val) => is_mez_utc(val),
767 None => ConditionResult::False, // segment absent → condition not applicable
768 }
769 }
770
771 /// [1] Nur MP-ID aus Sparte Strom
772 /// EXTERNAL: Requires context from outside the message.
773 // REVIEW: Whether the MP-ID belongs to the electricity sector (Sparte Strom) cannot be determined from the EDIFACT message alone — requires external market participant registry lookup. (medium confidence)
774 fn evaluate_1(&self, ctx: &EvaluationContext) -> ConditionResult {
775 ctx.external.evaluate("mp_id_is_strom_sector")
776 }
777
778 /// [2] Wenn SG5 STS+Z23+Z34 (Berechnungsformel muss beim Absender angefragt werden) in einem SG5 IDE vorhanden
779 fn evaluate_2(&self, ctx: &EvaluationContext) -> ConditionResult {
780 ctx.any_group_has_qualified_value("STS", 0, "Z23", 1, 0, &["Z34"], &["SG5"])
781 }
782
783 /// [5] Wenn das SG8 RFF+Z19 (Referenz auf eine Messlokation) in derselben SG8 SEQ+Z37 nicht vorhanden
784 fn evaluate_5(&self, ctx: &EvaluationContext) -> ConditionResult {
785 // HAND-EDITED: "RFF+Z19 in derselben SG8 SEQ+Z37 nicht vorhanden" — the enclosing SG8 only, when the scope
786 // reaches one; the message as before otherwise.
787 if let Some(scope) = ctx.enclosing_scope("SG8") {
788 return ConditionResult::from(
789 ctx.find_segments_at(scope, "SEQ")
790 .iter()
791 .any(|s| s.get_element(0) == "Z37")
792 && !ctx
793 .find_segments_at(scope, "RFF")
794 .iter()
795 .any(|s| s.get_element(0) == "Z19"),
796 );
797 }
798 ctx.any_group_has_qualifier_without("RFF", 0, "Z19", "SEQ", 0, "Z37", &["SG4", "SG8"])
799 }
800
801 /// [6] Wenn das SG8 RFF+Z23 (Referenz auf Rechenschritt) in derselben SG8 SEQ+Z37 nicht vorhanden
802 fn evaluate_6(&self, ctx: &EvaluationContext) -> ConditionResult {
803 // HAND-EDITED: "RFF+Z23 in derselben SG8 SEQ+Z37 nicht vorhanden" — the enclosing SG8 only, when the scope
804 // reaches one; the message as before otherwise.
805 if let Some(scope) = ctx.enclosing_scope("SG8") {
806 return ConditionResult::from(
807 ctx.find_segments_at(scope, "SEQ")
808 .iter()
809 .any(|s| s.get_element(0) == "Z37")
810 && !ctx
811 .find_segments_at(scope, "RFF")
812 .iter()
813 .any(|s| s.get_element(0) == "Z23"),
814 );
815 }
816 ctx.any_group_has_qualifier_without("RFF", 0, "Z23", "SEQ", 0, "Z37", &["SG4", "SG8"])
817 }
818
819 /// [7] Wenn in derselben SG8 SEQ+Z37 das SG8 RFF+Z19 (Referenz auf eine Messlokation) vorhanden
820 fn evaluate_7(&self, ctx: &EvaluationContext) -> ConditionResult {
821 // HAND-EDITED: "in derselben SG8 SEQ+Z37 das SG8 RFF+Z19 vorhanden" — the enclosing SG8 only, when the scope
822 // reaches one; the message as before otherwise.
823 if let Some(scope) = ctx.enclosing_scope("SG8") {
824 return ConditionResult::from(
825 ctx.find_segments_at(scope, "SEQ")
826 .iter()
827 .any(|s| s.get_element(0) == "Z37")
828 && ctx
829 .find_segments_at(scope, "RFF")
830 .iter()
831 .any(|s| s.get_element(0) == "Z19"),
832 );
833 }
834 ctx.any_group_has_co_occurrence("SEQ", 0, &["Z37"], "RFF", 0, 0, &["Z19"], &["SG4", "SG8"])
835 }
836
837 /// [10] wenn vorhanden
838 fn evaluate_10(&self, _ctx: &EvaluationContext) -> ConditionResult {
839 // "wenn vorhanden" — conditional modifier meaning the associated rule applies
840 // only when the element is present. As a standalone boolean predicate this
841 // always evaluates to True: the condition itself imposes no additional
842 // constraint beyond the field's own optionality.
843 ConditionResult::True
844 }
845
846 /// [13] Wenn in SG8 SEQ+Z37 SG9 CCI+++Z86 CAV+Z80/Z81 (Divisor / Dividend) vorhanden, muss in diesem Vorgang genau eine zweite SG8 SEQ+Z37 mit identischen Rechenschrittidentifikator und derselben Zeitraum-...
847 // REVIEW: Complex cross-SG8 pairing invariant: for each Z80 (Divisor) or Z81 (Dividend) operator in SG9 CCI+Z86, there must be exactly one matching SG8 with the same Rechenschrittidentifikator (SEQ.elements[1][0]) and Zeitraum-ID (RFF+Z46) that carries the complementary operator. Requires full navigator traversal. (medium confidence)
848 fn evaluate_13(&self, ctx: &EvaluationContext) -> ConditionResult {
849 {
850 let nav = match ctx.navigator() {
851 Some(n) => n,
852 None => return ConditionResult::Unknown,
853 };
854
855 let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
856 // Collect (rs_id, zt_id, operator) for all Z37 SG8s that have CCI+++Z86 and CAV+Z80/Z81
857 let mut div_instances: Vec<(String, String, String)> = Vec::new();
858
859 for i in 0..sg8_count {
860 let seqs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
861 let seq_z37 = seqs.iter().find(|s| {
862 s.elements
863 .first()
864 .and_then(|e| e.first())
865 .is_some_and(|v| v == "Z37")
866 });
867 let seq_z37 = match seq_z37 {
868 Some(s) => s,
869 None => continue,
870 };
871
872 let rs_id = seq_z37
873 .elements
874 .get(1)
875 .and_then(|e| e.first())
876 .cloned()
877 .unwrap_or_default();
878
879 let rffs = nav.find_segments_in_group("RFF", &["SG5", "SG8"], i);
880 let zt_id = rffs
881 .iter()
882 .find(|s| {
883 s.elements
884 .first()
885 .and_then(|e| e.first())
886 .is_some_and(|v| v == "Z46")
887 })
888 .and_then(|s| s.elements.first())
889 .and_then(|e| e.get(1))
890 .cloned()
891 .unwrap_or_default();
892
893 let sg9_count = nav.child_group_instance_count(&["SG5", "SG8"], i, "SG9");
894 for j in 0..sg9_count {
895 let ccis =
896 nav.find_segments_in_child_group("CCI", &["SG5", "SG8"], i, "SG9", j);
897 let has_z86 = ccis.iter().any(|s| {
898 s.elements
899 .get(2)
900 .and_then(|e| e.first())
901 .is_some_and(|v| v == "Z86")
902 });
903 if !has_z86 {
904 continue;
905 }
906 let cavs =
907 nav.find_segments_in_child_group("CAV", &["SG5", "SG8"], i, "SG9", j);
908 for cav in &cavs {
909 let op = cav
910 .elements
911 .first()
912 .and_then(|e| e.first())
913 .map(|s| s.as_str());
914 if matches!(op, Some("Z80") | Some("Z81")) {
915 div_instances.push((
916 rs_id.clone(),
917 zt_id.clone(),
918 op.unwrap().to_string(),
919 ));
920 }
921 }
922 }
923 }
924
925 if div_instances.is_empty() {
926 return ConditionResult::Unknown;
927 }
928
929 // Each Z80 must have exactly one Z81 counterpart with same IDs, and vice versa
930 for (rs_id, zt_id, op) in &div_instances {
931 let counterpart = if op == "Z80" { "Z81" } else { "Z80" };
932 let count = div_instances
933 .iter()
934 .filter(|(rs, zt, o)| rs == rs_id && zt == zt_id && o == counterpart)
935 .count();
936 if count != 1 {
937 return ConditionResult::False;
938 }
939 }
940
941 ConditionResult::True
942 }
943 }
944
945 /// [14] Wenn in SG8 SEQ+Z37 SG9 CCI+++Z86 CAV+Z82 (Faktor) vorhanden, darf es in dem Vorgang beliebig viele weitere SG8 SEQ+Z37 mit identischem Rechenschrittidentifikator und derselben Zeitraum-ID geben, d...
946 // REVIEW: When a Z82 (Faktor/Multiplikation) operator is present in a group of SG8s sharing the same Rechenschrittidentifikator and Zeitraum-ID, ALL members of that group must exclusively carry Z82 — no mixing with Z80/Z81. Returns True when this homogeneity constraint holds. (medium confidence)
947 fn evaluate_14(&self, ctx: &EvaluationContext) -> ConditionResult {
948 {
949 let nav = match ctx.navigator() {
950 Some(n) => n,
951 None => return ConditionResult::Unknown,
952 };
953
954 let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
955 // Collect (rs_id, zt_id, operators) for all Z37 SG8s with CCI+++Z86
956 let mut instances: Vec<(String, String, Vec<String>)> = Vec::new();
957
958 for i in 0..sg8_count {
959 let seqs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
960 let seq_z37 = seqs.iter().find(|s| {
961 s.elements
962 .first()
963 .and_then(|e| e.first())
964 .is_some_and(|v| v == "Z37")
965 });
966 let seq_z37 = match seq_z37 {
967 Some(s) => s,
968 None => continue,
969 };
970
971 let rs_id = seq_z37
972 .elements
973 .get(1)
974 .and_then(|e| e.first())
975 .cloned()
976 .unwrap_or_default();
977
978 let rffs = nav.find_segments_in_group("RFF", &["SG5", "SG8"], i);
979 let zt_id = rffs
980 .iter()
981 .find(|s| {
982 s.elements
983 .first()
984 .and_then(|e| e.first())
985 .is_some_and(|v| v == "Z46")
986 })
987 .and_then(|s| s.elements.first())
988 .and_then(|e| e.get(1))
989 .cloned()
990 .unwrap_or_default();
991
992 let sg9_count = nav.child_group_instance_count(&["SG5", "SG8"], i, "SG9");
993 let mut ops: Vec<String> = Vec::new();
994 for j in 0..sg9_count {
995 let ccis =
996 nav.find_segments_in_child_group("CCI", &["SG5", "SG8"], i, "SG9", j);
997 if !ccis.iter().any(|s| {
998 s.elements
999 .get(2)
1000 .and_then(|e| e.first())
1001 .is_some_and(|v| v == "Z86")
1002 }) {
1003 continue;
1004 }
1005 let cavs =
1006 nav.find_segments_in_child_group("CAV", &["SG5", "SG8"], i, "SG9", j);
1007 for cav in &cavs {
1008 if let Some(op) = cav.elements.first().and_then(|e| e.first()) {
1009 ops.push(op.clone());
1010 }
1011 }
1012 }
1013 if !ops.is_empty() {
1014 instances.push((rs_id, zt_id, ops));
1015 }
1016 }
1017
1018 // Find any (rs_id, zt_id) key that has a Z82 operator
1019 let z82_keys: Vec<(&str, &str)> = instances
1020 .iter()
1021 .filter(|(_, _, ops)| ops.iter().any(|op| op == "Z82"))
1022 .map(|(rs, zt, _)| (rs.as_str(), zt.as_str()))
1023 .collect();
1024
1025 if z82_keys.is_empty() {
1026 return ConditionResult::Unknown;
1027 }
1028
1029 // For each group keyed by (rs_id, zt_id) that has Z82, all operators must be exclusively Z82
1030 for (rs_id, zt_id) in &z82_keys {
1031 for (rs, zt, ops) in &instances {
1032 if rs.as_str() == *rs_id && zt.as_str() == *zt_id {
1033 if ops.iter().any(|op| op != "Z82") {
1034 return ConditionResult::False;
1035 }
1036 }
1037 }
1038 }
1039
1040 ConditionResult::True
1041 }
1042 }
1043
1044 /// [15] Wenn in einem SG5 IDE+24 nur eine SEQ+Z37 mit einer SG8 RFF+Z19 (Messlokation) und der selben Zeitraum-ID vorhanden ist
1045 // REVIEW: Checks that within the current SG5 (IDE+24) transaction group there is exactly one SG8 that contains both SEQ+Z37 (Bestandteil des Rechenschritts) and RFF+Z19 (Messlokation reference), and that all such matching SG8s reference the same Zeitraum-ID via RFF+Z46. (medium confidence)
1046 fn evaluate_15(&self, ctx: &EvaluationContext) -> ConditionResult {
1047 {
1048 let nav = match ctx.navigator() {
1049 Some(n) => n,
1050 None => return ConditionResult::Unknown,
1051 };
1052
1053 let sg8_count = nav.group_instance_count(&["SG5", "SG8"]);
1054 let mut matching_count = 0usize;
1055 let mut first_zt_id: Option<String> = None;
1056
1057 for i in 0..sg8_count {
1058 let seqs = nav.find_segments_in_group("SEQ", &["SG5", "SG8"], i);
1059 let has_z37 = seqs.iter().any(|s| {
1060 s.elements
1061 .first()
1062 .and_then(|e| e.first())
1063 .is_some_and(|v| v == "Z37")
1064 });
1065 if !has_z37 {
1066 continue;
1067 }
1068
1069 let rffs = nav.find_segments_in_group("RFF", &["SG5", "SG8"], i);
1070 let has_z19 = rffs.iter().any(|s| {
1071 s.elements
1072 .first()
1073 .and_then(|e| e.first())
1074 .is_some_and(|v| v == "Z19")
1075 });
1076 if !has_z19 {
1077 continue;
1078 }
1079
1080 // Verify Zeitraum-ID consistency across all matching instances
1081 let zt_id = rffs
1082 .iter()
1083 .find(|s| {
1084 s.elements
1085 .first()
1086 .and_then(|e| e.first())
1087 .is_some_and(|v| v == "Z46")
1088 })
1089 .and_then(|s| s.elements.first())
1090 .and_then(|e| e.get(1))
1091 .cloned();
1092
1093 if let Some(ref zt) = zt_id {
1094 match &first_zt_id {
1095 None => first_zt_id = Some(zt.clone()),
1096 Some(first) => {
1097 if first != zt {
1098 return ConditionResult::False;
1099 }
1100 }
1101 }
1102 }
1103
1104 matching_count += 1;
1105 }
1106
1107 ConditionResult::from(matching_count == 1)
1108 }
1109 }
1110
1111 /// [21] Wenn in dieser CAV+ZD3 der Wert im DE7110 mit Z32 (sonstiger Zählzeitdefinitionstyp) vorhanden ist
1112 fn evaluate_21(&self, ctx: &EvaluationContext) -> ConditionResult {
1113 ctx.has_qualified_value("CAV", 0, "ZD3", 0, 3, &["Z32"])
1114 }
1115
1116 /// [22] Wenn MP-ID in SG2 NAD+MS (Nachrichtenabsender) in der Rolle NB
1117 /// EXTERNAL: Requires context from outside the message.
1118 // REVIEW: Whether the MP-ID in NAD+MS (Nachrichtenabsender) holds the role of NB (Netzbetreiber) cannot be determined from the EDIFACT message alone — requires external market participant role registry. (medium confidence)
1119 fn evaluate_22(&self, ctx: &EvaluationContext) -> ConditionResult {
1120 ctx.external.evaluate("sender_is_nb")
1121 }
1122
1123 /// [24] Wenn SG5 STS+Z36+Z45 (Definitionen werden verwendet) vorhanden
1124 fn evaluate_24(&self, ctx: &EvaluationContext) -> ConditionResult {
1125 ctx.has_qualified_value("STS", 0, "Z36", 1, 0, &["Z45"])
1126 }
1127
1128 /// [27] Wenn in SG9 CAV+ZD4+Z26 (keine Verwendung des Hochlastzeitfensters) vorhanden
1129 fn evaluate_27(&self, ctx: &EvaluationContext) -> ConditionResult {
1130 ctx.has_qualified_value("CAV", 0, "ZD4", 0, 3, &["Z26"])
1131 }
1132
1133 /// [29] Wenn in SG8 SEQ+Z43 DTM+Z33 (Zählzeitänderungszeitpunkt) im DE2379 der Code 303 vorhanden
1134 // REVIEW: In the same SG8, SEQ+Z43 (Zählzeitdefinition) must be present and DTM must have format code 303 at elements[0][2]. In a SEQ+Z43 SG8, the only DTM present is DTM+Z33, so checking format code 303 without qualifying by Z33 is safe. (medium confidence)
1135 fn evaluate_29(&self, ctx: &EvaluationContext) -> ConditionResult {
1136 ctx.any_group_has_co_occurrence("SEQ", 0, &["Z43"], "DTM", 0, 2, &["303"], &["SG5", "SG8"])
1137 }
1138
1139 /// [30] Der Wert von CCYY in diesem DE muss genau um eins höher sein, als der Wert CCYY des SG5 DTM+Z34 (Gültigkeitsbeginn) DE2380
1140 // REVIEW: Compares CCYY of DTM+Z35 (Gültigkeitsende) to CCYY of DTM+Z34 (Gültigkeitsbeginn), checking that the end year is exactly start year + 1. 'Diesem DE' most plausibly refers to DTM+Z35 since it is the natural counterpart to DTM+Z34 and its year is constrained relative to Z34. (medium confidence)
1141 fn evaluate_30(&self, ctx: &EvaluationContext) -> ConditionResult {
1142 // CCYY in this DE must be exactly one higher than CCYY of SG5 DTM+Z34 DE2380
1143 // This applies to DTM+Z35 (Gültigkeitsende) whose year must be Z34 year + 1
1144 let z34_segs = ctx.find_segments_with_qualifier("DTM", 0, "Z34");
1145 let z35_segs = ctx.find_segments_with_qualifier("DTM", 0, "Z35");
1146 let z34_year = z34_segs
1147 .first()
1148 .and_then(|s| s.elements.first())
1149 .and_then(|e| e.get(1))
1150 .and_then(|v| v.get(..4))
1151 .and_then(|y| y.parse::<u32>().ok());
1152 let this_year = z35_segs
1153 .first()
1154 .and_then(|s| s.elements.first())
1155 .and_then(|e| e.get(1))
1156 .and_then(|v| v.get(..4))
1157 .and_then(|y| y.parse::<u32>().ok());
1158 match (z34_year, this_year) {
1159 (Some(start), Some(end)) => ConditionResult::from(end == start + 1),
1160 _ => ConditionResult::Unknown,
1161 }
1162 }
1163
1164 /// [31] Wenn im DE2379 dieses Segments der Code 303 vorhanden
1165 // REVIEW: Checks if any DTM segment has format code 303 at elements[0][2] (DE2379). Applies to DTM+Z33/Z44/Z45 which can carry format codes 303 or 401. Message-wide check is sufficient since these are the only DTMs with variable format codes. (medium confidence)
1166 fn evaluate_31(&self, ctx: &EvaluationContext) -> ConditionResult {
1167 ConditionResult::from(ctx.find_segments("DTM").iter().any(|s| {
1168 s.elements
1169 .first()
1170 .and_then(|e| e.get(2))
1171 .map(|v| v == "303")
1172 .unwrap_or(false)
1173 }))
1174 }
1175
1176 /// [32] Der Zeitpunkt in diesem DE muss ≥ dem Zeitpunkt aus dem DE2380 des Gültigkeitsbeginn der ausgerollten Definition (SG5 DTM+Z34) sein
1177 // REVIEW: Validates that SG8 change timestamps (Z33/Z44/Z45) are >= the SG5 DTM+Z34 validity start. 'Diesem DE' refers to the change-point timestamp being validated in SG8. First 12 characters (YYYYMMDDHHmm) are used for consistent lexicographic date comparison in format 303. (medium confidence)
1178 fn evaluate_32(&self, ctx: &EvaluationContext) -> ConditionResult {
1179 // Timestamp in this DE >= DTM+Z34 (Gültigkeitsbeginn der ausgerollten Definition)
1180 // Applies to SG8 change-point timestamps (Z33 Zählzeit, Z44 Schaltzeit, Z45 Leistungskurve)
1181 let z34_segs = ctx.find_segments_with_qualifier("DTM", 0, "Z34");
1182 let z34_value = match z34_segs
1183 .first()
1184 .and_then(|s| s.elements.first())
1185 .and_then(|e| e.get(1))
1186 {
1187 Some(v) => v.clone(),
1188 None => return ConditionResult::Unknown,
1189 };
1190 let threshold = z34_value.get(..12).unwrap_or(z34_value.as_str());
1191 for qual in &["Z33", "Z44", "Z45"] {
1192 let segs = ctx.find_segments_with_qualifier("DTM", 0, qual);
1193 for seg in &segs {
1194 if let Some(val) = seg.elements.first().and_then(|e| e.get(1)) {
1195 let v = val.get(..12).unwrap_or(val);
1196 if v < threshold {
1197 return ConditionResult::False;
1198 }
1199 }
1200 }
1201 }
1202 ConditionResult::True
1203 }
1204
1205 /// [33] Der Zeitpunkt in diesem DE muss ≤ dem Zeitpunkt aus dem DE2380 des Gültigkeitsende der ausgerollten Definition (SG5 DTM+Z35) sein
1206 // REVIEW: Validates that SG8 change timestamps (Z33/Z44/Z45) are <= the SG5 DTM+Z35 validity end. Symmetric counterpart to condition 32. First 12 characters used for format-303 string comparison. (medium confidence)
1207 fn evaluate_33(&self, ctx: &EvaluationContext) -> ConditionResult {
1208 // Timestamp in this DE <= DTM+Z35 (Gültigkeitsende der ausgerollten Definition)
1209 // Applies to SG8 change-point timestamps (Z33 Zählzeit, Z44 Schaltzeit, Z45 Leistungskurve)
1210 let z35_segs = ctx.find_segments_with_qualifier("DTM", 0, "Z35");
1211 let z35_value = match z35_segs
1212 .first()
1213 .and_then(|s| s.elements.first())
1214 .and_then(|e| e.get(1))
1215 {
1216 Some(v) => v.clone(),
1217 None => return ConditionResult::Unknown,
1218 };
1219 let threshold = z35_value.get(..12).unwrap_or(z35_value.as_str());
1220 for qual in &["Z33", "Z44", "Z45"] {
1221 let segs = ctx.find_segments_with_qualifier("DTM", 0, qual);
1222 for seg in &segs {
1223 if let Some(val) = seg.elements.first().and_then(|e| e.get(1)) {
1224 let v = val.get(..12).unwrap_or(val);
1225 if v > threshold {
1226 return ConditionResult::False;
1227 }
1228 }
1229 }
1230 }
1231 ConditionResult::True
1232 }
1233
1234 /// [34] Wenn im DE2379 dieses Segments der Code 401 vorhanden
1235 // REVIEW: Checks if any DTM segment has format code 401 at elements[0][2] (DE2379). Counterpart to condition [31] for the 401 format code (point-in-time without timezone vs. UTC offset). (medium confidence)
1236 fn evaluate_34(&self, ctx: &EvaluationContext) -> ConditionResult {
1237 ConditionResult::from(ctx.find_segments("DTM").iter().any(|s| {
1238 s.elements
1239 .first()
1240 .and_then(|e| e.get(2))
1241 .map(|v| v == "401")
1242 .unwrap_or(false)
1243 }))
1244 }
1245
1246 /// [36] Wenn in SG8 SEQ+Z43 DTM+Z33 (Zählzeitänderungszeitpunkt) im DE2379 der Code 401 vorhanden
1247 // REVIEW: In the same SG8, SEQ+Z43 (Zählzeitdefinition) must be present and DTM+Z33 must have format code 401 at elements[0][2]. Counterpart to condition [29] for the 401 format code. (medium confidence)
1248 fn evaluate_36(&self, ctx: &EvaluationContext) -> ConditionResult {
1249 ctx.any_group_has_co_occurrence("SEQ", 0, &["Z43"], "DTM", 0, 2, &["401"], &["SG5", "SG8"])
1250 }
1251
1252 /// [41] Wenn SG8 SEQ+Z42 (Zählzeitdefinition) vorhanden
1253 fn evaluate_41(&self, ctx: &EvaluationContext) -> ConditionResult {
1254 ctx.any_group_has_qualifier("SEQ", 0, "Z42", &["SG5", "SG8"])
1255 }
1256
1257 /// [44] Der in diesem Datenlement angegebene Code der Zählzeitdefinition muss innerhalb eines Vorgangs (IDE) eindeutig sein.
1258 // REVIEW: RFF+Z27 (elements[0][1]) in SG8 holds the Zählzeitdefinition code per the segment reference. Collects all Z27 reference values message-wide and checks for duplicates. Returns True when all codes are distinct (unique), False when a duplicate exists, Unknown when no Z27 segments found. Group navigator would be needed to properly scope to SG5 boundaries — approximated as message-wide. (medium confidence)
1259 fn evaluate_44(&self, ctx: &EvaluationContext) -> ConditionResult {
1260 // RFF+Z27 in SG8 holds the Zählzeitdefinition code — must be unique within the IDE (SG5) scope.
1261 // Approximate with message-wide check: all Z27 values must be distinct.
1262 let rff_segs = ctx.find_segments("RFF");
1263 let z27_values: Vec<&str> = rff_segs
1264 .iter()
1265 .filter(|s| {
1266 s.elements
1267 .first()
1268 .and_then(|e| e.first())
1269 .is_some_and(|q| q == "Z27")
1270 })
1271 .filter_map(|s| {
1272 s.elements
1273 .first()
1274 .and_then(|e| e.get(1))
1275 .map(|s| s.as_str())
1276 })
1277 .collect();
1278 if z27_values.is_empty() {
1279 return ConditionResult::Unknown;
1280 }
1281 let unique_count: std::collections::HashSet<&str> = z27_values.iter().copied().collect();
1282 ConditionResult::from(unique_count.len() == z27_values.len())
1283 }
1284
1285 /// [46] Wenn in SG8 SEQ+Z73 DTM+Z44 (Schaltzeitänderungszeitpunkt) im DE2379 der Code 303 vorhanden
1286 fn evaluate_46(&self, ctx: &EvaluationContext) -> ConditionResult {
1287 ctx.any_group_has_co_occurrence("SEQ", 0, &["Z73"], "DTM", 0, 2, &["303"], &["SG5", "SG8"])
1288 }
1289
1290 /// [47] Wenn in SG8 SEQ+Z73 DTM+Z44 (Schaltzeitänderungszeitpunkt) im DE2379 der Code 401 vorhanden
1291 fn evaluate_47(&self, ctx: &EvaluationContext) -> ConditionResult {
1292 ctx.any_group_has_co_occurrence("SEQ", 0, &["Z73"], "DTM", 0, 2, &["401"], &["SG5", "SG8"])
1293 }
1294
1295 /// [48] Wenn in SG8 SEQ+Z74 DTM+Z45 (Leistungskurvenänderungszeitpunkt) im DE2379 der Code 303 vorhanden
1296 fn evaluate_48(&self, ctx: &EvaluationContext) -> ConditionResult {
1297 ctx.any_group_has_co_occurrence("SEQ", 0, &["Z74"], "DTM", 0, 2, &["303"], &["SG5", "SG8"])
1298 }
1299
1300 /// [49] Wenn in SG8 SEQ+Z74 DTM+Z45 (Leistungskurvenänderungszeitpunkt) im DE2379 der Code 401 vorhanden
1301 fn evaluate_49(&self, ctx: &EvaluationContext) -> ConditionResult {
1302 ctx.any_group_has_co_occurrence("SEQ", 0, &["Z74"], "DTM", 0, 2, &["401"], &["SG5", "SG8"])
1303 }
1304
1305 /// [50] In jedem DE2379 dieses DTM-Segments innerhalb eines IDE+24 (Vorgangs) muss der gleiche Code angegeben werden
1306 // REVIEW: Checks that all DTM segments in the message use the same DE2379 format code. Full per-IDE+24 scoping would require a navigator, but message-wide consistency is a practical approximation — in well-formed UTILTS messages a single IDE is common. Returns Unknown when no DTMs are found. (medium confidence)
1307 fn evaluate_50(&self, ctx: &EvaluationContext) -> ConditionResult {
1308 {
1309 let dtms = ctx.find_segments("DTM");
1310 let format_codes: Vec<&str> = dtms
1311 .iter()
1312 .filter_map(|s| s.elements.first()?.get(2).map(|s| s.as_str()))
1313 .filter(|s| !s.is_empty())
1314 .collect();
1315 if format_codes.is_empty() {
1316 return ConditionResult::Unknown;
1317 }
1318 let first = format_codes[0];
1319 ConditionResult::from(format_codes.iter().all(|&c| c == first))
1320 }
1321 }
1322
1323 /// [53] Wenn im DE3155 in demselben COM der Code EM vorhanden ist
1324 fn evaluate_53(&self, ctx: &EvaluationContext) -> ConditionResult {
1325 {
1326 let coms = ctx.find_segments("COM");
1327 ConditionResult::from(coms.iter().any(|s| {
1328 s.elements
1329 .first()
1330 .and_then(|e| e.get(1))
1331 .is_some_and(|v| v == "EM")
1332 }))
1333 }
1334 }
1335
1336 /// [54] Wenn im DE3155 in demselben COM der Code TE / FX / AJ / AL vorhanden ist
1337 fn evaluate_54(&self, ctx: &EvaluationContext) -> ConditionResult {
1338 {
1339 let coms = ctx.find_segments("COM");
1340 ConditionResult::from(coms.iter().any(|s| {
1341 s.elements
1342 .first()
1343 .and_then(|e| e.get(1))
1344 .is_some_and(|v| matches!(v.as_str(), "TE" | "FX" | "AJ" | "AL"))
1345 }))
1346 }
1347 }
1348
1349 /// [55] Es ist der Wert einzutragen, der sich aus der Wiederholungshäufigkeit des SG6 RFF+Z49/ Z53 (Verwendungszeitraum der Daten: Gültige Daten/ Keine Daten) ergibt. Bedeutet: Das erste SG6 RFF+Z49/ Z53...
1350 fn evaluate_55(&self, _ctx: &EvaluationContext) -> ConditionResult {
1351 // Hinweis: The value to enter is derived from the repetition index of SG6 RFF+Z49/Z53:
1352 // first occurrence = "1", second = "2", third = "3", etc.
1353 // This is an informational annotation describing how to populate the data element.
1354 ConditionResult::True
1355 }
1356
1357 /// [58] Wenn im selben SG6 RFF+Z49/ Z53 (Verwendungszeitraum der Daten: Gültige Daten/ Keine Daten) im DE1156 (Zeitraum-ID) eine Zeitraum ID genannt ist, die kleiner ist als in einem anderen SG6 RFF+Z49/ ...
1358 // REVIEW: True when at least two SG6 RFF+Z49/Z53 instances exist with different DE1156 Zeitraum-IDs (i.e. min < max across all parsed integer IDs). This satisfies the 'smaller than another' ordering condition. Returns False for single-instance or equal-ID cases. (medium confidence)
1359 fn evaluate_58(&self, ctx: &EvaluationContext) -> ConditionResult {
1360 {
1361 let rffs = ctx.find_segments("RFF");
1362 let ids: Vec<i64> = rffs
1363 .iter()
1364 .filter(|s| {
1365 s.elements
1366 .first()
1367 .and_then(|e| e.first())
1368 .is_some_and(|q| q == "Z49" || q == "Z53")
1369 })
1370 .filter_map(|s| s.elements.first()?.get(2)?.parse::<i64>().ok())
1371 .collect();
1372 if ids.len() < 2 {
1373 return ConditionResult::False;
1374 }
1375 let min = *ids.iter().min().unwrap();
1376 let max = *ids.iter().max().unwrap();
1377 ConditionResult::from(min < max)
1378 }
1379 }
1380
1381 /// [59] Es ist die Zeitraum-ID vom DE1156 aus einem passenden SG6 RFF+Z49 (Verwendungszeitraum der Daten) einzutragen
1382 // REVIEW: Cross-group Zeitraum-ID correlation: SG8 RFF+Z46 DE1154 (reference to Zeitraum-ID) must match a DE1156 value from SG6 RFF+Z49 (Verwendungszeitraum der Daten). Collects DE1156 values from all RFF+Z49 instances and checks whether any RFF+Z46 reference value matches. Returns Unknown when either set is empty. (medium confidence)
1383 fn evaluate_59(&self, ctx: &EvaluationContext) -> ConditionResult {
1384 {
1385 let rffs = ctx.find_segments("RFF");
1386 let sg6_ids: Vec<String> = rffs
1387 .iter()
1388 .filter(|s| {
1389 s.elements
1390 .first()
1391 .and_then(|e| e.first())
1392 .is_some_and(|q| q == "Z49")
1393 })
1394 .filter_map(|s| s.elements.first()?.get(2).cloned())
1395 .filter(|v| !v.is_empty())
1396 .collect();
1397 if sg6_ids.is_empty() {
1398 return ConditionResult::Unknown;
1399 }
1400 let sg8_refs: Vec<String> = rffs
1401 .iter()
1402 .filter(|s| {
1403 s.elements
1404 .first()
1405 .and_then(|e| e.first())
1406 .is_some_and(|q| q == "Z46")
1407 })
1408 .filter_map(|s| s.elements.first()?.get(1).cloned())
1409 .filter(|v| !v.is_empty())
1410 .collect();
1411 if sg8_refs.is_empty() {
1412 return ConditionResult::Unknown;
1413 }
1414 ConditionResult::from(sg8_refs.iter().any(|r| sg6_ids.contains(r)))
1415 }
1416 }
1417
1418 /// [61] Wenn in einem STS+E01 im DE9013 (Status der Antwort) ein Antwortcode aus dem Cluster Ablehnung vorhanden ist
1419 // HAND-EDITED: driven by per-EBD cluster data (mako_prozesse).
1420 // The condition is existential — any Ablehnung STS+E01 → True.
1421 fn evaluate_61(&self, ctx: &EvaluationContext) -> ConditionResult {
1422 any_sts_e01_in_cluster(ctx, Cluster::is_ablehnung)
1423 }
1424
1425 /// [62] Wenn MP-ID in SG2 NAD+MR (Nachrichtenempfänger) in der Rolle MSB
1426 /// EXTERNAL: Requires context from outside the message.
1427 fn evaluate_62(&self, ctx: &EvaluationContext) -> ConditionResult {
1428 ctx.external.evaluate("recipient_is_msb")
1429 }
1430
1431 /// [494] Das hier genannte Datum muss der Zeitpunkt sein, zu dem das Dokument erstellt wurde, oder ein Zeitpunkt, der davor liegt.
1432 /// EXTERNAL: Requires context from outside the message.
1433 // REVIEW: The date must be the document creation time or earlier (i.e. not in the future relative to when the document was created). This requires comparing the EDIFACT field value against an external reference timestamp (the actual document creation time or processing time), which is not available within the message segments themselves. (medium confidence)
1434 fn evaluate_494(&self, ctx: &EvaluationContext) -> ConditionResult {
1435 ctx.external.evaluate("document_date_not_in_future")
1436 }
1437
1438 /// [501] Hinweis: Verwendung der ID der Marktlokation
1439 fn evaluate_501(&self, _ctx: &EvaluationContext) -> ConditionResult {
1440 ConditionResult::True
1441 }
1442
1443 /// [502] Hinweis: Verwendung der ID der Messlokation
1444 fn evaluate_502(&self, _ctx: &EvaluationContext) -> ConditionResult {
1445 // Hinweis: Verwendung der ID der Messlokation — informational note, always applies
1446 ConditionResult::True
1447 }
1448
1449 /// [504] Hinweis: Wert aus BGM+Z55 DE1004 der ORDERS mit der die Reklamation einer Definition erfolgt ist
1450 fn evaluate_504(&self, _ctx: &EvaluationContext) -> ConditionResult {
1451 // Hinweis: Wert aus BGM+Z55 DE1004 der ORDERS mit der die Reklamation einer Definition erfolgt ist — informational note, always applies
1452 ConditionResult::True
1453 }
1454
1455 /// [505] Hinweis: Jede ausgerollte Zählzeitdefinition ist in einem eigenen IDE anzugeben
1456 fn evaluate_505(&self, _ctx: &EvaluationContext) -> ConditionResult {
1457 // Hinweis: Jede ausgerollte Zählzeitdefinition ist in einem eigenen IDE anzugeben — informational note, always applies
1458 ConditionResult::True
1459 }
1460
1461 /// [506] Hinweis: Zeitpunkt, ab dem die Übersicht der Zählzeitdefinitionen gültig ist
1462 fn evaluate_506(&self, _ctx: &EvaluationContext) -> ConditionResult {
1463 // Hinweis: Zeitpunkt, ab dem die Übersicht der Zählzeitdefinitionen gültig ist — informational note, always applies
1464 ConditionResult::True
1465 }
1466
1467 /// [507] Hinweis: Es ist die Zeit nach der deutschen gesetzlichen Zeit anzugeben
1468 fn evaluate_507(&self, _ctx: &EvaluationContext) -> ConditionResult {
1469 // Hinweis: Es ist die Zeit nach der deutschen gesetzlichen Zeit anzugeben — informational note, always applies
1470 ConditionResult::True
1471 }
1472
1473 /// [508] Hinweis: Zeitpunkt, ab dem die Übersicht der Schaltzeitdefinitionen gültig ist
1474 fn evaluate_508(&self, _ctx: &EvaluationContext) -> ConditionResult {
1475 // Hinweis: Zeitpunkt, ab dem die Übersicht der Schaltzeitdefinitionen gültig ist — informational note, always applies
1476 ConditionResult::True
1477 }
1478
1479 /// [509] Hinweis: Zeitpunkt, ab dem die Übersicht der Leistungskurvendefinition gültig ist
1480 fn evaluate_509(&self, _ctx: &EvaluationContext) -> ConditionResult {
1481 // Hinweis: Zeitpunkt, ab dem die Übersicht der Leistungskurvendefinition gültig ist — informational note, always applies
1482 ConditionResult::True
1483 }
1484
1485 /// [510] Hinweis: Für jeden Zählzeitänderungszeitpunkt (SG8 DTM+Z33) ist diese Sementgruppe einmal anzugeben
1486 fn evaluate_510(&self, _ctx: &EvaluationContext) -> ConditionResult {
1487 // Hinweis: Für jeden Zählzeitänderungszeitpunkt (SG8 DTM+Z33) ist diese Segmentgruppe einmal anzugeben — informational note, always applies
1488 ConditionResult::True
1489 }
1490
1491 /// [511] Hinweis: Der Zählzeitänderungszeitpunkt (SG8DTM+Z33) dieser SG8 darf in keiner anderen SG8 „Zählzeitdefinition“ wiederholt werden
1492 // REVIEW: Checks that no DTM+Z33 DE2380 value is repeated across SG8 instances. Collects all Z33 qualifier DTM segments and verifies uniqueness. Medium confidence because SG8-scoped iteration falls back to message-wide, which is correct here since each DTM+Z33 belongs to a distinct SG8. (medium confidence)
1493 fn evaluate_511(&self, ctx: &EvaluationContext) -> ConditionResult {
1494 // HAND-EDITED: "DTM+Z33 dieser SG8 darf in keiner anderen SG8 „Zählzeitdefinition“ wiederholt werden" — compared with the other instances when the
1495 // scope reaches an SG8; the message as before otherwise.
1496 if let Some(scope) = ctx.enclosing_scope("SG8") {
1497 let z33 = |path: &[(&str, usize)]| -> Vec<String> {
1498 ctx.find_segments_at(path, "DTM")
1499 .iter()
1500 .filter(|s| s.get_element(0) == "Z33")
1501 .map(|s| s.get_component(0, 1).to_string())
1502 .filter(|v| !v.is_empty())
1503 .collect()
1504 };
1505 let own = z33(scope);
1506 if own.is_empty() {
1507 return ConditionResult::Unknown;
1508 }
1509 let others = ctx.scopes_under("SG5", "SG8").unwrap_or_default();
1510 return ConditionResult::from(!others.iter().any(|p| {
1511 p.as_slice() != scope
1512 && ctx
1513 .find_segments_at(p, "SEQ")
1514 .iter()
1515 .any(|s| s.get_element(0) == "Z42")
1516 && z33(p).iter().any(|v| own.contains(v))
1517 }));
1518 }
1519 // Uniqueness check: no DTM+Z33 value in DE2380 may appear in more than one SG8
1520 let dtm_z33_segments = ctx.find_segments_with_qualifier("DTM", 0, "Z33");
1521 let values: Vec<&str> = dtm_z33_segments
1522 .iter()
1523 .filter_map(|s| s.elements.first()?.get(1).map(|v| v.as_str()))
1524 .filter(|v| !v.is_empty())
1525 .collect();
1526 if values.is_empty() {
1527 return ConditionResult::Unknown;
1528 }
1529 // Check for duplicates: if all values are unique, condition is satisfied (True)
1530 let mut seen = std::collections::HashSet::new();
1531 let all_unique = values.iter().all(|v| seen.insert(*v));
1532 ConditionResult::from(all_unique)
1533 }
1534
1535 /// [512] Hinweis: Wenn der Code 303 im DE2379 des Zählzeitänderungszeitpunkt (SG8 DTM+Z33) genutzt wird, muss genau ein Wert im DE2380 des Zählzeitänderungszeitpunkt (SG8 DTM+Z33) identisch mit dem Wert...
1536 // REVIEW: When DTM+Z33 uses format code 303 in elements[0][2], exactly one DE2380 value (elements[0][1]) across all Z33 instances must equal the DE2380 value of DTM+Z34 from SG5. Medium confidence due to cross-SG group scoping — the message-wide fallback gives correct semantics here since we're correlating across SG5 and SG8 boundaries. (medium confidence)
1537 fn evaluate_512(&self, ctx: &EvaluationContext) -> ConditionResult {
1538 // When format code 303 is used in DTM+Z33 DE2379,
1539 // exactly one DE2380 value of DTM+Z33 must match the DE2380 of SG5 DTM+Z34
1540 let dtm_z33_segments = ctx.find_segments_with_qualifier("DTM", 0, "Z33");
1541 // Only consider Z33 segments that use format code 303
1542 let z33_303_values: Vec<&str> = dtm_z33_segments
1543 .iter()
1544 .filter(|s| {
1545 s.elements
1546 .first()
1547 .and_then(|e| e.get(2))
1548 .is_some_and(|v| v == "303")
1549 })
1550 .filter_map(|s| s.elements.first()?.get(1).map(|v| v.as_str()))
1551 .filter(|v| !v.is_empty())
1552 .collect();
1553 if z33_303_values.is_empty() {
1554 // No Z33 with format 303 present — condition not applicable
1555 return ConditionResult::Unknown;
1556 }
1557 // Collect DE2380 values from SG5 DTM+Z34
1558 let dtm_z34_segments = ctx.find_segments_with_qualifier("DTM", 0, "Z34");
1559 let z34_values: Vec<&str> = dtm_z34_segments
1560 .iter()
1561 .filter_map(|s| s.elements.first()?.get(1).map(|v| v.as_str()))
1562 .filter(|v| !v.is_empty())
1563 .collect();
1564 if z34_values.is_empty() {
1565 return ConditionResult::Unknown;
1566 }
1567 // Exactly one Z33 DE2380 value must match a Z34 DE2380 value
1568 let match_count = z33_303_values
1569 .iter()
1570 .filter(|v| z34_values.contains(v))
1571 .count();
1572 ConditionResult::from(match_count == 1)
1573 }
1574
1575 /// [513] Hinweis: Wenn der Code 401 im DE2379 des Zählzeitänderungszeitpunkt (SG8 DTM+Z33) genutzt wird, muss genau ein Wert = 0000 im DE2380 des Zählzeitänderungszeitpunkt (SG8 DTM+Z33) sein
1576 fn evaluate_513(&self, ctx: &EvaluationContext) -> ConditionResult {
1577 // When format code 401 is used in DTM+Z33 DE2379,
1578 // exactly one DE2380 value must equal "0000"
1579 let dtm_z33_segments = ctx.find_segments_with_qualifier("DTM", 0, "Z33");
1580 // Only consider Z33 segments that use format code 401
1581 let z33_401_values: Vec<&str> = dtm_z33_segments
1582 .iter()
1583 .filter(|s| {
1584 s.elements
1585 .first()
1586 .and_then(|e| e.get(2))
1587 .is_some_and(|v| v == "401")
1588 })
1589 .filter_map(|s| s.elements.first()?.get(1).map(|v| v.as_str()))
1590 .filter(|v| !v.is_empty())
1591 .collect();
1592 if z33_401_values.is_empty() {
1593 // No Z33 with format 401 present — condition not applicable
1594 return ConditionResult::Unknown;
1595 }
1596 // Exactly one value must equal "0000"
1597 let zero_count = z33_401_values.iter().filter(|v| **v == "0000").count();
1598 ConditionResult::from(zero_count == 1)
1599 }
1600
1601 /// [514] Hinweis: Für jeden Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) ist diese Sementgruppe einmal anzugeben
1602 fn evaluate_514(&self, _ctx: &EvaluationContext) -> ConditionResult {
1603 // Hinweis: Für jeden Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) ist diese Segmentgruppe einmal anzugeben — informational note, always applies
1604 ConditionResult::True
1605 }
1606
1607 /// [515] Hinweis: Kein Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) darf mehrfach vorkommen
1608 fn evaluate_515(&self, ctx: &EvaluationContext) -> ConditionResult {
1609 let dtm_z44 = ctx.find_segments_with_qualifier("DTM", 0, "Z44");
1610 let mut values = std::collections::HashSet::new();
1611 for seg in &dtm_z44 {
1612 let c507 = match seg.elements.first() {
1613 Some(e) => e,
1614 None => continue,
1615 };
1616 let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1617 if !value.is_empty() {
1618 if !values.insert(value.to_string()) {
1619 return ConditionResult::False;
1620 }
1621 }
1622 }
1623 if dtm_z44.is_empty() {
1624 ConditionResult::Unknown
1625 } else {
1626 ConditionResult::True
1627 }
1628 }
1629
1630 /// [516] Hinweis: Wenn der Code 303 im DE2379 des Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) genutzt wird, muss genau ein Wert im DE2380 des Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) identisch mit dem We...
1631 // REVIEW: When format code 303 is used in DTM+Z44 DE2379, at least one DTM+Z44 DE2380 value must match a SG5 DTM+Z34 DE2380 value. Cross-group check; medium confidence due to SG5 context dependency. (medium confidence)
1632 fn evaluate_516(&self, ctx: &EvaluationContext) -> ConditionResult {
1633 let dtm_z44 = ctx.find_segments_with_qualifier("DTM", 0, "Z44");
1634 let dtm_z34 = ctx.find_segments_with_qualifier("DTM", 0, "Z34");
1635 let mut has_303 = false;
1636 let mut z34_values: std::collections::HashSet<String> = std::collections::HashSet::new();
1637 for seg in &dtm_z34 {
1638 let c507 = match seg.elements.first() {
1639 Some(e) => e,
1640 None => continue,
1641 };
1642 let value = c507.get(1).map(|s| s.clone()).unwrap_or_default();
1643 if !value.is_empty() {
1644 z34_values.insert(value);
1645 }
1646 }
1647 for seg in &dtm_z44 {
1648 let c507 = match seg.elements.first() {
1649 Some(e) => e,
1650 None => continue,
1651 };
1652 let format_code = c507.get(2).map(|s| s.as_str()).unwrap_or("");
1653 if format_code == "303" {
1654 has_303 = true;
1655 let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1656 if z34_values.contains(value) {
1657 return ConditionResult::True;
1658 }
1659 }
1660 }
1661 if has_303 {
1662 ConditionResult::False
1663 } else {
1664 ConditionResult::Unknown
1665 }
1666 }
1667
1668 /// [517] Hinweis: Wenn der Code 401 im DE2379 des Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) genutzt wird, muss genau ein Wert = 0000 im DE2380 des Schaltzeitänderungszeitpunkt (SG8 DTM+Z44) sein
1669 fn evaluate_517(&self, ctx: &EvaluationContext) -> ConditionResult {
1670 let dtm_z44 = ctx.find_segments_with_qualifier("DTM", 0, "Z44");
1671 let mut has_401 = false;
1672 for seg in &dtm_z44 {
1673 let c507 = match seg.elements.first() {
1674 Some(e) => e,
1675 None => continue,
1676 };
1677 let format_code = c507.get(2).map(|s| s.as_str()).unwrap_or("");
1678 if format_code == "401" {
1679 has_401 = true;
1680 let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1681 if value == "0000" {
1682 return ConditionResult::True;
1683 }
1684 }
1685 }
1686 if has_401 {
1687 ConditionResult::False
1688 } else {
1689 ConditionResult::Unknown
1690 }
1691 }
1692
1693 /// [518] Hinweis: Für jeden Leistungskurvenänderungszeitpunkt (SG8 DTM+Z45) ist diese Sementgruppe einmal anzugeben
1694 fn evaluate_518(&self, _ctx: &EvaluationContext) -> ConditionResult {
1695 // Hinweis: Für jeden Leistungskurvenänderungszeitpunkt (SG8 DTM+Z45) ist diese Segmentgruppe einmal anzugeben — informational note, always applies
1696 ConditionResult::True
1697 }
1698
1699 /// [519] Hinweis: Kein Leistungskurvenänderungszeitpunkt (SG8 DTM+Z45) darf mehrfach vorkommen
1700 fn evaluate_519(&self, ctx: &EvaluationContext) -> ConditionResult {
1701 let dtm_z45 = ctx.find_segments_with_qualifier("DTM", 0, "Z45");
1702 let mut values = std::collections::HashSet::new();
1703 for seg in &dtm_z45 {
1704 let c507 = match seg.elements.first() {
1705 Some(e) => e,
1706 None => continue,
1707 };
1708 let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1709 if !value.is_empty() {
1710 if !values.insert(value.to_string()) {
1711 return ConditionResult::False;
1712 }
1713 }
1714 }
1715 if dtm_z45.is_empty() {
1716 ConditionResult::Unknown
1717 } else {
1718 ConditionResult::True
1719 }
1720 }
1721
1722 /// [520] Hinweis: Wenn der Code 303 im DE2379 des Leistungskurvenänderungszeitpunkt (SG8 DTM+Z45) genutzt wird, muss genau ein Wert im DE2380 des Leistungskurvenänderungszeitpunkt (SG8 DTM+Z45) identisch ...
1723 // REVIEW: When format code 303 is used in DTM+Z45 DE2379, at least one DTM+Z45 DE2380 value must match a SG5 DTM+Z34 DE2380 value. Same pattern as 516 but for Z45. (medium confidence)
1724 fn evaluate_520(&self, ctx: &EvaluationContext) -> ConditionResult {
1725 let dtm_z45 = ctx.find_segments_with_qualifier("DTM", 0, "Z45");
1726 let dtm_z34 = ctx.find_segments_with_qualifier("DTM", 0, "Z34");
1727 let mut has_303 = false;
1728 let mut z34_values: std::collections::HashSet<String> = std::collections::HashSet::new();
1729 for seg in &dtm_z34 {
1730 let c507 = match seg.elements.first() {
1731 Some(e) => e,
1732 None => continue,
1733 };
1734 let value = c507.get(1).map(|s| s.clone()).unwrap_or_default();
1735 if !value.is_empty() {
1736 z34_values.insert(value);
1737 }
1738 }
1739 for seg in &dtm_z45 {
1740 let c507 = match seg.elements.first() {
1741 Some(e) => e,
1742 None => continue,
1743 };
1744 let format_code = c507.get(2).map(|s| s.as_str()).unwrap_or("");
1745 if format_code == "303" {
1746 has_303 = true;
1747 let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1748 if z34_values.contains(value) {
1749 return ConditionResult::True;
1750 }
1751 }
1752 }
1753 if has_303 {
1754 ConditionResult::False
1755 } else {
1756 ConditionResult::Unknown
1757 }
1758 }
1759
1760 /// [521] Hinweis: Wenn der Code 401 im DE2379 des Leistungskurvenänderungszeitpunkt (SG8 DTM+Z45)
1761 // REVIEW: Incomplete condition text but follows the same pattern as 517 (DTM+Z44/401/'0000') but for DTM+Z45. Medium confidence due to incomplete specification. (medium confidence)
1762 fn evaluate_521(&self, ctx: &EvaluationContext) -> ConditionResult {
1763 let dtm_z45 = ctx.find_segments_with_qualifier("DTM", 0, "Z45");
1764 let mut has_401 = false;
1765 for seg in &dtm_z45 {
1766 let c507 = match seg.elements.first() {
1767 Some(e) => e,
1768 None => continue,
1769 };
1770 let format_code = c507.get(2).map(|s| s.as_str()).unwrap_or("");
1771 if format_code == "401" {
1772 has_401 = true;
1773 let value = c507.get(1).map(|s| s.as_str()).unwrap_or("");
1774 if value == "0000" {
1775 return ConditionResult::True;
1776 }
1777 }
1778 }
1779 if has_401 {
1780 ConditionResult::False
1781 } else {
1782 ConditionResult::Unknown
1783 }
1784 }
1785
1786 /// [522] Hinweis: Jede ausgerollte Schaltzeitdefinition ist in einem eigenen IDE anzugeben
1787 fn evaluate_522(&self, _ctx: &EvaluationContext) -> ConditionResult {
1788 // Hinweis: Jede ausgerollte Schaltzeitdefinition ist in einem eigenen IDE anzugeben — informational note, always applies
1789 ConditionResult::True
1790 }
1791
1792 /// [523] Hinweis: Jede ausgerollte Leistungskurvendefinition ist in einem eigenen IDE anzugeben
1793 fn evaluate_523(&self, _ctx: &EvaluationContext) -> ConditionResult {
1794 // Hinweis: Jede ausgerollte Leistungskurvendefinition ist in einem eigenen IDE anzugeben — informational note, always applies
1795 ConditionResult::True
1796 }
1797
1798 /// [524] Hinweis: Es ist der Code einer Zählzeitdefinition anzugeben
1799 fn evaluate_524(&self, _ctx: &EvaluationContext) -> ConditionResult {
1800 // Hinweis: Es ist der Code einer Zählzeitdefinition anzugeben — informational note, always applies
1801 ConditionResult::True
1802 }
1803
1804 /// [525] Hinweis: Es ist der Code einer Schaltzeitdefinition anzugeben
1805 fn evaluate_525(&self, _ctx: &EvaluationContext) -> ConditionResult {
1806 // Hinweis: Es ist der Code einer Schaltzeitdefinition anzugeben — informational note, always applies
1807 ConditionResult::True
1808 }
1809
1810 /// [526] Hinweis: Es ist der Code einer Leistungskurvendefinition anzugeben
1811 fn evaluate_526(&self, _ctx: &EvaluationContext) -> ConditionResult {
1812 // Hinweis: Es ist der Code einer Leistungskurvendefinition anzugeben — informational note, always applies
1813 ConditionResult::True
1814 }
1815
1816 /// [527] Hinweis: Dieser Code ist anzugeben, wenn es sich um eine einmalig zu übermittelnde Definition handelt
1817 fn evaluate_527(&self, _ctx: &EvaluationContext) -> ConditionResult {
1818 // Hinweis: Dieser Code ist anzugeben, wenn es sich um eine einmalig zu übermittelnde Definition handelt — informational note, always applies
1819 ConditionResult::True
1820 }
1821
1822 /// [528] Hinweis: Dieser Code ist anzugeben, wenn es sich um eine jährlich zu übermittelnde Definition handelt
1823 fn evaluate_528(&self, _ctx: &EvaluationContext) -> ConditionResult {
1824 // Hinweis: Dieser Code ist anzugeben, wenn es sich um eine jährlich zu übermittelnde Definition handelt — informational note, always applies
1825 ConditionResult::True
1826 }
1827
1828 /// [529] Hinweis: Verwendung der ID der Netzlokation
1829 fn evaluate_529(&self, _ctx: &EvaluationContext) -> ConditionResult {
1830 // Hinweis: Verwendung der ID der Netzlokation — informational note, always applies
1831 ConditionResult::True
1832 }
1833
1834 /// [530] Hinweis: Es darf nur eine Information im DE3148 übermittelt werden
1835 fn evaluate_530(&self, _ctx: &EvaluationContext) -> ConditionResult {
1836 // Hinweis: Es darf nur eine Information im DE3148 übermittelt werden — informational note, always applies
1837 ConditionResult::True
1838 }
1839
1840 /// [531] Hinweis: Für weitere Details siehe Kapitel 4.1 "Übermittlung einer Vielzahl von Berechnungsformeln in einem Vorgang"
1841 fn evaluate_531(&self, _ctx: &EvaluationContext) -> ConditionResult {
1842 // Hinweis: Für weitere Details siehe Kapitel 4.1 "Übermittlung einer Vielzahl von Berechnungsformeln in einem Vorgang" — informational note, always applies
1843 ConditionResult::True
1844 }
1845
1846 /// [532] Hinweis: Es ist die Zeitraum-ID vom DE1156 aus einem passenden SG6 RFF+Z49/Z53 (Verwendungszeitraum der Daten: "Gültige Daten", "Keine Daten") aus der Übermittlung der Berechnungsformel aus SG6 R...
1847 // REVIEW: Cross-group Zeitraum-ID matching: collect DE1156 values from SG6 RFF+Z49/Z53 segments, then verify that SG8 RFF+Z46 references (DE1154) point to one of those IDs. Medium confidence because the condition is a hint about correct data linkage across groups, and the exact scoping (per-SG5/SG6 instance vs message-wide) may require navigator-based logic for full correctness. (medium confidence)
1848 fn evaluate_532(&self, ctx: &EvaluationContext) -> ConditionResult {
1849 {
1850 // Collect Zeitraum-IDs (DE1156 = elements[0][2]) from SG6 RFF+Z49 and RFF+Z53
1851 let rff_segments = ctx.find_segments("RFF");
1852 let zeitraum_ids: Vec<String> = rff_segments
1853 .iter()
1854 .filter(|s| {
1855 s.elements
1856 .first()
1857 .and_then(|e| e.first())
1858 .map(|q| q == "Z49" || q == "Z53")
1859 .unwrap_or(false)
1860 })
1861 .filter_map(|s| {
1862 s.elements
1863 .first()
1864 .and_then(|e| e.get(2))
1865 .filter(|v| !v.is_empty())
1866 .cloned()
1867 })
1868 .collect();
1869
1870 if zeitraum_ids.is_empty() {
1871 return ConditionResult::Unknown;
1872 }
1873
1874 // Check SG8 RFF+Z46 references (DE1154 = elements[0][1]) against collected Zeitraum-IDs
1875 let rff_z46_segments = ctx.find_segments_with_qualifier("RFF", 0, "Z46");
1876 if rff_z46_segments.is_empty() {
1877 return ConditionResult::Unknown;
1878 }
1879
1880 let any_match = rff_z46_segments.iter().any(|s| {
1881 s.elements
1882 .first()
1883 .and_then(|e| e.get(1))
1884 .map(|ref_id| zeitraum_ids.iter().any(|zid| zid == ref_id))
1885 .unwrap_or(false)
1886 });
1887
1888 ConditionResult::from(any_match)
1889 }
1890 }
1891
1892 /// [533] Hinweis: Für jeden übermittelten Zeitraum aus der Übermittlung der Berechnungsformel ist genau einmal das Segement anzugeben
1893 fn evaluate_533(&self, _ctx: &EvaluationContext) -> ConditionResult {
1894 // Hinweis: Für jeden übermittelten Zeitraum aus der Übermittlung der Berechnungsformel ist genau einmal das Segment anzugeben — informational note, always applies
1895 ConditionResult::True
1896 }
1897
1898 /// [534] Hinweis: Wert aus SG5 IDE+24 DE7402 mit der die Übermitt-lung der Berechnungsformel erfolgt ist.
1899 fn evaluate_534(&self, _ctx: &EvaluationContext) -> ConditionResult {
1900 // Hinweis: Wert aus SG5 IDE+24 DE7402 — value comes from the Vorgangsnummer of the Berechnungsformel transmission; informational note, always applies
1901 ConditionResult::True
1902 }
1903
1904 /// [912] Format: Wert kann mit maximal 6 Nachkommastellen angegeben werden
1905 // REVIEW: Format condition: max 6 decimal places. Applies to a numeric quantity value; QTY is the standard numeric segment in UTILTS. The AHB row context is not in the structure reference but QTY.elements[0][1] is the standard value position. (medium confidence)
1906 fn evaluate_912(&self, ctx: &EvaluationContext) -> ConditionResult {
1907 ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 6))
1908 }
1909
1910 /// [913] Format: Mögliche Werte: 1 bis 99999
1911 // REVIEW: Format condition: value must be in range [1, 99999]. Both bounds checked with validate_numeric and combined with AND logic. Applied to QTY value element as the most common numeric data element in UTILTS. (medium confidence)
1912 fn evaluate_913(&self, ctx: &EvaluationContext) -> ConditionResult {
1913 // Format: Mögliche Werte: 1 bis 99999
1914 let segs = ctx.find_segments("QTY");
1915 match segs
1916 .first()
1917 .and_then(|s| s.elements.first())
1918 .and_then(|e| e.get(1))
1919 {
1920 Some(val) => {
1921 let ge1 = validate_numeric(val, ">=", 1.0);
1922 let le99999 = validate_numeric(val, "<=", 99999.0);
1923 match (ge1, le99999) {
1924 (ConditionResult::True, ConditionResult::True) => ConditionResult::True,
1925 (ConditionResult::False, _) | (_, ConditionResult::False) => {
1926 ConditionResult::False
1927 }
1928 _ => ConditionResult::Unknown,
1929 }
1930 }
1931 None => ConditionResult::False, // segment absent → condition not applicable
1932 }
1933 }
1934
1935 /// [914] Format: Möglicher Wert: > 0
1936 // REVIEW: Format condition: value must be strictly greater than 0. Applied to QTY value element; validate_numeric with '>' operator handles this directly. (medium confidence)
1937 fn evaluate_914(&self, ctx: &EvaluationContext) -> ConditionResult {
1938 ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, ">", 0.0))
1939 }
1940
1941 /// [915] Format: Möglicher Wert: ≠ 1
1942 // REVIEW: Format condition: value must not equal 1. Applied to the QTY quantity value (element[0][1]) as this is the standard numeric data element in UTILTS. Medium confidence because the exact segment/element this applies to is inferred from context rather than stated explicitly. (medium confidence)
1943 fn evaluate_915(&self, ctx: &EvaluationContext) -> ConditionResult {
1944 ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, "!=", 1.0))
1945 }
1946
1947 /// [930] Format: max. 2 Nachkommastellen
1948 fn evaluate_930(&self, ctx: &EvaluationContext) -> ConditionResult {
1949 ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 2))
1950 }
1951
1952 /// [931] Format: ZZZ = +00
1953 fn evaluate_931(&self, ctx: &EvaluationContext) -> ConditionResult {
1954 ctx.format_check("DTM", 0, 1, validate_timezone_utc)
1955 }
1956
1957 /// [932] Format: HHMM = 2200
1958 fn evaluate_932(&self, ctx: &EvaluationContext) -> ConditionResult {
1959 ctx.format_check("DTM", 0, 1, |val| validate_hhmm_equals(val, "2200"))
1960 }
1961
1962 /// [933] Format: HHMM = 2300
1963 fn evaluate_933(&self, ctx: &EvaluationContext) -> ConditionResult {
1964 ctx.format_check("DTM", 0, 1, |val| validate_hhmm_equals(val, "2300"))
1965 }
1966
1967 /// [937] Format: keine Nachkommastelle
1968 fn evaluate_937(&self, ctx: &EvaluationContext) -> ConditionResult {
1969 ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 0))
1970 }
1971
1972 /// [939] Format: Die Zeichenkette muss die Zeichen @ und . enthalten
1973 fn evaluate_939(&self, ctx: &EvaluationContext) -> ConditionResult {
1974 ctx.format_check("COM", 0, 0, validate_email)
1975 }
1976
1977 /// [940] Format: Die Zeichenkette muss mit dem Zeichen + beginnen und danach dürfen nur noch Ziffern folgen
1978 fn evaluate_940(&self, ctx: &EvaluationContext) -> ConditionResult {
1979 ctx.format_check("COM", 0, 0, validate_phone)
1980 }
1981
1982 /// [947] Format: MMDDHHMM = 12312300
1983 fn evaluate_947(&self, ctx: &EvaluationContext) -> ConditionResult {
1984 ctx.format_check("DTM", 0, 1, |val| validate_mmddhhmm_equals(val, "12312300"))
1985 }
1986
1987 /// [950] Format: Marktlokations-ID
1988 fn evaluate_950(&self, ctx: &EvaluationContext) -> ConditionResult {
1989 ctx.format_check_qualified("LOC", 0, "Z16", 1, 0, validate_malo_id)
1990 }
1991
1992 /// [951] Format: Zählpunktbezeichnung
1993 // REVIEW: Zählpunktbezeichnung (metering point designation) is a 33-character alphanumeric ID. The validate_zahlpunkt helper checks this format. The segment is typically LOC with a metering point qualifier (Z17 for Messlokation or Z19 for SteuerbareRessource in UTILTS). Using both as fallback. (medium confidence)
1994 fn evaluate_951(&self, ctx: &EvaluationContext) -> ConditionResult {
1995 ctx.format_check_qualified("LOC", 0, "Z19", 1, 0, validate_zahlpunkt)
1996 }
1997
1998 /// [960] Format: Netzlokations-ID
1999 // HAND-EDITED: a Netzlokations-ID is "E" + ten alphanumerics (`E1688117482`),
2000 // not MaLo-shaped; `validate_malo_id` rejected every real one (#173).
2001 fn evaluate_960(&self, ctx: &EvaluationContext) -> ConditionResult {
2002 ctx.format_check_qualified("LOC", 0, "Z18", 1, 0, validate_nelo_id)
2003 }
2004
2005 /// [963] Format: Möglicher Wert: ≤ 100
2006 // REVIEW: Format condition: value must be <= 100. Applied to QTY segment value (element 0, component 1). Uses validate_numeric helper. (medium confidence)
2007 fn evaluate_963(&self, ctx: &EvaluationContext) -> ConditionResult {
2008 ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, "<=", 100.0))
2009 }
2010
2011 /// [964] Format: HHMM ≥ 0000
2012 // REVIEW: Format condition: HHMM >= 0000. Combined with condition 965 (HHMM <= 2359), together they validate a valid time range. Using validate_hhmm_range covering both bounds. Applied to DTM segment time value. (medium confidence)
2013 fn evaluate_964(&self, ctx: &EvaluationContext) -> ConditionResult {
2014 ctx.format_check("DTM", 0, 1, |val| validate_hhmm_range(val, "0000", "2359"))
2015 }
2016
2017 /// [965] Format: HHMM ≤ 2359
2018 // REVIEW: Format condition: HHMM <= 2359. Paired with condition 964 (HHMM >= 0000). Together they validate a valid HHMM time. Using validate_hhmm_range for both bounds on the DTM segment. (medium confidence)
2019 fn evaluate_965(&self, ctx: &EvaluationContext) -> ConditionResult {
2020 ctx.format_check("DTM", 0, 1, |val| validate_hhmm_range(val, "0000", "2359"))
2021 }
2022
2023 /// [969] Format: Möglicher Wer: ≤ 1
2024 // REVIEW: 900-series format condition: 'Möglicher Wert: ≤ 1' means the value must be <= 1. Applied to QTY quantity value (most common numeric value in UTILTS). validate_numeric with "<=" operator handles this. Medium confidence because the exact target segment is inferred from context. (medium confidence)
2025 fn evaluate_969(&self, ctx: &EvaluationContext) -> ConditionResult {
2026 ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, "<=", 1.0))
2027 }
2028
2029 /// [2001] Segment bzw. Segmentgruppe ist genau einmal anzugeben
2030 fn evaluate_2001(&self, _ctx: &EvaluationContext) -> ConditionResult {
2031 // Hinweis: Segment bzw. Segmentgruppe ist genau einmal anzugeben — informational cardinality note, always applies
2032 ConditionResult::True
2033 }
2034
2035 /// [2002] Für jeden Code der Zählzeit aus SG8 SEQ+Z42 (Zählzeitdefinition) SG9 CCI+Z39 (Code der Zählzeitdefinition) sind mindestens zwei Register anzugeben, bei denen in dieser SG8 das SG8 RFF+Z27 mit d...
2036 // REVIEW: For each CCI+Z39 code (Zählzeitdefinition code, elements[2][0]) found in SG9 groups, at least 2 SG8 instances must have RFF+Z27 (elements[0][0]=Z27, elements[0][1]=code value) matching that code. Uses message-wide scan since navigator API does not expose a simple find-in-group method for non-child groups. (medium confidence)
2037 fn evaluate_2002(&self, ctx: &EvaluationContext) -> ConditionResult {
2038 // HAND-EDITED: "mindestens zwei Register, bei denen … RFF+Z27 mit diesem Code gefüllt ist" — compared with the other instances when the
2039 // scope reaches an SG8; the message as before otherwise.
2040 if let Some(scope) = ctx.enclosing_scope("SG8") {
2041 let codes = |path: &[(&str, usize)]| -> Vec<String> {
2042 ctx.find_segments_at(path, "RFF")
2043 .iter()
2044 .filter(|s| s.get_element(0) == "Z27")
2045 .map(|s| s.get_component(0, 1).to_string())
2046 .filter(|v| !v.is_empty())
2047 .collect()
2048 };
2049 let own = codes(scope);
2050 if own.is_empty() {
2051 return ConditionResult::Unknown;
2052 }
2053 let sg8s = ctx.scopes_under("SG5", "SG8").unwrap_or_default();
2054 return ConditionResult::from(
2055 own.iter()
2056 .all(|code| sg8s.iter().filter(|p| codes(p).contains(code)).count() >= 2),
2057 );
2058 }
2059 // Collect CCI+Z39 codes from SG9 (Code der Zählzeitdefinition) message-wide
2060 let cci_segments = ctx.find_segments("CCI");
2061 let codes: Vec<String> = cci_segments
2062 .iter()
2063 .filter(|s| {
2064 s.elements
2065 .first()
2066 .and_then(|e| e.first())
2067 .is_some_and(|v| v == "Z39")
2068 })
2069 .filter_map(|s| s.elements.get(2).and_then(|e| e.first()).cloned())
2070 .filter(|c| !c.is_empty())
2071 .collect();
2072 if codes.is_empty() {
2073 return ConditionResult::Unknown;
2074 }
2075 // For each code, at least 2 SG8 instances must have RFF+Z27 referencing that code
2076 let rff_z27 = ctx.find_segments_with_qualifier("RFF", 0, "Z27");
2077 for code in &codes {
2078 let count = rff_z27
2079 .iter()
2080 .filter(|s| {
2081 s.elements
2082 .first()
2083 .and_then(|e| e.get(1))
2084 .is_some_and(|v| v == code)
2085 })
2086 .count();
2087 if count < 2 {
2088 return ConditionResult::False;
2089 }
2090 }
2091 ConditionResult::True
2092 }
2093
2094 /// [2004] Segment ist genau einmal für jede Zeitraum-ID aus dem DE1156 der SG6 RFF+Z49 (Verwendungszeitraum der Daten: "Gültige Daten") anzugeben
2095 // REVIEW: For each Zeitraum-ID from SG6 RFF+Z49 DE1156 (elements[0][2]), exactly one SG5 STS must reference it. STS+E01 (Status der Antwort) stores Zeitraum-ID at elements[2][3] (DE9012). STS+Z23 (Status der Berechnungsformel) stores it at elements[2][0] (DE9013). Checks count == 1 for each Zeitraum-ID. (medium confidence)
2096 fn evaluate_2004(&self, ctx: &EvaluationContext) -> ConditionResult {
2097 // Collect Zeitraum-IDs from SG6 RFF+Z49 DE1156 (elements[0][2])
2098 let rff_z49 = ctx.find_segments_with_qualifier("RFF", 0, "Z49");
2099 if rff_z49.is_empty() {
2100 return ConditionResult::Unknown;
2101 }
2102 let zeitraum_ids: Vec<String> = rff_z49
2103 .iter()
2104 .filter_map(|s| s.elements.first().and_then(|e| e.get(2)).cloned())
2105 .filter(|id| !id.is_empty())
2106 .collect();
2107 if zeitraum_ids.is_empty() {
2108 return ConditionResult::Unknown;
2109 }
2110 let sts_segments = ctx.find_segments("STS");
2111 for zid in &zeitraum_ids {
2112 // STS+E01: Zeitraum-ID at elements[2][3] (DE9012)
2113 // STS+Z23: Zeitraum-ID at elements[2][0] (DE9013)
2114 let count = sts_segments
2115 .iter()
2116 .filter(|s| {
2117 let qual = s
2118 .elements
2119 .first()
2120 .and_then(|e| e.first())
2121 .map(|v| v.as_str())
2122 .unwrap_or("");
2123 match qual {
2124 "E01" => s
2125 .elements
2126 .get(2)
2127 .and_then(|e| e.get(3))
2128 .is_some_and(|v| v == zid),
2129 "Z23" => s
2130 .elements
2131 .get(2)
2132 .and_then(|e| e.first())
2133 .is_some_and(|v| v == zid),
2134 _ => false,
2135 }
2136 })
2137 .count();
2138 if count != 1 {
2139 return ConditionResult::False;
2140 }
2141 }
2142 ConditionResult::True
2143 }
2144
2145 /// [2005] Segment ist genau einmal für jede Zeitraum-ID aus dem DE9012 der SG5 STS+E01 ("Status der Antwort") anzugeben, wenn im selben SG5 STS+E01 im DE9013 der Code A99 ("Sontiges") enthalten ist
2146 // REVIEW: Finds STS+E01 segments where DE9013 (Code des Prüfschritts, elements[2][0]) = 'A99' (Sonstiges), then collects Zeitraum-IDs from DE9012 (elements[2][3]). For each such ID, checks that exactly one SG8 RFF+Z46 (elements[0][1]) references it, confirming the annotated segment appears exactly once per qualifying Zeitraum-ID. (medium confidence)
2147 fn evaluate_2005(&self, ctx: &EvaluationContext) -> ConditionResult {
2148 // Collect Zeitraum-IDs from STS+E01 where DE9013 (elements[2][0]) = A99 (Sonstiges)
2149 let sts_e01 = ctx.find_segments_with_qualifier("STS", 0, "E01");
2150 let zeitraum_ids: Vec<String> = sts_e01
2151 .iter()
2152 .filter(|s| {
2153 s.elements
2154 .get(2)
2155 .and_then(|e| e.first())
2156 .is_some_and(|v| v == "A99")
2157 })
2158 .filter_map(|s| s.elements.get(2).and_then(|e| e.get(3)).cloned())
2159 .filter(|id| !id.is_empty())
2160 .collect();
2161 if zeitraum_ids.is_empty() {
2162 return ConditionResult::Unknown;
2163 }
2164 // For each Zeitraum-ID, exactly one SG8 RFF+Z46 (Referenz auf Zeitraum-ID) must reference it
2165 let rff_z46 = ctx.find_segments_with_qualifier("RFF", 0, "Z46");
2166 for zid in &zeitraum_ids {
2167 let count = rff_z46
2168 .iter()
2169 .filter(|s| {
2170 s.elements
2171 .first()
2172 .and_then(|e| e.get(1))
2173 .is_some_and(|v| v == zid)
2174 })
2175 .count();
2176 if count != 1 {
2177 return ConditionResult::False;
2178 }
2179 }
2180 ConditionResult::True
2181 }
2182
2183 /// [2006] Segmentgruppe ist mindestens einmal für jede Zeitraum-ID aus dem DE9013 der SG5 STS+Z23+Z33 (Berechnungsformel angefügt) anzugeben
2184 // REVIEW: Finds STS+Z23 (Status der Berechnungsformel) where status code (elements[1][0]) = 'Z33' (formula attached), collects Zeitraum-IDs from DE9013 (elements[2][0]). For each such ID, checks that at least one SG8 RFF+Z46 (elements[0][1]) references it, confirming the calculation formula group appears at minimum once per qualifying Zeitraum-ID. (medium confidence)
2185 fn evaluate_2006(&self, ctx: &EvaluationContext) -> ConditionResult {
2186 // Collect Zeitraum-IDs from STS+Z23 (Berechnungsformel) where status code = Z33 (angefügt)
2187 let sts_z23 = ctx.find_segments_with_qualifier("STS", 0, "Z23");
2188 let zeitraum_ids: Vec<String> = sts_z23
2189 .iter()
2190 .filter(|s| {
2191 s.elements
2192 .get(1)
2193 .and_then(|e| e.first())
2194 .is_some_and(|v| v == "Z33")
2195 })
2196 .filter_map(|s| s.elements.get(2).and_then(|e| e.first()).cloned())
2197 .filter(|id| !id.is_empty())
2198 .collect();
2199 if zeitraum_ids.is_empty() {
2200 return ConditionResult::Unknown;
2201 }
2202 // For each Zeitraum-ID, at least one SG8 RFF+Z46 must reference it
2203 let rff_z46 = ctx.find_segments_with_qualifier("RFF", 0, "Z46");
2204 for zid in &zeitraum_ids {
2205 let count = rff_z46
2206 .iter()
2207 .filter(|s| {
2208 s.elements
2209 .first()
2210 .and_then(|e| e.get(1))
2211 .is_some_and(|v| v == zid)
2212 })
2213 .count();
2214 if count < 1 {
2215 return ConditionResult::False;
2216 }
2217 }
2218 ConditionResult::True
2219 }
2220
2221 /// [2007] Segmentgruppe ist genau einmal für jede Zeitraum-ID aus dem DE9013 der SG5 STS+Z23+Z33 (Berechnungsformel angefügt) anzugeben
2222 // REVIEW: The condition says the segment group must appear exactly once for each Zeitraum-ID from SG5 STS+Z23+Z33 (Berechnungsformel angefügt). From the MIG reference, STS with Statuskategorie Z23 has elements[0][0]=Z23, elements[1][0]=status code (Z33=angefügt), and elements[2][0]=Zeitraum-ID. The evaluator returns True when at least one such STS+Z23+Z33 is present in the message, which is when the cardinality rule triggers. Full cardinality counting (exactly one group per Zeitraum-ID) would require navigator-level group instance counting and cross-referencing, which goes beyond what the boolean ConditionResult can express — the condition is really a presence trigger for the group requirement. (medium confidence)
2223 fn evaluate_2007(&self, ctx: &EvaluationContext) -> ConditionResult {
2224 // Condition 2007: Segment group required exactly once per Zeitraum-ID from SG5 STS+Z23+Z33
2225 // Evaluates to True when at least one STS in SG5 has Statuskategorie=Z23 and Status=Z33
2226 // (Berechnungsformel angefügt), indicating the group must be present for that Zeitraum-ID.
2227 // STS Z23 structure: elements[0][0]=Z23 (Statuskategorie), elements[1][0]=Z33 (Status), elements[2][0]=Zeitraum-ID
2228 let sts_segments = ctx.find_segments("STS");
2229 let has_z23_z33 = sts_segments.iter().any(|s| {
2230 s.elements
2231 .first()
2232 .and_then(|e| e.first())
2233 .is_some_and(|v| v == "Z23")
2234 && s.elements
2235 .get(1)
2236 .and_then(|e| e.first())
2237 .is_some_and(|v| v == "Z33")
2238 });
2239 ConditionResult::from(has_z23_z33)
2240 }
2241}