use super::matching::{compare_numbers, number, time, times, times_match};
use super::{EvalError, Evaluator, Model, REFERENCED_COMPONENT, term_matches};
use crate::ast::{
ActiveValue, ConceptSet, Equality, FieldValue, FocusConcept, MemberFilter, RefsetFields, Sctid,
};
use concept_graph::ordinal::Ordinal;
use concept_graph::refsets::{Table, ValueRef as FieldRef};
use roaring::RoaringBitmap;
impl<M: Model> Evaluator<'_, M> {
pub(super) fn member_of(
&self,
set: &RoaringBitmap,
fields: Option<&RefsetFields>,
filter_groups: &[Vec<MemberFilter>],
) -> Result<RoaringBitmap, EvalError> {
let mut out = RoaringBitmap::new();
for concept in set {
let Some(id) = self.model.sctid(Ordinal::new(concept))? else {
continue;
};
let Some(table) = self.model.members().table(id.0) else {
return Err(EvalError::NotAReferenceSet(id));
};
out |= self.table_members(id, table, fields, filter_groups)?;
}
Ok(out)
}
pub(super) fn containing_any(
&self,
set: &RoaringBitmap,
filter_groups: &[Vec<MemberFilter>],
) -> Result<RoaringBitmap, EvalError> {
let members = self.model.members();
let mut out = RoaringBitmap::new();
for refset in members.refsets() {
let Some(table) = members.table(refset) else {
continue;
};
if table.members().is_disjoint(set) {
continue;
}
let mut contains = filter_groups.is_empty();
for row in 0..table.len() {
if contains {
break;
}
let referenced = table.concept(row).is_some_and(|c| set.contains(c.index()));
contains = referenced && self.row_passes(table, row, filter_groups)?;
}
if contains && let Some(ordinal) = self.model.concept(Sctid(refset))? {
out.insert(ordinal.index());
}
}
Ok(out)
}
pub(super) fn table_members(
&self,
refset: Sctid,
table: &Table,
fields: Option<&RefsetFields>,
filter_groups: &[Vec<MemberFilter>],
) -> Result<RoaringBitmap, EvalError> {
let columns = Self::selected_columns(refset, table, fields)?;
let mut out = RoaringBitmap::new();
for row in 0..table.len() {
if !self.row_passes(table, row, filter_groups)? {
continue;
}
for column in &columns {
if let Some(value) = Self::column_concept(table, row, *column) {
out.insert(value.index());
}
}
}
Ok(out)
}
pub(super) fn column_concept(
table: &Table,
row: usize,
column: Option<usize>,
) -> Option<Ordinal> {
match column {
None => table.concept(row),
Some(field) => match table.value(row, field) {
Some(FieldRef::Concept(value)) => Some(value),
_ => None,
},
}
}
pub(super) fn selected_columns(
refset: Sctid,
table: &Table,
fields: Option<&RefsetFields>,
) -> Result<Vec<Option<usize>>, EvalError> {
Ok(match fields {
None => vec![None],
Some(RefsetFields::Any) => {
let mut columns = vec![None];
columns.extend(
table
.kinds()
.iter()
.enumerate()
.filter(|(_, kind)| **kind == concept_graph::refsets::FieldKind::Component)
.map(|(i, _)| Some(i)),
);
columns
}
Some(RefsetFields::Names(names)) => {
let mut columns = Vec::new();
for name in names {
if name.eq_ignore_ascii_case(REFERENCED_COMPONENT) {
columns.push(None);
} else {
columns.push(Some(table.field(name).ok_or_else(|| {
EvalError::UnknownField {
refset,
field: name.clone(),
}
})?));
}
}
columns
}
})
}
pub(super) fn row_passes(
&self,
table: &Table,
row: usize,
filter_groups: &[Vec<MemberFilter>],
) -> Result<bool, EvalError> {
for filter in filter_groups.iter().flatten() {
let passes = match filter {
MemberFilter::Active { operator, value } => match (operator, value) {
(Equality::Equal, ActiveValue::Active)
| (Equality::NotEqual, ActiveValue::Inactive) => true,
(Equality::NotEqual, ActiveValue::Any) => false,
_ => {
return Err(EvalError::Unsupported(
"a member filter on inactive members: the tables hold active members",
));
}
},
MemberFilter::EffectiveTime { operator, values } => {
let actual = table.effective_time(row).ok_or_else(|| {
EvalError::Storage(format!("reference set row {row} has no effective time"))
})?;
times_match(*operator, ×(values)?, actual)
}
MemberFilter::Module { operator, value } => {
let modules = self.sctids_of(value)?;
let inside = table.module(row).is_some_and(|m| modules.contains(&m));
inside == (*operator == Equality::Equal)
}
MemberFilter::Field { name, value } => {
let Some(column) = table.field(name) else {
return Ok(false);
};
self.field_passes(table.value(row, column), value)?
}
};
if !passes {
return Ok(false);
}
}
Ok(true)
}
pub(super) fn field_passes(
&self,
actual: Option<FieldRef<'_>>,
expected: &FieldValue,
) -> Result<bool, EvalError> {
Ok(match (expected, actual) {
(FieldValue::Expression { operator, value }, Some(FieldRef::Concept(concept))) => {
let set = self.sub(value)?;
set.contains(concept.index()) == (*operator == Equality::Equal)
}
(FieldValue::Expression { operator, .. }, _) => *operator == Equality::NotEqual,
(FieldValue::Numeric { operator, value }, Some(FieldRef::Integer(actual))) => {
let expected = number(&value.0)?;
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "reference set integers are small"
)]
let actual = actual as f64;
compare_numbers(*operator, actual, expected)
}
(FieldValue::String { operator, terms }, Some(FieldRef::String(text))) => {
terms.iter().any(|t| term_matches(t, text)) == (*operator == Equality::Equal)
}
(FieldValue::Boolean { operator, value }, Some(FieldRef::String(text))) => {
let actual = text.eq_ignore_ascii_case("true");
(actual == *value) == (*operator == Equality::Equal)
}
(FieldValue::Time { operator, values }, Some(FieldRef::String(text))) => {
times_match(*operator, ×(values)?, time(text)?)
}
(FieldValue::Time { operator, values }, Some(FieldRef::Integer(actual))) => {
times_match(*operator, ×(values)?, time(&actual.to_string())?)
}
_ => false,
})
}
pub(super) fn sctids_of(&self, set: &ConceptSet) -> Result<Vec<u64>, EvalError> {
match set {
ConceptSet::Set(references) => Ok(references.iter().map(|r| r.id.0).collect()),
ConceptSet::Expression(sub)
if sub.operator.is_none()
&& sub.refset_operator.is_none()
&& sub.filters.is_empty()
&& sub.member_filters.is_empty()
&& sub.history.is_none() =>
{
match &sub.focus {
FocusConcept::Reference(reference) => Ok(vec![reference.id.0]),
_ => self.sctids_of_set(&self.sub(sub)?),
}
}
ConceptSet::Expression(sub) => self.sctids_of_set(&self.sub(sub)?),
}
}
pub(super) fn sctids_of_set(&self, set: &RoaringBitmap) -> Result<Vec<u64>, EvalError> {
let mut ids = Vec::new();
for concept in set {
if let Some(id) = self.model.sctid(Ordinal::new(concept))? {
ids.push(id.0);
}
}
Ok(ids)
}
}