use crate::canonicalize::format_canonical_float;
use indexmap::IndexMap;
use std::cmp::Ordering;
type FxIndexMap<K, V> = IndexMap<K, V, rustc_hash::FxBuildHasher>;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("FloatKeyError: {0}")]
pub struct FloatKeyError(pub String);
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Key {
Int(i64),
Str(String),
Bool(bool),
Tuple(Vec<Key>),
}
impl Key {
pub fn try_from_json(value: &serde_json::Value) -> Result<Key, FloatKeyError> {
match value {
serde_json::Value::Bool(b) => Ok(Key::Bool(*b)),
serde_json::Value::String(s) => Ok(Key::Str(s.clone())),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(Key::Int(i))
} else if let Some(u) = n.as_u64() {
Err(FloatKeyError(format!(
"integer {u} out of range for a relational key (i64 domain)"
)))
} else {
Err(FloatKeyError(format!(
"float {n} forbidden in a relational key; keys must be \
integer / categorical IDs (CONFORMANCE_SPEC.md §5.5.1 \
rule 1) — normalise to an ID before the build-time \
relational pre-pass"
)))
}
}
serde_json::Value::Array(items) => {
let mut out = Vec::with_capacity(items.len());
for item in items {
out.push(Key::try_from_json(item)?);
}
Ok(Key::Tuple(out))
}
serde_json::Value::Null | serde_json::Value::Object(_) => Err(FloatKeyError(format!(
"unsupported relational key component {value}; keys must be \
integer / categorical IDs or tuples thereof"
))),
}
}
fn write_token(&self, out: &mut String) {
match self {
Key::Int(i) => out.push_str(&i.to_string()),
Key::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Key::Str(s) => {
out.push_str(&serde_json::to_string(s).expect("a string always serialises to JSON"))
}
Key::Tuple(items) => write_array(out, items, |o, k| k.write_token(o)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Num {
Int(i64),
Float(f64),
}
impl Num {
fn as_f64(self) -> f64 {
match self {
Num::Int(i) => i as f64,
Num::Float(f) => f,
}
}
fn canonical_cmp(&self, other: &Num) -> Ordering {
match (self, other) {
(Num::Int(a), Num::Int(b)) => a.cmp(b),
(Num::Float(a), Num::Float(b)) => a.total_cmp(b),
(Num::Int(_), Num::Float(_)) => Ordering::Less,
(Num::Float(_), Num::Int(_)) => Ordering::Greater,
}
}
fn write_token(&self, out: &mut String) {
match self {
Num::Int(i) => out.push_str(&i.to_string()),
Num::Float(f) => out.push_str(&format_canonical_float(*f)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SemiringOp {
Sum,
Prod,
Min,
Max,
}
impl SemiringOp {
fn apply(self, a: Num, b: Num) -> Num {
match (a, b) {
(Num::Int(x), Num::Int(y)) => Num::Int(match self {
SemiringOp::Sum => x.wrapping_add(y),
SemiringOp::Prod => x.wrapping_mul(y),
SemiringOp::Min => x.min(y),
SemiringOp::Max => x.max(y),
}),
_ => {
let prec = crate::precision::active();
let (x, y) = (prec.round(a.as_f64()), prec.round(b.as_f64()));
Num::Float(match self {
SemiringOp::Sum => prec.round(x + y),
SemiringOp::Prod => prec.round(x * y),
SemiringOp::Min => x.min(y),
SemiringOp::Max => x.max(y),
})
}
}
}
}
pub fn skolem_edge(a: Key, b: Key) -> Key {
if a <= b {
Key::Tuple(vec![a, b])
} else {
Key::Tuple(vec![b, a])
}
}
pub fn skolem(mut components: Vec<Key>, symmetric: bool) -> Key {
if symmetric {
components.sort_unstable();
}
Key::Tuple(components)
}
pub fn distinct(rows: &[Key]) -> Vec<Key> {
let mut v = rows.to_vec();
v.sort_unstable();
v.dedup(); v
}
pub fn equijoin(left: &[Key], right: &[Key]) -> Vec<(usize, usize)> {
let mut buckets: FxIndexMap<&Key, Vec<usize>> =
FxIndexMap::with_capacity_and_hasher(right.len(), Default::default());
for (j, k) in right.iter().enumerate() {
buckets.entry(k).or_default().push(j);
}
let mut matched: FxIndexMap<&Key, Vec<usize>> = FxIndexMap::default();
for (i, k) in left.iter().enumerate() {
if buckets.contains_key(k) {
matched.entry(k).or_default().push(i);
}
}
let mut keys: Vec<&Key> = matched.keys().copied().collect();
keys.sort_unstable();
let total: usize = keys
.iter()
.map(|k| matched[*k].len() * buckets[*k].len())
.sum();
let mut out = Vec::with_capacity(total);
for k in keys {
for &i in &matched[k] {
for &j in &buckets[k] {
out.push((i, j));
}
}
}
out
}
#[must_use]
pub(crate) fn equijoin_match_count(left: &[Key], right: &[Key]) -> usize {
let mut buckets: FxIndexMap<&Key, usize> =
FxIndexMap::with_capacity_and_hasher(right.len(), Default::default());
for k in right {
*buckets.entry(k).or_default() += 1;
}
left.iter()
.filter_map(|k| buckets.get(k))
.fold(0usize, |acc, n| acc.saturating_add(*n))
}
#[derive(Debug, Clone)]
pub struct Ranking {
pub order: Vec<Key>,
pub id: IndexMap<Key, i64>,
pub base: i64,
}
impl Ranking {
pub fn dense_ids(&self) -> Vec<i64> {
(0..self.order.len() as i64)
.map(|i| i + self.base)
.collect()
}
pub fn canonical_dense_ids(&self) -> Vec<i64> {
(0..self.order.len() as i64).collect()
}
}
pub fn rank(rows: &[Key]) -> Ranking {
rank_with_base(rows, 0)
}
pub fn rank_with_base(rows: &[Key], base: i64) -> Ranking {
let order = distinct(rows);
let mut id = IndexMap::with_capacity(order.len());
for (i, t) in order.iter().enumerate() {
id.insert(t.clone(), i as i64 + base);
}
Ranking { order, id, base }
}
pub fn group_aggregate(rows: &[(Key, Num)], op: SemiringOp) -> Vec<(Key, Num)> {
let mut buckets: IndexMap<Key, Vec<Num>> = IndexMap::new();
for (k, v) in rows {
buckets.entry(k.clone()).or_default().push(*v);
}
let mut keys: Vec<Key> = buckets.keys().cloned().collect();
keys.sort_unstable(); let mut out = Vec::with_capacity(keys.len());
for key in keys {
let mut vals = buckets.swap_remove(&key).expect("key came from the map");
vals.sort_by(Num::canonical_cmp); let agg = vals
.into_iter()
.reduce(|acc, x| op.apply(acc, x))
.expect("each bucket has ≥ 1 value");
out.push((key, agg));
}
out
}
pub fn canonical_index_set_json(rows: &[Key]) -> String {
serialize_keys(&distinct(rows))
}
pub fn serialize_keys(rows: &[Key]) -> String {
let mut out = String::new();
write_array(&mut out, rows, |o, k| k.write_token(o));
out
}
pub fn serialize_pairs(rows: &[(Key, Num)]) -> String {
let mut out = String::new();
write_array(&mut out, rows, |o, (k, v)| {
o.push('[');
k.write_token(o);
o.push(',');
v.write_token(o);
o.push(']');
});
out
}
fn write_array<T>(out: &mut String, items: &[T], emit: impl Fn(&mut String, &T)) {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
emit(out, item);
}
out.push(']');
}
#[cfg(test)]
mod tests {
use super::*;
fn ki(i: i64) -> Key {
Key::Int(i)
}
fn ks(s: &str) -> Key {
Key::Str(s.to_string())
}
fn t2(a: i64, b: i64) -> Key {
Key::Tuple(vec![ki(a), ki(b)])
}
fn tup(items: Vec<Key>) -> Key {
Key::Tuple(items)
}
fn undirected_edges(faces: &[Vec<i64>]) -> Vec<Key> {
let mut edges = Vec::new();
for face in faces {
let n = face.len();
for i in 0..n {
edges.push(skolem_edge(ki(face[i]), ki(face[(i + 1) % n])));
}
}
edges
}
#[test]
fn distinct_sorts_and_dedups() {
assert_eq!(
distinct(&[ki(3), ki(1), ki(2), ki(1)]),
vec![ki(1), ki(2), ki(3)]
);
assert_eq!(
distinct(&[t2(2, 1), t2(1, 2), t2(2, 1)]),
vec![t2(1, 2), t2(2, 1)]
);
assert_eq!(
distinct(&[ks("a"), ks("Z"), ks("B"), ks("a")]),
vec![ks("B"), ks("Z"), ks("a")]
);
assert_eq!(distinct(&[]), Vec::<Key>::new());
}
#[test]
fn match_count_is_what_equijoin_would_emit() {
let cases: Vec<(Vec<Key>, Vec<Key>)> = vec![
(
vec![ki(1), ki(1), ki(1), ki(2)],
vec![ki(1), ki(1), ki(2), ki(3)],
),
(vec![ki(1), ki(2)], vec![ki(3), ki(4)]),
(
(0..50).map(|i| ki(i % 2)).collect(),
(0..200).map(|i| ki(i % 2)).collect(),
),
(
vec![ks("a"), t2(1, 2), ki(7), Key::Bool(true)],
vec![t2(1, 2), t2(1, 2), ks("a"), Key::Bool(false)],
),
(vec![], vec![ki(1)]),
(vec![ki(1)], vec![]),
(vec![], vec![]),
];
for (left, right) in cases {
assert_eq!(
equijoin_match_count(&left, &right),
equijoin(&left, &right).len(),
"count disagrees with the pair list it is standing in for: \
left={left:?} right={right:?}"
);
}
}
#[test]
fn skolem_edge_canonicalises_undirected() {
assert_eq!(skolem_edge(ki(2), ki(5)), t2(2, 5));
assert_eq!(skolem_edge(ki(5), ki(2)), t2(2, 5)); assert_eq!(skolem_edge(ki(7), ki(7)), t2(7, 7)); }
#[test]
fn skolem_directed_preserves_symmetric_sorts() {
assert_eq!(skolem(vec![ki(1), ki(2)], false), t2(1, 2));
assert_eq!(skolem(vec![ki(2), ki(1)], false), t2(2, 1));
assert_eq!(
skolem(vec![ki(3), ki(1), ki(2)], true),
tup(vec![ki(1), ki(2), ki(3)])
);
}
#[test]
fn rank_dense_ids_zero_based_and_base_pin_roundtrip() {
let rows = [ki(30), ki(10), ki(20), ki(10)];
let rk = rank(&rows); assert_eq!(rk.order, vec![ki(10), ki(20), ki(30)]);
assert_eq!(rk.base, 0);
assert_eq!(rk.id[&ki(10)], 0);
assert_eq!(rk.id[&ki(20)], 1);
assert_eq!(rk.id[&ki(30)], 2);
assert_eq!(rk.dense_ids(), vec![0, 1, 2]);
assert_eq!(rk.canonical_dense_ids(), vec![0, 1, 2]);
let rk1 = rank_with_base(&rows, 1);
assert_eq!(rk1.id[&ki(10)], 1);
assert_eq!(rk1.id[&ki(20)], 2);
assert_eq!(rk1.id[&ki(30)], 3);
for t in &rk.order {
assert_eq!(rk.id[t] - rk.base, rk1.id[t] - rk1.base);
}
assert_eq!(rk1.canonical_dense_ids(), vec![0, 1, 2]);
}
#[test]
fn group_aggregate_semirings_and_permutation_invariance() {
let rows = [
(ks("b"), Num::Int(3)),
(ks("a"), Num::Int(1)),
(ks("b"), Num::Int(4)),
(ks("a"), Num::Int(10)),
(ks("c"), Num::Int(5)),
];
let sum = group_aggregate(&rows, SemiringOp::Sum);
assert_eq!(
sum,
vec![
(ks("a"), Num::Int(11)),
(ks("b"), Num::Int(7)),
(ks("c"), Num::Int(5))
]
);
let max = group_aggregate(&rows, SemiringOp::Max);
assert_eq!(
max,
vec![
(ks("a"), Num::Int(10)),
(ks("b"), Num::Int(4)),
(ks("c"), Num::Int(5))
]
);
let min = group_aggregate(&rows, SemiringOp::Min);
assert_eq!(
min,
vec![
(ks("a"), Num::Int(1)),
(ks("b"), Num::Int(3)),
(ks("c"), Num::Int(5))
]
);
let rows_r: Vec<(Key, Num)> = rows.iter().rev().cloned().collect();
assert_eq!(group_aggregate(&rows_r, SemiringOp::Sum), sum);
assert_eq!(
group_aggregate(
&[
(ki(1), Num::Int(2)),
(ki(1), Num::Int(3)),
(ki(1), Num::Int(4))
],
SemiringOp::Prod
),
vec![(ki(1), Num::Int(24))]
);
}
#[test]
fn group_aggregate_bool_keys_allowed() {
let rows = [
(Key::Bool(true), Num::Int(1)),
(Key::Bool(false), Num::Int(2)),
(Key::Bool(true), Num::Int(3)),
];
assert_eq!(
group_aggregate(&rows, SemiringOp::Sum),
vec![
(Key::Bool(false), Num::Int(2)),
(Key::Bool(true), Num::Int(4))
]
);
}
#[test]
fn group_aggregate_float_values_reduced_in_canonical_order() {
let rows = [
(ki(1), Num::Float(0.3)),
(ki(1), Num::Float(0.1)),
(ki(1), Num::Float(0.2)),
];
let got = group_aggregate(&rows, SemiringOp::Sum);
let want = (0.1_f64 + 0.2_f64) + 0.3_f64;
assert_eq!(got, vec![(ki(1), Num::Float(want))]);
assert_eq!(
serialize_pairs(&got),
format!("[[1,{}]]", format_canonical_float(want))
);
}
#[test]
fn canonical_index_set_json_matches_reference() {
assert_eq!(
canonical_index_set_json(&[ki(3), ki(1), ki(2), ki(1)]),
"[1,2,3]"
);
assert_eq!(
canonical_index_set_json(&[t2(2, 1), t2(1, 2)]),
"[[1,2],[2,1]]"
);
assert_eq!(
canonical_index_set_json(&[ks("a"), ks("B")]),
"[\"B\",\"a\"]"
);
assert_eq!(canonical_index_set_json(&[]), "[]");
}
#[test]
fn fixture_edge_enumeration_undirected() {
let golden_set = vec![t2(1, 2), t2(1, 3), t2(2, 3), t2(2, 4), t2(3, 4)];
let golden_json = "[[1,2],[1,3],[2,3],[2,4],[3,4]]";
let golden_ids = vec![0, 1, 2, 3, 4];
let canonical = vec![vec![1, 2, 3], vec![3, 2, 4]];
let variants = [
("permuted_faces", vec![vec![3, 2, 4], vec![1, 2, 3]]),
("reversed_winding", vec![vec![3, 2, 1], vec![4, 2, 3]]),
(
"duplicate_face",
vec![vec![1, 2, 3], vec![3, 2, 4], vec![1, 2, 3]],
),
];
for faces in std::iter::once(&("canonical", canonical.clone()))
.chain(variants.iter())
.map(|(_, f)| f)
{
let edges = undirected_edges(faces);
assert_eq!(distinct(&edges), golden_set);
assert_eq!(canonical_index_set_json(&edges), golden_json);
assert_eq!(rank(&edges).canonical_dense_ids(), golden_ids);
}
}
#[test]
fn fixture_directed_arcs() {
let golden_set = vec![t2(1, 2), t2(2, 1), t2(2, 3)];
let golden_json = "[[1,2],[2,1],[2,3]]";
let golden_ids = vec![0, 1, 2];
let canonical = vec![t2(1, 2), t2(2, 1), t2(2, 3), t2(1, 2)];
let variants: Vec<Vec<Key>> = vec![
vec![t2(2, 3), t2(1, 2), t2(2, 1), t2(1, 2)], vec![t2(1, 2), t2(2, 1), t2(2, 3), t2(1, 2), t2(2, 3)], ];
for arcs in std::iter::once(&canonical).chain(variants.iter()) {
let keyed: Vec<Key> = arcs.iter().map(|k| skolem(field_pair(k), false)).collect();
assert_eq!(distinct(&keyed), golden_set);
assert_eq!(canonical_index_set_json(&keyed), golden_json);
assert_eq!(rank(&keyed).canonical_dense_ids(), golden_ids);
}
}
fn field_pair(k: &Key) -> Vec<Key> {
match k {
Key::Tuple(v) => v.clone(),
_ => panic!("expected a tuple arc"),
}
}
#[test]
fn fixture_group_by_sum_string_keys() {
let golden = vec![
(ks("B"), Num::Int(5)),
(ks("Z"), Num::Int(1)),
(ks("a"), Num::Int(9)),
];
let golden_json = "[[\"B\",5],[\"Z\",1],[\"a\",9]]";
let golden_ids = vec![0, 1, 2];
let canonical = vec![
(ks("B"), Num::Int(2)),
(ks("a"), Num::Int(5)),
(ks("B"), Num::Int(3)),
(ks("Z"), Num::Int(1)),
(ks("a"), Num::Int(4)),
];
let permuted = vec![
(ks("a"), Num::Int(4)),
(ks("Z"), Num::Int(1)),
(ks("B"), Num::Int(2)),
(ks("a"), Num::Int(5)),
(ks("B"), Num::Int(3)),
];
for rows in [&canonical, &permuted] {
let agg = group_aggregate(rows, SemiringOp::Sum);
assert_eq!(agg, golden);
assert_eq!(serialize_pairs(&agg), golden_json);
let keys: Vec<Key> = agg.iter().map(|(k, _)| k.clone()).collect();
assert_eq!(rank(&keys).canonical_dense_ids(), golden_ids);
}
}
#[test]
fn float_in_key_rejected_at_boundary() {
assert!(Key::try_from_json(&serde_json::json!(1.5)).is_err());
assert!(Key::try_from_json(&serde_json::json!(1.0)).is_err()); assert!(Key::try_from_json(&serde_json::json!([1, 2.5])).is_err());
assert_eq!(Key::try_from_json(&serde_json::json!(7)).unwrap(), ki(7));
assert_eq!(
Key::try_from_json(&serde_json::json!("B")).unwrap(),
ks("B")
);
assert_eq!(
Key::try_from_json(&serde_json::json!([1, 2])).unwrap(),
t2(1, 2)
);
assert_eq!(
Key::try_from_json(&serde_json::json!(true)).unwrap(),
Key::Bool(true)
);
}
}