use std::collections::HashMap;
use linkrs_core::Value;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum IntersectKey {
Int(i64),
Str(String),
Other(String),
}
impl From<&Value> for IntersectKey {
fn from(value: &Value) -> Self {
match value {
Value::SmallInt(v) => Self::Int(*v as i64),
Value::Int(v) => Self::Int(*v as i64),
Value::BigInt(v) => Self::Int(*v),
Value::String(s) => Self::Str(s.to_string()),
Value::FixedString(s) => Self::Str(s.clone()),
Value::Bool(v) => Self::Other(format!("bool:{v}")),
other => Self::Other(format!("{other:?}")),
}
}
}
#[derive(Debug, Default)]
pub struct IntersectBuild {
bound_col: usize,
intersect_col: usize,
table: HashMap<IntersectKey, Vec<Vec<Value>>>,
sealed: bool,
}
impl IntersectBuild {
pub fn new(bound_col: usize, intersect_col: usize) -> Self {
Self {
bound_col,
intersect_col,
table: HashMap::new(),
sealed: false,
}
}
pub fn bound_col(&self) -> usize {
self.bound_col
}
pub fn intersect_col(&self) -> usize {
self.intersect_col
}
pub fn append_row(&mut self, row: Vec<Value>) {
let width = self.bound_col.max(self.intersect_col) + 1;
if row.len() < width {
return;
}
debug_assert!(!self.sealed, "appended after finish; resort needed");
let key = IntersectKey::from(&row[self.bound_col]);
self.table.entry(key).or_default().push(row);
self.sealed = false;
}
pub fn append_rows(&mut self, rows: &[Vec<Value>]) {
for row in rows {
self.append_row(row.clone());
}
}
pub fn finish(&mut self) {
for rows in self.table.values_mut() {
rows.sort_by(|a, b| {
IntersectKey::from(&a[self.intersect_col])
.cmp(&IntersectKey::from(&b[self.intersect_col]))
});
}
self.sealed = true;
}
pub fn lookup(&self, bound: &Value) -> &[Vec<Value>] {
let key = IntersectKey::from(bound);
self.table.get(&key).map(Vec::as_slice).unwrap_or(&[])
}
pub fn num_keys(&self) -> usize {
self.table.len()
}
pub fn num_rows(&self) -> usize {
self.table.values().map(Vec::len).sum()
}
pub fn is_sealed(&self) -> bool {
self.sealed
}
pub fn clear(&mut self) {
self.table.clear();
self.sealed = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build() -> IntersectBuild {
let mut b = IntersectBuild::new(0, 1);
b.append_rows(&[
vec![Value::Int(1), Value::Int(20)],
vec![Value::Int(1), Value::Int(10)],
vec![Value::Int(2), Value::Int(30)],
]);
b.finish();
b
}
#[test]
fn finish_sorts_adjacency_by_intersect_column() {
let b = build();
let rows = b.lookup(&Value::Int(1));
assert_eq!(rows.len(), 2);
assert_eq!(rows[0][1], Value::Int(10));
assert_eq!(rows[1][1], Value::Int(20));
}
#[test]
fn missing_bound_yields_empty_slice() {
let b = build();
assert!(b.lookup(&Value::Int(99)).is_empty());
}
#[test]
fn narrow_rows_are_skipped() {
let mut b = IntersectBuild::new(0, 1);
b.append_row(vec![Value::Int(1)]);
b.append_row(vec![Value::Int(1), Value::Int(5)]);
b.finish();
assert_eq!(b.num_rows(), 1);
}
#[test]
fn key_ordering_groups_integers_before_strings() {
assert!(IntersectKey::Int(1) < IntersectKey::Int(2));
assert!(IntersectKey::Str("a".to_string()) < IntersectKey::Str("b".to_string()));
}
}