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