use super::context::EvaluationContext;
use super::evaluator::ConditionResult;
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::OnceLock;
const EMBEDDED_JSON: &str = include_str!("../../data/ebd_cluster_map.json");
#[derive(Debug, Deserialize)]
struct RawFile {
ebds: HashMap<String, HashMap<String, String>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Cluster {
Zustimmung,
Ablehnung,
AblehnungKopfebene,
AblehnungPositionsebene,
AblehnungSummenebene,
Other(String),
}
impl Cluster {
pub fn from_token(token: &str) -> Self {
match token {
"Zustimmung" => Cluster::Zustimmung,
"Ablehnung" => Cluster::Ablehnung,
"Ablehnung auf Kopfebene" => Cluster::AblehnungKopfebene,
"Ablehnung auf Positionsebene" => Cluster::AblehnungPositionsebene,
"Ablehnung auf Summenebene" => Cluster::AblehnungSummenebene,
_ => Cluster::Other(token.to_owned()),
}
}
pub fn is_zustimmung(&self) -> bool {
matches!(self, Cluster::Zustimmung)
}
pub fn is_ablehnung(&self) -> bool {
matches!(
self,
Cluster::Ablehnung
| Cluster::AblehnungKopfebene
| Cluster::AblehnungPositionsebene
| Cluster::AblehnungSummenebene
)
}
pub fn is_ablehnung_kopfebene(&self) -> bool {
matches!(self, Cluster::AblehnungKopfebene)
}
pub fn is_ablehnung_positionsebene(&self) -> bool {
matches!(self, Cluster::AblehnungPositionsebene)
}
pub fn is_ablehnung_summenebene(&self) -> bool {
matches!(self, Cluster::AblehnungSummenebene)
}
}
#[derive(Debug)]
pub struct EbdClusterLookup {
clusters: HashMap<(String, String), Cluster>,
}
impl EbdClusterLookup {
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
let raw: RawFile = serde_json::from_str(json)?;
let clusters = raw
.ebds
.into_iter()
.flat_map(|(ebd, codes)| {
codes
.into_iter()
.map(move |(code, token)| ((ebd.clone(), code), Cluster::from_token(&token)))
})
.collect();
Ok(Self { clusters })
}
pub fn embedded() -> &'static Self {
static CELL: OnceLock<EbdClusterLookup> = OnceLock::new();
CELL.get_or_init(|| {
EbdClusterLookup::from_json(EMBEDDED_JSON)
.expect("embedded ebd_cluster_map.json is malformed")
})
}
pub fn cluster(&self, ebd: &str, code: &str) -> Option<&Cluster> {
self.clusters.get(&(ebd.to_owned(), code.to_owned()))
}
}
fn sts_e01_code_ebd(seg: &mig_types::segment::OwnedSegment) -> Option<(&str, &str)> {
let c556 = seg.elements.get(2)?;
let code = c556.first().filter(|v| !v.is_empty())?.as_str();
let ebd = c556.get(1).filter(|v| !v.is_empty())?.as_str();
Some((code, ebd))
}
fn ajt_code_ebd(seg: &mig_types::segment::OwnedSegment) -> Option<(&str, &str)> {
let code = seg
.elements
.first()?
.first()
.filter(|v| !v.is_empty())?
.as_str();
let ebd = seg
.elements
.get(1)?
.first()
.filter(|v| !v.is_empty())?
.as_str();
Some((code, ebd))
}
pub fn all_sts_e01_in_cluster(
ctx: &EvaluationContext,
predicate: impl Fn(&Cluster) -> bool,
) -> ConditionResult {
let sts_segs = ctx.find_segments_with_qualifier("STS", 0, "E01");
if sts_segs.is_empty() {
return ConditionResult::Unknown;
}
let all = sts_segs.iter().all(|s| {
let Some((code, ebd)) = sts_e01_code_ebd(s) else {
return false;
};
ctx.ebd_clusters.cluster(ebd, code).is_some_and(&predicate)
});
ConditionResult::from(all)
}
pub fn ebd_ablehnung_except(
ctx: &EvaluationContext,
required_ebd: &str,
excluded_codes: &[&str],
) -> ConditionResult {
let sts_segs = ctx.find_segments_with_qualifier("STS", 0, "E01");
if sts_segs.is_empty() {
return ConditionResult::Unknown;
}
let all = sts_segs.iter().all(|s| {
let Some((code, ebd)) = sts_e01_code_ebd(s) else {
return false;
};
ebd == required_ebd
&& !excluded_codes.contains(&code)
&& ctx
.ebd_clusters
.cluster(ebd, code)
.is_some_and(Cluster::is_ablehnung)
});
ConditionResult::from(all)
}
pub fn any_sts_e01_in_cluster(
ctx: &EvaluationContext,
predicate: impl Fn(&Cluster) -> bool,
) -> ConditionResult {
let sts_segs = ctx.find_segments_with_qualifier("STS", 0, "E01");
let found = sts_segs.iter().any(|s| {
sts_e01_code_ebd(s)
.is_some_and(|(code, ebd)| ctx.ebd_clusters.cluster(ebd, code).is_some_and(&predicate))
});
ConditionResult::from(found)
}
pub fn resolved_sts_code_in_cluster(
ctx: &EvaluationContext,
predicate: impl Fn(&Cluster) -> bool,
) -> ConditionResult {
let (Some(code), Some(segment)) = (ctx.resolved_value, ctx.resolved_segment) else {
return ConditionResult::Unknown;
};
if code.is_empty() {
return ConditionResult::Unknown;
}
let ebd = segment
.get(2)
.and_then(|e| e.get(1))
.map(|s| s.as_str())
.filter(|s| !s.is_empty());
let Some(ebd) = ebd else {
return ConditionResult::Unknown;
};
ctx.ebd_clusters
.cluster(ebd, code)
.map(|c| ConditionResult::from(predicate(c)))
.unwrap_or(ConditionResult::Unknown)
}
pub fn resolved_ajt_code_in_cluster(
ctx: &EvaluationContext,
predicate: impl Fn(&Cluster) -> bool,
) -> ConditionResult {
let (Some(code), Some(segment)) = (ctx.resolved_value, ctx.resolved_segment) else {
return ConditionResult::Unknown;
};
if code.is_empty() {
return ConditionResult::Unknown;
}
let ebd = segment
.get(1)
.and_then(|e| e.first())
.map(|s| s.as_str())
.filter(|s| !s.is_empty());
let Some(ebd) = ebd else {
return ConditionResult::Unknown;
};
ctx.ebd_clusters
.cluster(ebd, code)
.map(|c| ConditionResult::from(predicate(c)))
.unwrap_or(ConditionResult::Unknown)
}
pub fn any_scoped_ajt_in_cluster(
ctx: &EvaluationContext,
excluded_codes: &[&str],
predicate: impl Fn(&Cluster) -> bool,
) -> ConditionResult {
let ajt_segs = ctx.scoped_find_segments("AJT");
let found = ajt_segs.iter().any(|s| {
ajt_code_ebd(s).is_some_and(|(code, ebd)| {
!excluded_codes.contains(&code)
&& ctx.ebd_clusters.cluster(ebd, code).is_some_and(&predicate)
})
});
ConditionResult::from(found)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a36_in_e0624_is_zustimmung() {
let lookup = EbdClusterLookup::embedded();
assert_eq!(lookup.cluster("E_0624", "A36"), Some(&Cluster::Zustimmung));
}
#[test]
fn a30_in_e0624_is_ablehnung() {
let lookup = EbdClusterLookup::embedded();
assert_eq!(lookup.cluster("E_0624", "A30"), Some(&Cluster::Ablehnung));
}
#[test]
fn unknown_code_returns_none() {
let lookup = EbdClusterLookup::embedded();
assert_eq!(lookup.cluster("E_0624", "Z99"), None);
}
#[test]
fn unknown_ebd_returns_none() {
let lookup = EbdClusterLookup::embedded();
assert_eq!(lookup.cluster("E_9999", "A01"), None);
}
#[test]
fn same_code_can_differ_across_ebds() {
let lookup = EbdClusterLookup::embedded();
assert!(lookup.cluster("E_0014", "A01").is_some());
assert!(lookup.cluster("E_0049", "A01").is_some());
}
#[test]
fn korrekturliste_wegen_ablehnung_is_preserved_as_other() {
let lookup = EbdClusterLookup::embedded();
match lookup.cluster("E_0014", "A04") {
Some(Cluster::Other(s)) => assert_eq!(s, "Korrekturliste wegen Ablehnung"),
other => {
panic!("expected Cluster::Other(\"Korrekturliste wegen Ablehnung\"), got {other:?}")
}
}
}
#[test]
fn sub_level_ablehnung_variants_still_count_as_ablehnung() {
assert!(Cluster::Ablehnung.is_ablehnung());
assert!(Cluster::AblehnungKopfebene.is_ablehnung());
assert!(Cluster::AblehnungPositionsebene.is_ablehnung());
assert!(Cluster::AblehnungSummenebene.is_ablehnung());
assert!(!Cluster::Zustimmung.is_ablehnung());
assert!(!Cluster::Other("Abweisung".into()).is_ablehnung());
}
}