use serde::Deserialize;
use std::collections::HashMap;
use std::sync::OnceLock;
pub use super::abrechnungsdaten::Edition;
const EMBEDDED_JSON: &str = include_str!("../../data/konfigurationen_produkte.json");
#[derive(Debug, Clone, Deserialize)]
pub struct Product {
pub code: String,
pub name: String,
}
#[derive(Debug, Deserialize)]
struct Chapter {
title: String,
products: Vec<Product>,
}
#[derive(Debug, Deserialize)]
struct RawVersion {
chapters: HashMap<String, Chapter>,
}
#[derive(Debug, Deserialize)]
struct RawFile {
versions: HashMap<String, RawVersion>,
}
fn editions() -> &'static HashMap<String, RawVersion> {
static CELL: OnceLock<HashMap<String, RawVersion>> = OnceLock::new();
CELL.get_or_init(|| {
serde_json::from_str::<RawFile>(EMBEDDED_JSON)
.expect("embedded konfigurationen_produkte.json is malformed")
.versions
})
}
fn edition_key(edition: Edition) -> &'static str {
match edition {
Edition::V1_3c => "1.3c",
Edition::V1_4 => "1.4",
}
}
pub fn products(edition: Edition, chapter: &str) -> Vec<&'static Product> {
let Some(version) = editions().get(edition_key(edition)) else {
return Vec::new();
};
let prefix = format!("{chapter}.");
let mut numbers: Vec<&String> = version
.chapters
.keys()
.filter(|n| *n == chapter || n.starts_with(&prefix))
.collect();
numbers.sort_by_key(|n| {
n.split('.')
.map(|p| p.parse::<u32>().unwrap_or(0))
.collect::<Vec<_>>()
});
let mut out: Vec<&'static Product> = Vec::new();
for n in numbers {
for p in &version.chapters[n].products {
if !out.iter().any(|o| o.code == p.code) {
out.push(p);
}
}
}
out
}
pub fn title(edition: Edition, chapter: &str) -> Option<&'static str> {
editions()
.get(edition_key(edition))?
.chapters
.get(chapter)
.map(|c| c.title.as_str())
}
pub fn chapters_named(text: &str) -> Vec<String> {
if !text.contains("Codeliste der Konfigurationen") {
return Vec::new();
}
let mut out: Vec<String> = Vec::new();
for (i, _) in text.match_indices("Kapitel") {
let rest = text[i + "Kapitel".len()..].trim_start();
let number: String = rest
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect();
let number = number.trim_end_matches('.');
if !number.is_empty() && !out.iter().any(|n| n == number) {
out.push(number.to_string());
}
}
out
}
pub fn codes_named_by(fv: &str, text: &str) -> Vec<&'static str> {
let Some(edition) = Edition::of_format_version(fv) else {
return Vec::new();
};
let mut out: Vec<&'static str> = Vec::new();
for chapter in chapters_named(text) {
for p in products(edition, &chapter) {
if !out.contains(&p.code.as_str()) {
out.push(&p.code);
}
}
}
out
}
pub fn chapter_of(edition: Edition, code: &str) -> Option<&'static str> {
let version = editions().get(edition_key(edition))?;
version
.chapters
.iter()
.filter(|(_, c)| c.products.iter().any(|p| p.code == code))
.map(|(n, _)| n.as_str())
.max_by_key(|n| n.matches('.').count())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_chapter_holds_its_subchapters() {
let marktlokation = products(Edition::V1_4, "2.1");
let mit = products(Edition::V1_4, "2.1.1");
let ohne = products(Edition::V1_4, "2.1.2");
assert_eq!(marktlokation.len(), mit.len() + ohne.len());
assert!(!mit.is_empty() && !ohne.is_empty());
}
#[test]
fn esa_backend_and_smgw_are_apart() {
let backend = products(Edition::V1_4, "4.6.1");
let smgw = products(Edition::V1_4, "4.6.2");
assert!(backend.iter().any(|p| p.code == "9991000000416"));
assert!(smgw.iter().any(|p| p.code == "9991000000432"));
assert!(!backend
.iter()
.any(|p| smgw.iter().any(|s| s.code == p.code)));
assert!(backend.iter().any(|p| p.code == "9991000001539"));
}
#[test]
fn chapter_4_5_is_empty() {
assert!(products(Edition::V1_3c, "4.5").is_empty());
assert!(products(Edition::V1_4, "4.5").is_empty());
assert!(title(Edition::V1_4, "4.5").is_some());
}
#[test]
fn condition_texts_name_their_chapters() {
let text = "Es sind nur die Messprodukte erlaubt, die in der Codeliste der \
Konfigurationen im Kapitel 4.6.1 \"Werte nach Typ 2 aus Backend\" enthalten sind.";
assert_eq!(chapters_named(text), ["4.6.1"]);
let gas = "Messprodukt-Code aus Kapitel 3 \"Codeliste der Standard-Messprodukte Gas\" \
der Codeliste der Konfigurationen";
assert_eq!(chapters_named(gas), ["3"]);
assert!(chapters_named("Kapitel 4.6.1 der Codeliste der Artikelnummern").is_empty());
assert_eq!(codes_named_by("FV2610", gas).len(), 13);
assert!(codes_named_by("FV2310", gas).is_empty());
}
#[test]
fn a_code_knows_its_chapter() {
assert_eq!(chapter_of(Edition::V1_4, "9991000000713"), Some("4.1"));
assert_eq!(chapter_of(Edition::V1_4, "9991000000416"), Some("4.6.1"));
assert_eq!(chapter_of(Edition::V1_4, "1234567890123"), None);
}
}