use super::*;
#[derive(Debug, Clone)]
pub(crate) enum OverrideKeyError {
Unknown(String),
Ambiguous {
key: String,
candidates: Vec<String>,
},
Collision {
name: String,
keys: Vec<String>,
},
}
pub(crate) fn dotted_suffixes(name: &str) -> impl Iterator<Item = &str> {
let mut rest = name;
std::iter::from_fn(move || {
let (_, tail) = rest.split_once('.')?;
rest = tail;
Some(tail)
})
}
pub(crate) fn namespace_scope<'a>(
names: impl IntoIterator<Item = &'a str>,
extra: impl IntoIterator<Item = &'a str>,
) -> HashSet<String> {
let mut out: HashSet<String> = extra.into_iter().map(|s| s.to_string()).collect();
for n in names {
let mut rest = n;
while let Some((head, tail)) = rest.split_once('.') {
out.insert(head.to_string());
rest = tail;
}
}
out
}
pub(crate) trait KnownNames {
fn contains(&self, key: &str) -> bool;
fn suffix_owners(&self, key: &str) -> Vec<String>;
}
impl KnownNames for HashMap<String, usize> {
fn contains(&self, key: &str) -> bool {
self.contains_key(key)
}
fn suffix_owners(&self, key: &str) -> Vec<String> {
self.keys()
.filter(|n| is_dotted_suffix(n, key))
.cloned()
.collect()
}
}
pub(crate) fn is_dotted_suffix(name: &str, key: &str) -> bool {
name.len() > key.len() && dotted_suffixes(name).any(|s| s == key)
}
pub(crate) fn canonicalize_override_keys<K: KnownNames + ?Sized>(
known: &K,
namespaces: &HashSet<String>,
overrides: &HashMap<String, f64>,
renames: &HashMap<String, String>,
) -> Result<HashMap<String, f64>, OverrideKeyError> {
if overrides.is_empty() {
return Ok(HashMap::new());
}
let resolved;
let overrides = if renames.is_empty() {
overrides
} else {
resolved = resolve_merged_renames(overrides, renames);
&resolved
};
let mut out: HashMap<String, f64> = HashMap::new();
let mut exact: HashSet<&str> = HashSet::new();
let mut claims: HashMap<String, Vec<(&str, f64)>> = HashMap::new();
let mut failures: Vec<OverrideKeyError> = Vec::new();
for (k, v) in overrides {
if known.contains(k) {
exact.insert(k.as_str()); out.insert(k.clone(), *v);
continue;
}
let name: String = if let Some(suffix) = dotted_suffix_hit(known, namespaces, k) {
suffix.to_string() } else {
let mut cands = known.suffix_owners(k);
match cands.len() {
0 => {
failures.push(OverrideKeyError::Unknown(k.clone())); continue;
}
1 => cands.pop().expect("one candidate"), _ => {
cands.sort();
failures.push(OverrideKeyError::Ambiguous {
key: k.clone(),
candidates: cands,
}); continue;
}
}
};
claims.entry(name).or_default().push((k.as_str(), *v));
}
for (name, mut cs) in claims {
if exact.contains(name.as_str()) {
continue; }
if cs.len() > 1 {
let mut keys: Vec<String> = cs.iter().map(|(k, _)| (*k).to_string()).collect();
keys.sort();
failures.push(OverrideKeyError::Collision { name, keys });
continue;
}
out.insert(name, cs.pop().expect("one claim").1);
}
if !failures.is_empty() {
failures.sort_by(|a, b| override_key_of(a).cmp(override_key_of(b)));
return Err(failures.swap_remove(0));
}
Ok(out)
}
fn dotted_suffix_hit<'k, K: KnownNames + ?Sized>(
known: &K,
namespaces: &HashSet<String>,
k: &'k str,
) -> Option<&'k str> {
let mut rest = k;
while let Some((head, tail)) = rest.split_once('.') {
if !namespaces.contains(head) {
return None;
}
if known.contains(tail) {
return Some(tail);
}
rest = tail;
}
None
}
pub(crate) fn resolve_merged_renames(
overrides: &HashMap<String, f64>,
renames: &HashMap<String, String>,
) -> HashMap<String, f64> {
let mut out = HashMap::with_capacity(overrides.len());
for (k, v) in overrides {
match renames.get(k.as_str()) {
Some(survivor) if !overrides.contains_key(survivor.as_str()) => {
out.insert(survivor.clone(), *v);
}
Some(_) => {}
None => {
out.insert(k.clone(), *v);
}
}
}
out
}
fn override_key_of(e: &OverrideKeyError) -> &str {
match e {
OverrideKeyError::Unknown(k) => k,
OverrideKeyError::Ambiguous { key, .. } => key,
OverrideKeyError::Collision { keys, .. } => keys.first().map(String::as_str).unwrap_or(""),
}
}
pub(crate) fn param_key_error(e: OverrideKeyError) -> SimulateError {
match e {
OverrideKeyError::Unknown(name) => SimulateError::InvalidParameter { name },
OverrideKeyError::Ambiguous { key, candidates } => SimulateError::AmbiguousParameter {
name: key,
candidates,
},
OverrideKeyError::Collision { name, keys } => {
SimulateError::CollidingParameterKeys { name, keys }
}
}
}
pub(crate) fn ic_key_error(e: OverrideKeyError) -> SimulateError {
match e {
OverrideKeyError::Unknown(name) => SimulateError::InvalidInitialCondition { name },
OverrideKeyError::Ambiguous { key, candidates } => {
SimulateError::AmbiguousInitialCondition {
name: key,
candidates,
}
}
OverrideKeyError::Collision { name, keys } => {
SimulateError::CollidingInitialConditionKeys { name, keys }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn no_renames() -> HashMap<String, String> {
HashMap::new()
}
fn known(names: &[&str]) -> HashMap<String, usize> {
names
.iter()
.enumerate()
.map(|(i, n)| (n.to_string(), i))
.collect()
}
#[test]
fn dotted_suffix_binds_the_longest_known_suffix() {
let k = known(&["sub.g", "g", "Left.solo"]);
let ns = namespace_scope(k.keys().map(String::as_str), ["P"]);
assert_eq!(dotted_suffix_hit(&k, &ns, "P.sub.g"), Some("sub.g"));
assert_eq!(dotted_suffix_hit(&k, &ns, "P.g"), Some("g"));
assert_eq!(dotted_suffix_hit(&k, &ns, "Left.solo"), None);
assert_eq!(dotted_suffix_hit(&k, &ns, "Missing.solo"), None);
assert_eq!(dotted_suffix_hit(&k, &ns, "g"), None);
let over: HashMap<String, f64> = [("P.sub.g".to_string(), 1.5)].into_iter().collect();
let out = canonicalize_override_keys(&k, &ns, &over, &no_renames()).expect("resolves");
assert_eq!(out.get("sub.g"), Some(&1.5));
let bad: HashMap<String, f64> = [("Missing.solo".to_string(), 1.0)].into_iter().collect();
assert!(matches!(
canonicalize_override_keys(&k, &ns, &bad, &no_renames()),
Err(OverrideKeyError::Unknown(ref n)) if n == "Missing.solo"
));
}
#[test]
fn rule_3_binds_a_dotted_suffix_of_exactly_one_name() {
let k = known(&["P.sub.g", "P.x0"]);
let ns = namespace_scope(k.keys().map(String::as_str), []);
for key in ["P.sub.g", "sub.g", "g"] {
let over: HashMap<String, f64> = [(key.to_string(), 1.5)].into_iter().collect();
let out = canonicalize_override_keys(&k, &ns, &over, &no_renames())
.unwrap_or_else(|e| panic!("'{key}' must resolve, got {e:?}"));
assert_eq!(out.get("P.sub.g"), Some(&1.5), "key '{key}'");
}
for key in ["Missing.solo", "Missing.g", "x.sub.g"] {
let bad: HashMap<String, f64> = [(key.to_string(), 1.0)].into_iter().collect();
assert!(
matches!(
canonicalize_override_keys(&k, &ns, &bad, &no_renames()),
Err(OverrideKeyError::Unknown(ref n)) if n == key
),
"key '{key}' must be unknown"
);
}
let two = known(&["Left.sub.g", "Right.sub.g"]);
let ns2 = namespace_scope(two.keys().map(String::as_str), []);
for key in ["sub.g", "g"] {
match canonicalize_override_keys(
&two,
&ns2,
&[(key.to_string(), 1.5)].into_iter().collect(),
&no_renames(),
) {
Err(OverrideKeyError::Ambiguous {
key: got,
candidates,
}) => {
assert_eq!(got, key);
assert_eq!(candidates, vec!["Left.sub.g", "Right.sub.g"]);
}
other => panic!("expected ambiguous for '{key}', got {other:?}"),
}
}
let over: HashMap<String, f64> = [("sub.g".to_string(), 1.5), ("g".to_string(), 2.5)]
.into_iter()
.collect();
match canonicalize_override_keys(&k, &ns, &over, &no_renames()) {
Err(OverrideKeyError::Collision { name, keys }) => {
assert_eq!(name, "P.sub.g");
assert_eq!(keys, vec!["g", "sub.g"]);
}
other => panic!("expected a collision, got {other:?}"),
}
}
#[test]
fn rule_2_rejects_a_leading_segment_that_names_nothing() {
let k = known(&["Left.gain", "Left.solo", "Right.gain"]);
let ns = namespace_scope(k.keys().map(String::as_str), []);
assert!(ns.contains("Left") && ns.contains("Right") && !ns.contains("Doc"));
let bad: HashMap<String, f64> = [("Doc.Left.solo".to_string(), 9.0)].into_iter().collect();
assert!(matches!(
canonicalize_override_keys(&k, &ns, &bad, &no_renames()),
Err(OverrideKeyError::Unknown(ref n)) if n == "Doc.Left.solo"
));
let sub = known(&["sub.g"]);
let real = namespace_scope(sub.keys().map(String::as_str), ["P"]);
let over: HashMap<String, f64> = [("P.sub.g".to_string(), 1.5)].into_iter().collect();
assert_eq!(
canonicalize_override_keys(&sub, &real, &over, &no_renames())
.expect("resolves")
.get("sub.g"),
Some(&1.5)
);
}
#[test]
fn two_keys_designating_one_name_are_ambiguous() {
let k = known(&["Left.solo"]);
let ns = namespace_scope(k.keys().map(String::as_str), ["A", "B", "Doc"]);
let run = |pairs: &[(&str, f64)]| {
let over: HashMap<String, f64> =
pairs.iter().map(|(n, v)| ((*n).to_string(), *v)).collect();
canonicalize_override_keys(&k, &ns, &over, &no_renames())
};
match run(&[("solo", 2.0), ("Doc.Left.solo", 9.0)]) {
Err(OverrideKeyError::Collision { name, keys }) => {
assert_eq!(name, "Left.solo");
assert_eq!(keys, vec!["Doc.Left.solo", "solo"]);
}
other => panic!("expected a collision, got {other:?}"),
}
for pairs in [
[("B.Left.solo", 2.0), ("A.Left.solo", 1.0)],
[("A.Left.solo", 1.0), ("B.Left.solo", 2.0)],
] {
match run(&pairs) {
Err(OverrideKeyError::Collision { name, keys }) => {
assert_eq!(name, "Left.solo");
assert_eq!(keys, vec!["A.Left.solo", "B.Left.solo"]);
}
other => panic!("expected a collision, got {other:?}"),
}
}
let err = param_key_error(run(&[("solo", 2.0), ("Doc.Left.solo", 9.0)]).unwrap_err());
assert_eq!(
err.to_string(),
"parameter_overrides: 2 keys designate the parameter 'Left.solo' \
(Doc.Left.solo, solo). Supply exactly one override key per name \
(esm-spec \u{a7}6.6.2)."
);
let err = ic_key_error(run(&[("solo", 2.0), ("Doc.Left.solo", 9.0)]).unwrap_err());
assert_eq!(
err.to_string(),
"initial_conditions: 2 keys designate the state 'Left.solo' \
(Doc.Left.solo, solo). Supply exactly one override key per name \
(esm-spec \u{a7}6.6.2)."
);
let out = run(&[("Left.solo", 1.0), ("Doc.Left.solo", 9.0)]).expect("exact wins");
assert_eq!(out.get("Left.solo"), Some(&1.0));
let out =
run(&[("Left.solo", 1.0), ("solo", 2.0), ("Doc.Left.solo", 9.0)]).expect("exact wins");
assert_eq!(out.get("Left.solo"), Some(&1.0));
let two = known(&["Left.solo", "Right.gain"]);
let ns2 = namespace_scope(two.keys().map(String::as_str), []);
let over: HashMap<String, f64> = [("solo".to_string(), 2.0), ("gain".to_string(), 3.0)]
.into_iter()
.collect();
let out =
canonicalize_override_keys(&two, &ns2, &over, &no_renames()).expect("both resolve");
assert_eq!(out.get("Left.solo"), Some(&2.0));
assert_eq!(out.get("Right.gain"), Some(&3.0));
}
}