use concept_graph::attributes::{Row, ValueRef};
use roaring::RoaringBitmap;
use super::EvalError;
use crate::ast::{ActiveValue, Cardinality, Comparison, ConcreteString, Equality, TimeValue};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Kinds {
Any,
These(Vec<u32>),
}
impl Kinds {
pub(super) fn contains(&self, kind: u32) -> bool {
match self {
Self::Any => true,
Self::These(kinds) => kinds.contains(&kind),
}
}
pub(super) fn iter(&self, total: usize) -> Box<dyn Iterator<Item = u32> + '_> {
match self {
Self::Any => Box::new(0..u32::try_from(total).unwrap_or(u32::MAX)),
Self::These(kinds) => Box::new(kinds.iter().copied()),
}
}
}
#[derive(Debug)]
pub(super) enum ValueTest {
Concept {
set: Option<RoaringBitmap>,
negated: bool,
},
Number(Comparison, f64),
Text(Equality, Vec<ConcreteString>),
Boolean(Equality, bool),
}
impl ValueTest {
pub(super) fn matches(&self, value: ValueRef<'_>) -> Result<bool, EvalError> {
Ok(match (self, value) {
(Self::Concept { set: None, negated }, _) => !*negated,
(
Self::Concept {
set: Some(set),
negated,
},
ValueRef::Concept(target),
) => set.contains(target.index()) != *negated,
(Self::Number(operator, expected), ValueRef::Number(text)) => {
compare_numbers(*operator, number(text)?, *expected)
}
(Self::Text(operator, values), ValueRef::String(text)) => {
let hit = values.iter().any(|value| value.0 == text);
hit == (*operator == Equality::Equal)
}
(Self::Boolean(operator, expected), ValueRef::String(text)) => {
let actual = text.eq_ignore_ascii_case("true");
let boolean = actual || text.eq_ignore_ascii_case("false");
boolean && ((actual == *expected) == (*operator == Equality::Equal))
}
_ => false,
})
}
}
pub(super) fn compare_numbers(operator: Comparison, actual: f64, expected: f64) -> bool {
match operator {
Comparison::Equal => (actual - expected).abs() < f64::EPSILON,
Comparison::NotEqual => (actual - expected).abs() >= f64::EPSILON,
Comparison::Less => actual < expected,
Comparison::LessOrEqual => actual <= expected,
Comparison::Greater => actual > expected,
Comparison::GreaterOrEqual => actual >= expected,
}
}
fn compare_times(operator: Comparison, actual: u32, expected: u32) -> bool {
match operator {
Comparison::Equal => actual == expected,
Comparison::NotEqual => actual != expected,
Comparison::Less => actual < expected,
Comparison::LessOrEqual => actual <= expected,
Comparison::Greater => actual > expected,
Comparison::GreaterOrEqual => actual >= expected,
}
}
pub(super) fn within(cardinality: Option<Cardinality>, count: u32) -> bool {
let Cardinality { min, max } = cardinality.unwrap_or(Cardinality { min: 1, max: None });
count >= min && max.is_none_or(|max| count <= max)
}
pub(super) enum Activity {
Is(bool),
Either,
Neither,
}
pub(super) fn activity(operator: Equality, value: ActiveValue) -> Activity {
let equal = operator == Equality::Equal;
match value {
ActiveValue::Active => Activity::Is(equal),
ActiveValue::Inactive => Activity::Is(!equal),
ActiveValue::Any if equal => Activity::Either,
ActiveValue::Any => Activity::Neither,
}
}
pub(super) fn times_match(operator: Comparison, expected: &[u32], actual: u32) -> bool {
match operator {
Comparison::NotEqual => expected.iter().all(|&e| actual != e),
_ => expected.iter().any(|&e| compare_times(operator, actual, e)),
}
}
pub(super) fn number(value: &str) -> Result<f64, EvalError> {
value.parse().map_err(|source| EvalError::InvalidNumber {
value: value.to_owned(),
source,
})
}
pub(super) fn time(value: &str) -> Result<u32, EvalError> {
if value.is_empty() {
return Err(EvalError::Unsupported("an empty time value"));
}
value.parse().map_err(|source| EvalError::InvalidTime {
value: value.to_owned(),
source,
})
}
pub(super) fn times(values: &[TimeValue]) -> Result<Vec<u32>, EvalError> {
values.iter().map(|v| time(&v.0)).collect()
}
#[derive(Debug)]
pub(super) struct AttributeTest {
pub(super) kinds: Kinds,
pub(super) value: ValueTest,
pub(super) cardinality: Option<Cardinality>,
}
impl AttributeTest {
pub(super) fn matches(&self, row: &Row<'_>) -> Result<bool, EvalError> {
Ok(self.kinds.contains(row.kind) && self.value.matches(row.value)?)
}
}
#[derive(Debug)]
pub(super) enum CompiledSet {
Single(Box<CompiledItem>),
Conjunction(Vec<CompiledItem>),
Disjunction(Vec<CompiledItem>),
}
#[derive(Debug)]
pub(super) enum CompiledItem {
Attribute(AttributeTest),
Nested(CompiledSet),
}
pub(super) fn relationship_groups<'row, 'graph>(
rows: &'row [Row<'graph>],
) -> Vec<Vec<&'row Row<'graph>>> {
let mut groups: Vec<Vec<&Row<'_>>> = Vec::new();
let mut current: Option<u32> = None;
for row in rows {
if row.group == 0 || current != Some(row.group) {
groups.push(Vec::new());
current = Some(row.group);
}
if let Some(last) = groups.last_mut() {
last.push(row);
}
}
groups
}
pub(super) fn set_holds(set: &CompiledSet, rows: &[&Row<'_>]) -> Result<bool, EvalError> {
match set {
CompiledSet::Single(item) => item_holds(item, rows),
CompiledSet::Conjunction(items) => {
for item in items {
if !item_holds(item, rows)? {
return Ok(false);
}
}
Ok(true)
}
CompiledSet::Disjunction(items) => {
for item in items {
if item_holds(item, rows)? {
return Ok(true);
}
}
Ok(false)
}
}
}
fn item_holds(item: &CompiledItem, rows: &[&Row<'_>]) -> Result<bool, EvalError> {
match item {
CompiledItem::Nested(set) => set_holds(set, rows),
CompiledItem::Attribute(test) => {
let count = matching_rows(test, rows.iter().copied())?;
Ok(within(test.cardinality, count))
}
}
}
pub(super) fn matching_rows<'r, 'g: 'r>(
test: &AttributeTest,
rows: impl Iterator<Item = &'r Row<'g>>,
) -> Result<u32, EvalError> {
let mut count = 0_u32;
for row in rows {
if test.matches(row)? {
count = count.saturating_add(1);
}
}
Ok(count)
}
#[cfg(test)]
mod tests {
use concept_graph::attributes::ValueRef;
use super::{Cardinality, ValueTest, compare_numbers, number, time, within};
use crate::ast::{Comparison, ConcreteString, Equality, TypedSearchTerm};
use crate::eval::EvalError;
use crate::eval::{term_matches, wild_matches};
#[test]
fn a_concrete_string_matches_the_whole_value_with_its_case() {
let panadol =
|operator| ValueTest::Text(operator, vec![ConcreteString(String::from("PANADOL"))]);
assert!(
panadol(Equality::Equal)
.matches(ValueRef::String("PANADOL"))
.expect("compares")
);
assert!(
!panadol(Equality::Equal)
.matches(ValueRef::String("panadol"))
.expect("compares")
);
assert!(
!panadol(Equality::Equal)
.matches(ValueRef::String("PANADOL EXTRA"))
.expect("compares")
);
assert!(
!panadol(Equality::Equal)
.matches(ValueRef::String("PANA"))
.expect("compares")
);
assert!(
panadol(Equality::NotEqual)
.matches(ValueRef::String("PANADOL EXTRA"))
.expect("compares")
);
assert!(
!panadol(Equality::NotEqual)
.matches(ValueRef::String("PANADOL"))
.expect("compares")
);
let star = ValueTest::Text(Equality::Equal, vec![ConcreteString(String::from("a*"))]);
assert!(
star.matches(ValueRef::String("a*")).expect("compares"),
"an asterisk is itself"
);
assert!(
!star.matches(ValueRef::String("ab")).expect("compares"),
"not a wildcard"
);
let set = ValueTest::Text(
Equality::Equal,
vec![
ConcreteString(String::from("A")),
ConcreteString(String::from("B")),
],
);
assert!(
set.matches(ValueRef::String("B")).expect("compares"),
"any string of the set"
);
assert!(
!panadol(Equality::Equal)
.matches(ValueRef::Number("5"))
.expect("compares")
);
}
#[test]
fn cardinalities_patterns_and_words_match_as_the_specification_says() {
assert!(within(None, 1) && within(None, 5) && !within(None, 0));
assert!(within(
Some(Cardinality {
min: 0,
max: Some(0)
}),
0
));
assert!(!within(
Some(Cardinality {
min: 0,
max: Some(0)
}),
1
));
assert!(within(Some(Cardinality { min: 2, max: None }), 2));
assert!(wild_matches("cardi*opathy", "Cardiomyopathy"));
assert!(!wild_matches("cardi*opathy", "Cardiomyopathy X"));
assert!(wild_matches("*itis", "Bronchitis"));
assert!(wild_matches("a\\*b", "A*B"));
assert!(!wild_matches("a\\*b", "AXB"));
let term = TypedSearchTerm::Match(vec![String::from("hea"), String::from("att")]);
assert!(term_matches(&term, "Heart attack"));
assert!(!term_matches(&term, "Heart"));
assert!(compare_numbers(Comparison::GreaterOrEqual, 500.0, 500.0));
assert!(!compare_numbers(Comparison::Less, 500.0, 500.0));
}
#[test]
fn a_defective_number_or_time_is_an_error() {
assert_eq!(number("12.5"), Ok(12.5));
assert!(
matches!(number("12,5"), Err(EvalError::InvalidNumber { value, .. }) if value == "12,5")
);
assert_eq!(time("20260331"), Ok(20_260_331));
assert!(
matches!(time("2026-03-31"), Err(EvalError::InvalidTime { value, .. }) if value == "2026-03-31")
);
assert!(
matches!(time("-1"), Err(EvalError::InvalidTime { .. })),
"a negative stored time"
);
assert_eq!(time(""), Err(EvalError::Unsupported("an empty time value")));
let five = ValueTest::Number(Comparison::Equal, 5.0);
assert_eq!(five.matches(ValueRef::Number("5")), Ok(true));
assert!(matches!(
five.matches(ValueRef::Number("five")),
Err(EvalError::InvalidNumber { .. })
));
}
}