Skip to main content

automapper_validation/eval/
answer_lists.rs

1//! Which answer codes each EBD / Codeliste holds.
2//!
3//! A response names the list its answer comes from next to the answer: AJT
4//! 1082 beside AJT 4465, STS C556/1131 beside C556/9013 — `E_0204` (an EBD),
5//! `S_0066` (Codeliste Strom), `G_0059` (Codeliste Gas). The AHB says which
6//! lists a PID may name, not what they hold; `AJT+ZB5+G_0059` satisfies it
7//! though G_0059 holds no ZB5. The EBD document says what they hold.
8//!
9//! Data source: `machine-readable_mako-prozesse`
10//! (`<FV>/pipeline/09_ebds/answer_codes.yaml`), extracted with
11//! `scripts/extract_answer_code_lists.py`, plus the codes
12//! `ebd_cluster_map.json` knows for those EBDs. FV2504 has no dataset there
13//! and is read as FV2510.
14
15use serde::Deserialize;
16use std::collections::{HashMap, HashSet};
17use std::sync::OnceLock;
18
19const EMBEDDED_JSON: &str = include_str!("../../data/answer_code_lists.json");
20
21#[derive(Debug, Deserialize)]
22struct RawFile {
23    lists: HashMap<String, HashMap<String, Vec<String>>>,
24}
25
26/// The answer codes of every EBD and Codeliste, per format version.
27#[derive(Debug)]
28pub struct AnswerCodeLists {
29    by_fv: HashMap<String, HashMap<String, HashSet<String>>>,
30}
31
32impl AnswerCodeLists {
33    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
34        let raw: RawFile = serde_json::from_str(json)?;
35        let by_fv = raw
36            .lists
37            .into_iter()
38            .map(|(fv, lists)| {
39                let lists = lists
40                    .into_iter()
41                    .map(|(id, codes)| (id, codes.into_iter().collect()))
42                    .collect();
43                (fv, lists)
44            })
45            .collect();
46        Ok(Self { by_fv })
47    }
48
49    pub fn embedded() -> &'static Self {
50        static CELL: OnceLock<AnswerCodeLists> = OnceLock::new();
51        CELL.get_or_init(|| {
52            AnswerCodeLists::from_json(EMBEDDED_JSON)
53                .expect("embedded answer_code_lists.json is malformed")
54        })
55    }
56
57    /// The answer codes `list` holds in `fv`, or `None` when the data does
58    /// not know the list (or the format version).
59    pub fn codes(&self, fv: &str, list: &str) -> Option<&HashSet<String>> {
60        let fv = if fv == "FV2504" { "FV2510" } else { fv };
61        self.by_fv.get(fv)?.get(list)
62    }
63
64    /// Whether `list` holds `answer` in `fv`; `None` when the list is unknown.
65    pub fn holds(&self, fv: &str, list: &str, answer: &str) -> Option<bool> {
66        self.codes(fv, list).map(|codes| codes.contains(answer))
67    }
68}
69
70/// Whether `value` names an answer list: an EBD (`E_0204`) or a Codeliste
71/// (`S_0066`, `G_0059`).
72pub fn is_answer_list_id(value: &str) -> bool {
73    let bytes = value.as_bytes();
74    bytes.len() == 6
75        && matches!(bytes[0], b'E' | b'S' | b'G')
76        && bytes[1] == b'_'
77        && bytes[2..].iter().all(u8::is_ascii_digit)
78}
79
80/// For a rule at `list_path` that names the list, the path of the answer
81/// beside it: AJT `…/AJT/1082` → `…/AJT/4465`, STS `…/C556/1131` →
82/// `…/C556/9013`. `None` for any other element — a LIN's 1082 is a position
83/// number, not a list.
84pub fn answer_path(list_path: &str) -> Option<String> {
85    if let Some(prefix) = list_path.strip_suffix("AJT/1082") {
86        return Some(format!("{prefix}AJT/4465"));
87    }
88    list_path
89        .strip_suffix("C556/1131")
90        .map(|prefix| format!("{prefix}C556/9013"))
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn the_embedded_lists_name_what_the_ebd_document_says() {
99        let lists = AnswerCodeLists::embedded();
100        assert_eq!(lists.holds("FV2610", "S_0066", "ZB5"), Some(true));
101        assert_eq!(lists.holds("FV2610", "S_0066", "ZB4"), Some(false));
102        assert_eq!(lists.holds("FV2610", "S_0065", "ZB4"), Some(true));
103        // FV2504 reads FV2510.
104        assert_eq!(lists.holds("FV2504", "S_0066", "ZB5"), Some(true));
105        assert_eq!(lists.holds("FV2610", "S_9999", "ZB5"), None);
106    }
107
108    #[test]
109    fn only_ajt_and_sts_name_a_list() {
110        assert_eq!(answer_path("SG2/AJT/1082").as_deref(), Some("SG2/AJT/4465"));
111        assert_eq!(
112            answer_path("SG4/STS/C556/1131").as_deref(),
113            Some("SG4/STS/C556/9013")
114        );
115        assert_eq!(answer_path("SG26/LIN/1082"), None);
116        assert!(is_answer_list_id("E_0204") && is_answer_list_id("G_0059"));
117        assert!(!is_answer_list_id("Z07") && !is_answer_list_id("E_02041"));
118    }
119}