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))
}
fn sts_answers(ctx: &EvaluationContext, qualifier: &str) -> Vec<(String, String)> {
let answer = |elements: &[Vec<String>]| {
let c556 = elements.get(2)?;
let code = c556.first().filter(|v| !v.is_empty())?;
Some((code.clone(), c556.get(1).cloned().unwrap_or_default()))
};
if let (Some(value), Some(segment)) = (ctx.resolved_value, ctx.resolved_segment) {
let own = segment.first().and_then(|e| e.first()).map(String::as_str);
if own == Some(qualifier) && !value.is_empty() {
let ebd = segment
.get(2)
.and_then(|c| c.get(1))
.cloned()
.unwrap_or_default();
return vec![(value.to_string(), ebd)];
}
}
ctx.scoped_find_segments_in_with_qualifier("SG4", "STS", 0, qualifier)
.iter()
.filter_map(|s| answer(&s.elements))
.collect()
}
fn every_answer(
answers: &[(String, String)],
judge: impl Fn(&str, &str) -> ConditionResult,
) -> ConditionResult {
let mut result = ConditionResult::True;
for (code, ebd) in answers {
match judge(code, ebd) {
ConditionResult::False => return ConditionResult::False,
ConditionResult::Unknown => result = ConditionResult::Unknown,
ConditionResult::True => {}
}
}
result
}
pub fn all_sts_e01_in_cluster(
ctx: &EvaluationContext,
predicate: impl Fn(&Cluster) -> bool,
) -> ConditionResult {
every_answer(&sts_answers(ctx, "E01"), |code, ebd| {
if ebd.is_empty() {
return ConditionResult::Unknown;
}
ConditionResult::from(ctx.ebd_clusters.cluster(ebd, code).is_some_and(&predicate))
})
}
pub fn ebd_ablehnung_except(
ctx: &EvaluationContext,
qualifier: &str,
required_ebd: &str,
excluded_codes: &[&str],
) -> ConditionResult {
every_answer(&sts_answers(ctx, qualifier), |code, ebd| {
let ebd = if ebd.is_empty() { required_ebd } else { ebd };
ConditionResult::from(
ebd == required_ebd
&& !excluded_codes.contains(&code)
&& ctx
.ebd_clusters
.cluster(ebd, code)
.is_some_and(Cluster::is_ablehnung),
)
})
}
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_in("SG5", "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::*;
use crate::eval::NoOpExternalProvider;
use mig_types::segment::OwnedSegment;
fn sts(qualifier: &str, code: &str, ebd: &str) -> OwnedSegment {
OwnedSegment {
id: "STS".into(),
elements: vec![
vec![qualifier.into()],
vec![],
vec![code.into(), ebd.into()],
],
segment_number: 0,
}
}
fn judge(
segments: &[OwnedSegment],
f: impl Fn(&EvaluationContext) -> ConditionResult,
) -> ConditionResult {
let external = NoOpExternalProvider;
let ctx = EvaluationContext::new("55003", &external, segments);
f(&ctx)
}
#[test]
fn only_zustimmung_answers_hold_360() {
let zustimmung = |segs: &[OwnedSegment]| {
judge(segs, |ctx| {
all_sts_e01_in_cluster(ctx, Cluster::is_zustimmung)
})
};
assert_eq!(
zustimmung(&[sts("E01", "A51", "E_0623")]),
ConditionResult::True
);
assert_eq!(
zustimmung(&[sts("E01", "A50", "E_0623")]),
ConditionResult::False
);
assert_eq!(
zustimmung(&[sts("E01", "Q99", "E_0623")]),
ConditionResult::False
);
assert_eq!(zustimmung(&[]), ConditionResult::True);
}
#[test]
fn the_third_partys_answer_is_judged_by_366_and_368() {
let e01 = sts("E01", "A50", "E_0623");
let r366 = |z35: &str| {
judge(&[e01.clone(), sts("Z35", z35, "E_0624")], |ctx| {
ebd_ablehnung_except(ctx, "Z35", "E_0624", &["A30"])
})
};
assert_eq!(r366("A32"), ConditionResult::True, "A32 is an Ablehnung");
assert_eq!(r366("A30"), ConditionResult::False, "bis auf A30");
assert_eq!(r366("A31"), ConditionResult::False, "A31 is a Zustimmung");
let r368 = |z35: &str| {
judge(&[e01.clone(), sts("Z35", z35, "E_0624")], |ctx| {
ebd_ablehnung_except(ctx, "Z35", "E_0624", &["A41"])
})
};
assert_eq!(r368("A30"), ConditionResult::True);
assert_eq!(r368("A41"), ConditionResult::False);
assert_eq!(
judge(std::slice::from_ref(&e01), |ctx| ebd_ablehnung_except(
ctx,
"Z35",
"E_0624",
&["A41"]
)),
ConditionResult::True
);
}
#[test]
fn a_resolved_answer_is_judged_alone() {
let segments = [sts("Z35", "A30", "E_0624"), sts("Z35", "A41", "E_0624")];
let external = NoOpExternalProvider;
let ctx = EvaluationContext::new("55080", &external, &segments);
let first = ctx.with_resolved(Some("A30"), Some(&segments[0].elements));
assert_eq!(
ebd_ablehnung_except(&first, "Z35", "E_0624", &["A41"]),
ConditionResult::True
);
let second = ctx.with_resolved(Some("A41"), Some(&segments[1].elements));
assert_eq!(
ebd_ablehnung_except(&second, "Z35", "E_0624", &["A41"]),
ConditionResult::False
);
}
#[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());
}
}