use yo_common::num::DIGITS_MAX;
use crate::elem::Elements;
use crate::listpack::Entry;
use crate::set::Set;
use crate::zset::Zset;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Aggregate {
#[default]
Sum,
Min,
Max,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Op {
Union,
Inter,
Diff,
}
#[derive(Debug, Clone, Copy)]
pub enum Operand<'a> {
Zset(&'a Zset),
Set(&'a Set),
Missing,
}
impl Operand<'_> {
#[must_use]
pub fn len(&self) -> usize {
match self {
Operand::Zset(z) => z.len(),
Operand::Set(s) => s.len(),
Operand::Missing => 0,
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
fn score(&self, member: &[u8]) -> Option<f64> {
match self {
Operand::Zset(z) => z.score(member),
Operand::Set(s) => s.contains(member).then_some(1.0),
Operand::Missing => None,
}
}
fn walk<F: FnMut(&[u8], f64)>(&self, mut f: F) {
let mut digits = [0u8; DIGITS_MAX];
match self {
Operand::Zset(z) => z.walk(0, z.len(), false, |m, s| f(bytes(m, &mut digits), s)),
Operand::Set(s) => {
for m in s.iter() {
f(bytes(m, &mut digits), 1.0);
}
}
Operand::Missing => {}
}
}
}
#[inline]
fn bytes<'a>(m: Entry<'a>, digits: &'a mut [u8; DIGITS_MAX]) -> &'a [u8] {
match m {
Entry::Str(s) => s,
Entry::Int(n) => yo_common::num::i64_digits(digits, n),
}
}
#[inline]
fn weighted(score: f64, weight: f64) -> f64 {
let v = score * weight;
if v.is_nan() { 0.0 } else { v }
}
#[inline]
fn fold(now: f64, next: f64, agg: Aggregate) -> f64 {
match agg {
Aggregate::Sum => {
let v = now + next;
if v.is_nan() { 0.0 } else { v }
}
Aggregate::Min => {
if next < now {
next
} else {
now
}
}
Aggregate::Max => {
if next > now {
next
} else {
now
}
}
}
}
#[must_use]
pub fn gather(op: Op, inputs: &[Operand<'_>], weights: &[f64], agg: Aggregate) -> Elements<f64> {
let weight = |i: usize| weights.get(i).copied().unwrap_or(1.0);
match op {
Op::Union => {
let mut out = Elements::with_capacity(hint(inputs, op));
for (i, input) in inputs.iter().enumerate() {
let w = weight(i);
input.walk(|member, score| {
let v = weighted(score, w);
match out.get_mut(member) {
Some(now) => *now = fold(*now, v, agg),
None => {
let _ = out.insert(member, v);
}
}
});
}
out
}
Op::Inter => {
let Some(small) = (0..inputs.len()).min_by_key(|&i| inputs[i].len()) else {
return Elements::with_capacity(0);
};
let mut out = Elements::with_capacity(inputs[small].len().clamp(16, 1 << 16));
if inputs.iter().any(Operand::is_empty) {
return out;
}
inputs[small].walk(|member, score| {
let mut total = weighted(score, weight(small));
for (i, other) in inputs.iter().enumerate() {
if i == small {
continue;
}
let Some(s) = other.score(member) else { return };
total = fold(total, weighted(s, weight(i)), agg);
}
let _ = out.insert(member, total);
});
out
}
Op::Diff => {
let Some((first, rest)) = inputs.split_first() else {
return Elements::with_capacity(0);
};
let mut out = Elements::with_capacity(first.len().clamp(16, 1 << 16));
first.walk(|member, score| {
if rest.iter().any(|o| o.score(member).is_some()) {
return;
}
let _ = out.insert(member, score);
});
out
}
}
}
#[must_use]
pub fn intercard(inputs: &[Operand<'_>], limit: usize) -> usize {
if inputs.is_empty() || inputs.iter().any(Operand::is_empty) {
return 0;
}
let small = (0..inputs.len())
.min_by_key(|&i| inputs[i].len())
.expect("not empty");
let stop = if limit == 0 { usize::MAX } else { limit };
let mut found = 0;
inputs[small].walk(|member, _| {
if found >= stop {
return;
}
if inputs
.iter()
.enumerate()
.all(|(i, o)| i == small || o.score(member).is_some())
{
found += 1;
}
});
found
}
fn hint(inputs: &[Operand<'_>], op: Op) -> usize {
let total: usize = match op {
Op::Union => inputs.iter().map(Operand::len).sum(),
_ => inputs.first().map_or(0, Operand::len),
};
total.clamp(16, 1 << 20)
}
#[cfg(test)]
mod tests {
use core::cmp::Ordering;
use super::*;
use crate::set::Limits as SetLimits;
use crate::zset::Limits;
fn cmp_key(a: (f64, &[u8]), b: (f64, &[u8])) -> Ordering {
match a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal) {
Ordering::Equal => a.1.cmp(b.1),
other => other,
}
}
fn zs(pairs: &[(&str, f64)]) -> Zset {
let mut z = Zset::new();
for (m, s) in pairs {
z.add(m.as_bytes(), *s, &Limits::DEFAULT);
}
z
}
fn plain(members: &[&str]) -> Set {
let mut s = Set::new();
for m in members {
s.add(m.as_bytes(), &SetLimits::DEFAULT);
}
s
}
fn ordered(got: Elements<f64>) -> Vec<(String, f64)> {
let mut out: Vec<(String, f64)> = (0..got.len())
.map(|i| got.at(i).expect("in range"))
.map(|(n, s)| (String::from_utf8(n.to_vec()).unwrap(), *s))
.collect();
out.sort_by(|a, b| cmp_key((a.1, a.0.as_bytes()), (b.1, b.0.as_bytes())));
out
}
fn named(got: Vec<(String, f64)>) -> Vec<String> {
got.into_iter().map(|(m, _)| m).collect()
}
#[test]
fn a_union_adds_the_scores_of_a_member_in_both() {
let a = zs(&[("x", 1.0), ("y", 2.0)]);
let b = zs(&[("y", 3.0), ("z", 4.0)]);
let got = ordered(gather(
Op::Union,
&[Operand::Zset(&a), Operand::Zset(&b)],
&[],
Aggregate::Sum,
));
assert_eq!(
got,
[("x".into(), 1.0), ("z".into(), 4.0), ("y".into(), 5.0)]
);
}
#[test]
fn min_and_max_keep_one_score_rather_than_adding_them() {
let a = zs(&[("y", 2.0)]);
let b = zs(&[("y", 7.0)]);
let ops = [Operand::Zset(&a), Operand::Zset(&b)];
assert_eq!(
ordered(gather(Op::Union, &ops, &[], Aggregate::Min)),
[("y".to_string(), 2.0)]
);
assert_eq!(
ordered(gather(Op::Union, &ops, &[], Aggregate::Max)),
[("y".to_string(), 7.0)]
);
}
#[test]
fn weights_multiply_before_anything_is_aggregated() {
let a = zs(&[("x", 1.0), ("y", 2.0)]);
let b = zs(&[("y", 3.0)]);
let ops = [Operand::Zset(&a), Operand::Zset(&b)];
let got = ordered(gather(Op::Union, &ops, &[2.0, 10.0], Aggregate::Sum));
assert_eq!(got, [("x".into(), 2.0), ("y".into(), 34.0)]);
let got = ordered(gather(Op::Union, &ops, &[2.0, 0.5], Aggregate::Min));
assert_eq!(got, [("y".into(), 1.5), ("x".into(), 2.0)]);
}
#[test]
fn an_intersection_only_keeps_what_every_input_has() {
let a = zs(&[("x", 1.0), ("y", 2.0), ("z", 3.0)]);
let b = zs(&[("y", 10.0), ("z", 20.0)]);
let c = zs(&[("z", 100.0)]);
let ops = [Operand::Zset(&a), Operand::Zset(&b), Operand::Zset(&c)];
assert_eq!(
ordered(gather(Op::Inter, &ops, &[], Aggregate::Sum)),
[("z".to_string(), 123.0)]
);
assert_eq!(intercard(&ops, 0), 1);
let ops = [Operand::Zset(&a), Operand::Missing];
assert!(ordered(gather(Op::Inter, &ops, &[], Aggregate::Sum)).is_empty());
assert_eq!(intercard(&ops, 0), 0);
}
#[test]
fn a_difference_keeps_the_first_input_scores() {
let a = zs(&[("x", 1.0), ("y", 2.0), ("z", 3.0)]);
let b = zs(&[("y", 99.0)]);
let ops = [Operand::Zset(&a), Operand::Zset(&b)];
assert_eq!(
ordered(gather(Op::Diff, &ops, &[], Aggregate::Sum)),
[("x".into(), 1.0), ("z".into(), 3.0)]
);
assert!(
ordered(gather(
Op::Diff,
&[Operand::Missing, Operand::Zset(&a)],
&[],
Aggregate::Sum
))
.is_empty()
);
let ops = [Operand::Zset(&a), Operand::Missing];
assert_eq!(
named(ordered(gather(Op::Diff, &ops, &[], Aggregate::Sum))),
["x", "y", "z"]
);
}
#[test]
fn a_plain_set_counts_as_a_sorted_set_where_every_score_is_one() {
let a = zs(&[("x", 5.0), ("y", 6.0)]);
let b = plain(&["y", "z"]);
let ops = [Operand::Zset(&a), Operand::Set(&b)];
let got = ordered(gather(Op::Union, &ops, &[], Aggregate::Sum));
assert_eq!(
got,
[("z".into(), 1.0), ("x".into(), 5.0), ("y".into(), 7.0)]
);
assert_eq!(
ordered(gather(Op::Inter, &ops, &[], Aggregate::Sum)),
[("y".to_string(), 7.0)]
);
assert_eq!(
named(ordered(gather(Op::Diff, &ops, &[], Aggregate::Sum))),
["x"]
);
}
#[test]
fn an_integer_member_crosses_between_a_set_and_a_sorted_set() {
let a = zs(&[("17", 5.0), ("42", 6.0)]);
let b = plain(&["42", "99"]);
assert_eq!(b.encoding().name(), "intset");
let ops = [Operand::Zset(&a), Operand::Set(&b)];
assert_eq!(
ordered(gather(Op::Inter, &ops, &[], Aggregate::Sum)),
[("42".to_string(), 7.0)]
);
assert_eq!(intercard(&ops, 0), 1);
assert_eq!(
named(ordered(gather(Op::Union, &ops, &[], Aggregate::Sum))),
["99", "17", "42"]
);
}
#[test]
fn a_score_that_would_be_a_nan_becomes_a_zero() {
let a = zs(&[("x", f64::INFINITY)]);
let ops = [Operand::Zset(&a)];
assert_eq!(
ordered(gather(Op::Union, &ops, &[0.0], Aggregate::Sum)),
[("x".to_string(), 0.0)]
);
let b = zs(&[("x", f64::NEG_INFINITY)]);
let ops = [Operand::Zset(&a), Operand::Zset(&b)];
assert_eq!(
ordered(gather(Op::Union, &ops, &[], Aggregate::Sum)),
[("x".to_string(), 0.0)]
);
}
#[test]
fn a_limit_stops_a_cardinality_count_where_it_was_told_to() {
let a = zs(&[("a", 1.0), ("b", 1.0), ("c", 1.0), ("d", 1.0)]);
let b = zs(&[("a", 1.0), ("b", 1.0), ("c", 1.0), ("d", 1.0)]);
let ops = [Operand::Zset(&a), Operand::Zset(&b)];
assert_eq!(intercard(&ops, 0), 4);
assert_eq!(intercard(&ops, 2), 2);
assert_eq!(intercard(&ops, 99), 4);
assert_eq!(intercard(&[], 0), 0);
}
#[test]
fn a_union_of_thousands_agrees_with_the_slow_way_of_working_it_out() {
let one: Vec<(String, f64)> = (0..3_000)
.map(|i| (format!("m{i:05}"), f64::from(i)))
.collect();
let two: Vec<(String, f64)> = (1_500..4_500)
.map(|i| (format!("m{i:05}"), f64::from(i) * 2.0))
.collect();
let mut a = Zset::new();
for (m, s) in &one {
a.add(m.as_bytes(), *s, &Limits::DEFAULT);
}
let mut b = Zset::new();
for (m, s) in &two {
b.add(m.as_bytes(), *s, &Limits::DEFAULT);
}
let ops = [Operand::Zset(&a), Operand::Zset(&b)];
let mut want: std::collections::BTreeMap<String, f64> = std::collections::BTreeMap::new();
for (m, s) in one.iter().chain(two.iter()) {
*want.entry(m.clone()).or_insert(0.0) += s;
}
let mut want: Vec<(String, f64)> = want.into_iter().collect();
want.sort_by(|x, y| cmp_key((x.1, x.0.as_bytes()), (y.1, y.0.as_bytes())));
assert_eq!(ordered(gather(Op::Union, &ops, &[], Aggregate::Sum)), want);
assert_eq!(intercard(&ops, 0), 1_500);
}
}