use crate::{ast::RangeCase, Dynamic, INT};
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Switch {
pub cases: Vec<SwitchCase>,
pub ranges: Vec<SwitchRange>,
pub default: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SwitchCase {
pub hash: u64,
pub target: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SwitchRange {
pub from: INT,
pub to: INT,
pub inclusive: bool,
pub target: u32,
}
impl SwitchRange {
#[must_use]
pub fn contains(&self, value: &Dynamic) -> bool {
let case: RangeCase = if self.inclusive {
(self.from..=self.to).into()
} else {
(self.from..self.to).into()
};
case.contains(value)
}
}
impl Switch {
#[must_use]
pub fn dispatch(&self, subject: &Dynamic) -> u32 {
if !subject.is_hashable() {
return self.default;
}
let hash = hash_of(subject);
if let Some(case) = self.cases.iter().find(|case| case.hash == hash) {
return case.target;
}
if let Some(range) = self.ranges.iter().find(|r| r.contains(subject)) {
return range.target;
}
self.default
}
}
#[must_use]
pub fn probe() -> u64 {
hash_of(&Dynamic::from("rhaigrain switch probe"))
}
fn hash_of(value: &Dynamic) -> u64 {
use core::hash::{Hash, Hasher};
let mut hasher = crate::func::get_hasher();
value.hash(&mut hasher);
hasher.finish()
}
#[cfg(test)]
fn case_hash(value: &Dynamic) -> Option<u64> {
value.is_hashable().then(|| hash_of(value))
}
#[cfg(test)]
mod tests {
use super::*;
fn int(value: INT) -> Dynamic {
Dynamic::from(value)
}
#[derive(Debug, Clone)]
struct Opaque;
fn table(cases: &[(&Dynamic, u32)], ranges: Vec<SwitchRange>, default: u32) -> Switch {
Switch {
cases: cases
.iter()
.filter_map(|(value, target)| {
Some(SwitchCase {
hash: case_hash(value)?,
target: *target,
})
})
.collect(),
ranges,
default,
}
}
#[test]
fn a_matching_case_wins() {
let (one, two) = (int(1), int(2));
let table = table(&[(&one, 10), (&two, 20)], Vec::new(), 99);
assert_eq!(table.dispatch(&one), 10);
assert_eq!(table.dispatch(&two), 20);
assert_eq!(table.dispatch(&int(3)), 99);
}
#[cfg(not(feature = "no_float"))]
#[test]
fn a_float_does_not_match_an_integer_case() {
let one = int(1);
let table = table(&[(&one, 10)], Vec::new(), 99);
let float = Dynamic::from(1.0 as crate::FLOAT);
assert!(float.is_hashable(), "this test needs a hashable float");
assert_eq!(table.dispatch(&float), 99);
}
#[test]
fn strings_and_characters_match_by_value() {
let (text, ch, flag) = (
Dynamic::from("hello"),
Dynamic::from('x'),
Dynamic::from(true),
);
let table = table(&[(&text, 10), (&ch, 20), (&flag, 30)], Vec::new(), 99);
assert_eq!(table.dispatch(&Dynamic::from("hello")), 10);
assert_eq!(table.dispatch(&Dynamic::from("other")), 99);
assert_eq!(table.dispatch(&ch), 20);
assert_eq!(table.dispatch(&flag), 30);
}
#[test]
#[cfg(not(feature = "no_float"))]
fn a_range_catches_a_float_between_its_bounds() {
let table = table(
&[],
vec![SwitchRange {
from: 0,
to: 10,
inclusive: false,
target: 20,
}],
99,
);
assert_eq!(table.dispatch(&Dynamic::from(5.5 as rhai::FLOAT)), 20);
assert_eq!(
table.dispatch(&Dynamic::from(10.0 as rhai::FLOAT)),
99,
"exclusive end"
);
assert_eq!(table.dispatch(&Dynamic::from(-0.5 as rhai::FLOAT)), 99);
}
#[test]
fn a_range_catches_what_no_case_did() {
let one = int(1);
let table = table(
&[(&one, 10)],
vec![
SwitchRange {
from: 5,
to: 8,
inclusive: false,
target: 20,
},
SwitchRange {
from: 8,
to: 10,
inclusive: true,
target: 30,
},
],
99,
);
assert_eq!(table.dispatch(&one), 10, "a case still wins");
assert_eq!(table.dispatch(&int(5)), 20);
assert_eq!(table.dispatch(&int(7)), 20);
assert_eq!(table.dispatch(&int(8)), 30, "exclusive end");
assert_eq!(table.dispatch(&int(10)), 30, "inclusive end");
assert_eq!(table.dispatch(&int(11)), 99);
}
#[test]
fn a_non_hashable_subject_falls_through_rather_than_panicking() {
let one = int(1);
let table = table(&[(&one, 10)], Vec::new(), 99);
let non_hashable = Dynamic::from(Opaque);
assert!(
!non_hashable.is_hashable(),
"this test needs an non-hashable value",
);
assert_eq!(table.dispatch(&non_hashable), 99);
}
#[test]
fn a_non_hashable_case_has_no_hash_to_key_on() {
assert_eq!(case_hash(&Dynamic::from(Opaque)), None);
assert!(case_hash(&int(1)).is_some());
}
#[test]
fn the_probe_is_stable_within_a_process() {
assert_eq!(probe(), probe());
assert_ne!(probe(), 0, "a probe of zero could not be told from absent");
}
}