automapper-validation 0.51.0

AHB condition expression parsing, evaluation, and EDIFACT validation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
//! The products of chapter 6.2 of the Codeliste der Konfigurationen,
//! "Produkte zur Bestellung einer Änderung von Abrechnungsdaten": what an
//! ORDERS 17133 may order in PIA+5 DE7140, and what each product takes in its
//! SG30 — a Produkteigenschaft (CAV+ZH9 DE7110) from its own list, or a
//! Wertedetail (CAV+ZV4 DE7110) of its own format.
//!
//! The AHB conditions reading it: `[158]` (the product is one of them),
//! `[159]` (it has Produkteigenschaften), `[160]` (the CAV+ZH9 code is one of
//! its own), `[162]` (the CAV+ZV4 value fits its format) and `[180]` (it has a
//! Wertedetail). Bilanzkreis takes an EIC (`[970]` an..17),
//! Jahresverbrauchsprognose a whole number (`[909]` ∧ `[937]`).
//!
//! Chapter 6.1, "Produkte zur Anmeldung einer Zuordnung des LFN (UTILMD)",
//! has the same columns for the Anmeldungen (55001, 55077, 55014, ...: SG8
//! SEQ+Z79 PIA+5, SG10 CAV+ZH9 / CAV+ZV4, `[292]`, `[36]`, `[38]`, `[39]`,
//! `[40]`) and is [`AbrechnungsdatenProdukte::anmeldung`]. It adds the
//! Prüfidentifikatoren that may order each product (the Bilanzkreis, which
//! every Produktpaket must name, is the one product 55014 may order), how often
//! one may stand in a Produktpaket, and Wertedetails needed only beside one
//! Produkteigenschaft (Tranchengröße: a share beside `9991000003014`, in 1.4 a
//! TR-ID beside `9991000003220`).
//!
//! Data source: the Codeliste's .docx editions, extracted with
//! `scripts/extract_konfigurationen_abrechnungsdaten.py` (`--chapter 6.1` for
//! `konfigurationen_anmeldung.json`). FV2504 to FV2604 read version 1.3c,
//! FV2610 version 1.4 (which adds spezifische Arbeit to 6.2 and the
//! Ausschluss products to 6.1).

use serde::Deserialize;
use std::collections::HashMap;
use std::sync::OnceLock;

use super::{ConditionResult, EvaluationContext};
use crate::eval::format_validators::{
    is_edifact_number, validate_max_decimal_places, validate_numeric,
    validate_technische_ressource_id,
};

const EMBEDDED_JSON: &str = include_str!("../../data/konfigurationen_abrechnungsdaten.json");
const ANMELDUNG_JSON: &str = include_str!("../../data/konfigurationen_anmeldung.json");

/// The edition of the Codeliste der Konfigurationen a format version reads.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Edition {
    /// Version 1.3c: FV2504, FV2510, FV2604.
    V1_3c,
    /// Version 1.4: FV2610.
    V1_4,
}

impl Edition {
    /// The edition `fv` reads; `None` for a format version before FV2504.
    pub fn of_format_version(fv: &str) -> Option<Self> {
        match fv {
            "FV2504" | "FV2510" | "FV2604" => Some(Self::V1_3c),
            "FV2610" => Some(Self::V1_4),
            _ => None,
        }
    }

    fn key(self) -> &'static str {
        match self {
            Self::V1_3c => "1.3c",
            Self::V1_4 => "1.4",
        }
    }
}

#[derive(Debug, Deserialize)]
struct RawFile {
    versions: HashMap<String, RawVersion>,
}

#[derive(Debug, Deserialize)]
struct RawVersion {
    products: Vec<Product>,
}

/// One product of chapter 6.2 or 6.1.
#[derive(Debug, Deserialize)]
pub struct Product {
    /// The product code, PIA+5 DE7140.
    pub code: String,
    pub name: String,
    /// The codes its CAV+ZH9 DE7110 may carry; empty when it has none.
    pub property_codes: Vec<PropertyCode>,
    /// What its CAV+ZV4 DE7110 carries; `None` when it has none.
    pub wertedetails: Option<Wertedetails>,
    /// The Prüfidentifikatoren that may order it.
    #[serde(default)]
    pub pids: Vec<String>,
    /// The subchapter listing it (`6.1.1`); `None` in a chapter without.
    #[serde(default)]
    pub chapter: Option<String>,
    /// How often it may stand in one Produktpaket (6.1 only).
    #[serde(default)]
    pub max_repetitions: Option<u32>,
}

impl Product {
    /// Whether `pid` may order it. True when the table names no
    /// Prüfidentifikator.
    pub fn orderable_by(&self, pid: &str) -> bool {
        self.pids.is_empty() || self.pids.iter().any(|p| p == pid)
    }
}

#[derive(Debug, Deserialize)]
pub struct PropertyCode {
    pub code: String,
    pub name: String,
}

/// A product's Wertedetail: its name and the format conditions its value
/// must hold (`[970]`).
#[derive(Debug, Deserialize)]
pub struct Wertedetails {
    pub name: String,
    pub conditions: Vec<u32>,
    /// Where the Wertedetail is needed only beside a Produkteigenschaft, one
    /// entry per such code with the formats its value then holds. Empty when
    /// it stands beside any (or none).
    #[serde(default)]
    pub variants: Vec<WertedetailsVariant>,
}

/// The Wertedetail beside one Produkteigenschaft.
#[derive(Debug, Deserialize)]
pub struct WertedetailsVariant {
    pub with_property: String,
    pub conditions: Vec<u32>,
}

/// A valid EIC of a Bilanzkreis (`11X...`, check character included), for a
/// sample's Wertedetail.
pub const EXAMPLE_BILANZKREIS: &str = "11XBILANZKREIS-O";

impl Wertedetails {
    /// Whether `value` holds the format conditions: those of the variant
    /// beside `property` where the Wertedetail has variants (any variant's
    /// when `property` is `None` or none matches), else all of them. Unknown
    /// for a condition the table names but this code does not know.
    pub fn fits_beside(&self, value: &str, property: Option<&str>) -> ConditionResult {
        if self.variants.is_empty() {
            return holds(&self.conditions, value);
        }
        if let Some(v) = self
            .variants
            .iter()
            .find(|v| Some(v.with_property.as_str()) == property)
        {
            return holds(&v.conditions, value);
        }
        let results = self.variants.iter().map(|v| holds(&v.conditions, value));
        results.fold(ConditionResult::False, |best, r| match (best, r) {
            (ConditionResult::True, _) | (_, ConditionResult::True) => ConditionResult::True,
            (ConditionResult::Unknown, _) | (_, ConditionResult::Unknown) => {
                ConditionResult::Unknown
            }
            _ => ConditionResult::False,
        })
    }

    /// Whether `value` holds the format conditions, beside any
    /// Produkteigenschaft.
    pub fn fits(&self, value: &str) -> ConditionResult {
        self.fits_beside(value, None)
    }

    /// Whether the Wertedetail is needed beside `property` (the product's
    /// CAV+ZH9 code, `None` without one): always, unless it is tied to other
    /// Produkteigenschaften.
    pub fn needed_beside(&self, property: Option<&str>) -> bool {
        self.variants.is_empty()
            || self
                .variants
                .iter()
                .any(|v| Some(v.with_property.as_str()) == property)
    }

    /// Whether `value` is what a sample should carry beside `property`: it
    /// holds the formats, and a Bilanzkreis (`[970]`, which says only
    /// an..17) is an EIC.
    pub fn suits_sample(&self, value: &str, property: Option<&str>) -> bool {
        self.fits_beside(value, property) == ConditionResult::True
            && (!self.conditions.contains(&970) || is_eic(value))
    }

    /// A value holding the formats beside `property`, for a sample: a
    /// Bilanzkreis EIC, a TR-ID, a whole number, a share. `None` when none
    /// of these does.
    pub fn example_value(&self, property: Option<&str>) -> Option<&'static str> {
        [EXAMPLE_BILANZKREIS, "D1234567890", "4000", "50", "1.125"]
            .into_iter()
            .filter(|v| !self.conditions.contains(&970) || *v == EXAMPLE_BILANZKREIS)
            .find(|v| self.fits_beside(v, property) == ConditionResult::True)
    }
}

/// Whether `value` is an Energy Identification Code: 16 characters of
/// digits, capitals and `-`, the last the check character of the first 15
/// (weights 16 down to 2, modulo 37).
pub fn is_eic(value: &str) -> bool {
    let digit = |c: char| match c {
        '0'..='9' => Some(c as u32 - '0' as u32),
        'A'..='Z' => Some(c as u32 - 'A' as u32 + 10),
        '-' => Some(36),
        _ => None,
    };
    let Some(digits) = value.chars().map(digit).collect::<Option<Vec<u32>>>() else {
        return false;
    };
    if digits.len() != 16 {
        return false;
    }
    let sum: u32 = digits[..15]
        .iter()
        .enumerate()
        .map(|(i, d)| d * (16 - i as u32))
        .sum();
    let check = 36 - (sum + 36) % 37;
    check != 36 && digits[15] == check
}

/// Whether `value` holds every format condition of `conditions`.
fn holds(conditions: &[u32], value: &str) -> ConditionResult {
    let mut result = ConditionResult::True;
    for condition in conditions {
        let holds = match condition {
            // Format: an..17
            970 => ConditionResult::from(value.chars().count() <= 17),
            // Format: Mögliche Werte: 0 bis n
            909 => ConditionResult::from(
                is_edifact_number(value) && !value.trim_start().starts_with('-'),
            ),
            // Format: keine Nachkommastelle
            937 => ConditionResult::from(is_edifact_number(value) && !value.contains(['.', ','])),
            // Format: max. 3 Nachkommastellen
            906 => validate_max_decimal_places(value, 3),
            // Format: Möglicher Wert: > 0
            914 => validate_numeric(value, ">", 0.0),
            // Format: TR-ID
            922 => validate_technische_ressource_id(value),
            // Format: max. 2 Nachkommastellen
            930 => validate_max_decimal_places(value, 2),
            // Format: Möglicher Wert: < 100
            955 => validate_numeric(value, "<", 100.0),
            _ => ConditionResult::Unknown,
        };
        match holds {
            ConditionResult::False => return ConditionResult::False,
            ConditionResult::Unknown => result = ConditionResult::Unknown,
            ConditionResult::True => {}
        }
    }
    result
}

/// Chapter 6.2 (or 6.1) in every edition the format versions read.
#[derive(Debug)]
pub struct AbrechnungsdatenProdukte {
    editions: HashMap<String, Vec<Product>>,
}

impl AbrechnungsdatenProdukte {
    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
        let raw: RawFile = serde_json::from_str(json)?;
        Ok(Self {
            editions: raw
                .versions
                .into_iter()
                .map(|(version, v)| (version, v.products))
                .collect(),
        })
    }

    pub fn embedded() -> &'static Self {
        static CELL: OnceLock<AbrechnungsdatenProdukte> = OnceLock::new();
        CELL.get_or_init(|| {
            AbrechnungsdatenProdukte::from_json(EMBEDDED_JSON)
                .expect("embedded konfigurationen_abrechnungsdaten.json is malformed")
        })
    }

    /// Chapter 6.1, the products of the UTILMD Anmeldungen.
    pub fn anmeldung() -> &'static Self {
        static CELL: OnceLock<AbrechnungsdatenProdukte> = OnceLock::new();
        CELL.get_or_init(|| {
            AbrechnungsdatenProdukte::from_json(ANMELDUNG_JSON)
                .expect("embedded konfigurationen_anmeldung.json is malformed")
        })
    }

    /// The products of `edition`, in the table's order.
    pub fn products(&self, edition: Edition) -> &[Product] {
        self.editions
            .get(edition.key())
            .map_or(&[], |products| products.as_slice())
    }

    /// The product `code` of `edition`, if the table lists it.
    pub fn product(&self, edition: Edition, code: &str) -> Option<&Product> {
        self.products(edition).iter().find(|p| p.code == code)
    }
}

/// The product `code` of chapter 6.1 or 6.2 of `edition`, if either lists
/// it. Their codes are apart (the Bilanzkreis is `9991000002082` in 6.1,
/// `9991000002553` in 6.2).
pub fn chapter6_product(edition: Edition, code: &str) -> Option<&'static Product> {
    AbrechnungsdatenProdukte::anmeldung()
        .product(edition, code)
        .or_else(|| AbrechnungsdatenProdukte::embedded().product(edition, code))
}

/// The product code ordered in PIA+5 DE7140 of the SG29 the field sits in,
/// or of the message's first PIA+5 without a scope (17133 sends one SG29,
/// `[2050]`). `None` when there is none.
fn ordered_product(ctx: &EvaluationContext) -> Option<String> {
    ctx.scoped_find_segments_in("SG29", "PIA")
        .into_iter()
        .find(|s| s.get_element(0) == "5")
        .and_then(|s| s.elements.get(1)?.first().cloned())
        .filter(|code| !code.is_empty())
}

/// What `answer` makes of the ordered product: Unknown when there is none,
/// False for a product the table does not list.
fn of_product(
    ctx: &EvaluationContext,
    edition: Edition,
    answer: impl Fn(&Product) -> ConditionResult,
) -> ConditionResult {
    let Some(code) = ordered_product(ctx) else {
        return ConditionResult::Unknown;
    };
    match AbrechnungsdatenProdukte::embedded().product(edition, &code) {
        Some(product) => answer(product),
        None => ConditionResult::False,
    }
}

/// `[158]` Es sind nur die Produkt-Codes erlaubt, die in Kapitel 6.2 genannt
/// sind: judged on the element's own value (PIA+5 DE7140), else on the
/// ordered product.
pub fn product_is_listed(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
    let code = match ctx.resolved_value {
        Some(value) => value.to_string(),
        None => match ordered_product(ctx) {
            Some(code) => code,
            None => return ConditionResult::Unknown,
        },
    };
    ConditionResult::from(
        AbrechnungsdatenProdukte::embedded()
            .product(edition, &code)
            .is_some(),
    )
}

/// `[159]` Wenn der Produkt-Code in der Spalte "Code der Produkteigenschaft"
/// mit einem Code befüllt ist.
pub fn has_property_codes(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
    of_product(ctx, edition, |p| {
        ConditionResult::from(!p.property_codes.is_empty())
    })
}

/// `[160]` Nur die Codes der Produkteigenschaft des Produkts: the value of
/// CAV+ZH9 DE7110 is one of its codes.
pub fn property_code_allowed(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
    let Some(value) = ctx.resolved_value else {
        return ConditionResult::Unknown;
    };
    of_product(ctx, edition, |p| {
        ConditionResult::from(p.property_codes.iter().any(|c| c.code == value))
    })
}

/// `[162]` Nur der Wertebereich der Spalte "Wertedetails für Position": the
/// value of CAV+ZV4 DE7110 holds the product's format conditions. False for a
/// product without a Wertedetail.
pub fn wertedetails_value_fits(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
    let Some(value) = ctx.resolved_value else {
        return ConditionResult::Unknown;
    };
    of_product(ctx, edition, |p| match &p.wertedetails {
        Some(w) => w.fits(value),
        None => ConditionResult::False,
    })
}

/// `[180]` Wenn der Produkt-Code in der Spalte "Wertedetails für Position"
/// eine Angabe hat, deren ggf. enthaltene Bedingung erfüllt ist. The
/// Bedingungen there are the value's formats, which `[162]` beside it judges;
/// here, that the product has a Wertedetail.
pub fn has_wertedetails(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
    of_product(ctx, edition, |p| {
        ConditionResult::from(p.wertedetails.is_some())
    })
}

/// The product of the SG8 SEQ+Z79 the field sits in: its PIA+5 DE7140 beside
/// DE7143 `Z11`. Without a scope, the message's one such product; `None` when
/// there is none, or several without a scope to choose.
fn anmeldung_product(ctx: &EvaluationContext) -> Option<String> {
    let mut codes: Vec<String> = ctx
        .scoped_find_segments_in("SG8", "PIA")
        .into_iter()
        .filter(|s| s.get_element(0) == "5" && s.get_component(1, 1) == "Z11")
        .filter_map(|s| s.elements.get(1)?.first().cloned())
        .filter(|code| !code.is_empty())
        .collect();
    codes.dedup();
    match codes.as_slice() {
        [one] => Some(one.clone()),
        _ => None,
    }
}

/// The Produkteigenschaft of the same SG8 (SG10 CAV+ZH9 DE7110), if one is
/// sent.
fn anmeldung_property(ctx: &EvaluationContext) -> Option<String> {
    ctx.scoped_find_segments_in("SG8", "CAV")
        .into_iter()
        .find(|s| s.get_component(0, 0) == "ZH9")
        .map(|s| s.get_component(0, 3).to_string())
        .filter(|code| !code.is_empty())
}

/// What `answer` makes of the SG8's product in chapter 6.1: Unknown when there
/// is none, False for a product the chapter does not list.
fn of_anmeldung_product(
    ctx: &EvaluationContext,
    edition: Edition,
    answer: impl Fn(&Product) -> ConditionResult,
) -> ConditionResult {
    let Some(code) = anmeldung_product(ctx) else {
        return ConditionResult::Unknown;
    };
    match AbrechnungsdatenProdukte::anmeldung().product(edition, &code) {
        Some(product) => answer(product),
        None => ConditionResult::False,
    }
}

/// UTILMD `[292]` Es sind nur die Produkt-Codes erlaubt, die in Kapitel 6.1
/// mit dem Prüfidentifikator des Anwendungsfalls gekennzeichnet sind: judged
/// on the element's own value (PIA+5 DE7140), else on the SG8's product. A
/// Prüfidentifikator no row names (FV2610 55003, the Ablehnung naming the
/// products of the Anmeldung it refuses) may name any product of the chapter.
pub fn anmeldung_product_allowed(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
    let code = match ctx.resolved_value {
        Some(value) => value.to_string(),
        None => match anmeldung_product(ctx) {
            Some(code) => code,
            None => return ConditionResult::Unknown,
        },
    };
    let table = AbrechnungsdatenProdukte::anmeldung().products(edition);
    let pid = ctx.pruefidentifikator;
    let named = table.iter().any(|p| p.pids.iter().any(|q| q == pid));
    ConditionResult::from(
        table
            .iter()
            .any(|p| p.code == code && (!named || p.orderable_by(pid))),
    )
}

/// UTILMD `[36]` Wenn der Produkt-Code derselben SG8 in Kapitel 6.1 in der
/// Spalte "Code der Produkteigenschaft" mit einem Code befüllt ist.
pub fn anmeldung_has_property_codes(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
    of_anmeldung_product(ctx, edition, |p| {
        ConditionResult::from(!p.property_codes.is_empty())
    })
}

/// UTILMD `[38]` Nur die Codes der Produkteigenschaft des Produkts derselben
/// SG8: the value of CAV+ZH9 DE7110 is one of them.
pub fn anmeldung_property_code_allowed(
    ctx: &EvaluationContext,
    edition: Edition,
) -> ConditionResult {
    let Some(value) = ctx.resolved_value else {
        return ConditionResult::Unknown;
    };
    of_anmeldung_product(ctx, edition, |p| {
        ConditionResult::from(p.property_codes.iter().any(|c| c.code == value))
    })
}

/// UTILMD `[39]` Wenn der Produkt-Code derselben SG8 in Kapitel 6.1 in der
/// Spalte "Wertedetails für Position" eine Angabe hat, deren ggf. enthaltene
/// Bedingung erfüllt ist: that the product has a Wertedetail, and where it is
/// needed only beside a Produkteigenschaft (Tranchengröße), that the SG8's
/// CAV+ZH9 names it.
pub fn anmeldung_has_wertedetails(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
    let property = anmeldung_property(ctx);
    of_anmeldung_product(ctx, edition, |p| {
        ConditionResult::from(
            p.wertedetails
                .as_ref()
                .is_some_and(|w| w.needed_beside(property.as_deref())),
        )
    })
}

/// UTILMD `[40]` Nur der Wertebereich der Spalte "Wertedetails für Position":
/// the value of CAV+ZV4 DE7110 holds the formats of the product's Wertedetail
/// beside the SG8's Produkteigenschaft. False for a product without one.
pub fn anmeldung_wertedetails_value_fits(
    ctx: &EvaluationContext,
    edition: Edition,
) -> ConditionResult {
    let Some(value) = ctx.resolved_value else {
        return ConditionResult::Unknown;
    };
    let property = anmeldung_property(ctx);
    of_anmeldung_product(ctx, edition, |p| match &p.wertedetails {
        Some(w) => w.fits_beside(value, property.as_deref()),
        None => ConditionResult::False,
    })
}

/// Values that satisfy the ORDERS `condition` of `fv` for some product of
/// chapter 6.2 — every product code for `[158]`, every Produkteigenschaft for
/// `[160]` — for a sample generator to offer an element the condition judges.
/// Empty for any other condition or message type.
pub fn candidate_values(message_type: &str, fv: &str, condition: u32) -> Vec<String> {
    let Some(edition) = Edition::of_format_version(fv).filter(|_| message_type == "ORDERS") else {
        return Vec::new();
    };
    let products = AbrechnungsdatenProdukte::embedded().products(edition);
    match condition {
        158 => products.iter().map(|p| p.code.clone()).collect(),
        160 => products
            .iter()
            .flat_map(|p| p.property_codes.iter().map(|c| c.code.clone()))
            .collect(),
        _ => Vec::new(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn both_editions_load() {
        let table = AbrechnungsdatenProdukte::embedded();
        assert_eq!(table.products(Edition::V1_3c).len(), 9);
        assert_eq!(table.products(Edition::V1_4).len(), 10);
        assert!(table.product(Edition::V1_3c, "9991000003246").is_none());
        let arbeit = table.product(Edition::V1_4, "9991000003246").expect("1.4");
        assert_eq!(arbeit.name, "spezifische Arbeit");
    }

    #[test]
    fn wertedetails_hold_their_formats() {
        let table = AbrechnungsdatenProdukte::embedded();
        let w = |code: &str| {
            table
                .product(Edition::V1_4, code)
                .and_then(|p| p.wertedetails.as_ref())
                .expect("a Wertedetail")
        };
        let bilanzkreis = w("9991000002553");
        assert_eq!(bilanzkreis.fits("11XLIEFERANT2--E"), ConditionResult::True);
        assert_eq!(
            bilanzkreis.fits("11XLIEFERANT2--E-TOO-LONG"),
            ConditionResult::False
        );
        let prognose = w("9991000002561");
        assert_eq!(prognose.fits("4000"), ConditionResult::True);
        assert_eq!(prognose.fits("4000.5"), ConditionResult::False);
        assert_eq!(prognose.fits("-1"), ConditionResult::False);
        assert_eq!(prognose.fits("11XLIEFERANT2--E"), ConditionResult::False);
        let arbeit = w("9991000003246");
        assert_eq!(arbeit.fits("1.125"), ConditionResult::True);
        assert_eq!(arbeit.fits("1.1255"), ConditionResult::False);
        assert_eq!(arbeit.fits("-0.5"), ConditionResult::False);
    }

    #[test]
    fn anmeldung_products_name_their_pids() {
        let table = AbrechnungsdatenProdukte::anmeldung();
        assert_eq!(table.products(Edition::V1_3c).len(), 18);
        assert_eq!(table.products(Edition::V1_4).len(), 26);
        let bilanzkreis = table
            .product(Edition::V1_3c, "9991000002082")
            .expect("Bilanzkreis");
        assert_eq!(bilanzkreis.chapter.as_deref(), Some("6.1.1"));
        assert!(bilanzkreis.property_codes.is_empty());
        assert!(bilanzkreis.orderable_by("55014"));
        // 55014 may order the Bilanzkreis and nothing else.
        let for_55014: Vec<&str> = table
            .products(Edition::V1_3c)
            .iter()
            .filter(|p| p.orderable_by("55014"))
            .map(|p| p.code.as_str())
            .collect();
        assert_eq!(for_55014, ["9991000002082"]);
        let w = bilanzkreis.wertedetails.as_ref().expect("a Wertedetail");
        assert_eq!(w.example_value(None), Some(EXAMPLE_BILANZKREIS));
        let verbrauchsart = table
            .product(Edition::V1_3c, "9991000002727")
            .expect("Verbrauchsart");
        assert_eq!(verbrauchsart.max_repetitions, Some(5));
        assert!(verbrauchsart.wertedetails.is_none());
    }

    #[test]
    fn tranchengroesse_takes_its_value_beside_its_property() {
        let table = AbrechnungsdatenProdukte::anmeldung();
        let w = |edition| {
            table
                .product(edition, "9991000002090")
                .and_then(|p| p.wertedetails.as_ref())
                .expect("Tranchengröße")
        };
        let share = Some("9991000003014");
        let factor = Some("9991000003022");
        let tr = Some("9991000003220");
        let old = w(Edition::V1_3c);
        assert!(old.needed_beside(share));
        assert!(!old.needed_beside(factor));
        assert_eq!(old.fits_beside("50.25", share), ConditionResult::True);
        assert_eq!(old.fits_beside("100", share), ConditionResult::False);
        assert_eq!(old.fits_beside("0", share), ConditionResult::False);
        assert_eq!(old.fits_beside("50.125", share), ConditionResult::False);
        assert_eq!(old.example_value(share), Some("50"));
        let new = w(Edition::V1_4);
        assert!(new.needed_beside(tr));
        assert_eq!(new.example_value(tr), Some("D1234567890"));
        assert_eq!(new.fits_beside("50", tr), ConditionResult::False);
        assert_eq!(new.fits_beside("D1234567890", None), ConditionResult::True);
    }

    #[test]
    fn example_bilanzkreis_is_a_valid_eic() {
        assert!(is_eic(EXAMPLE_BILANZKREIS));
        assert!(is_eic("11XLIEFERANT2--E"));
        assert!(is_eic("10YDE-RWENET---I"));
        assert!(!is_eic("10YDE-RWENET---J"));
        assert!(!is_eic("DKWP98GBANI7"));
    }
}