use crate::merge::Merge;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ConstraintGCounter {
counts: HashMap<String, u64>,
violations: HashMap<String, u64>,
}
impl ConstraintGCounter {
pub fn new() -> Self {
Self {
counts: HashMap::new(),
violations: HashMap::new(),
}
}
pub fn record_satisfied(&mut self, node: &str, count: u64) {
*self.counts.entry(node.to_string()).or_insert(0) += count;
}
pub fn record_violations(&mut self, node: &str, count: u64) {
*self.violations.entry(node.to_string()).or_insert(0) += count;
}
pub fn total_satisfied(&self) -> u64 {
self.counts.values().sum()
}
pub fn total_violations(&self) -> u64 {
self.violations.values().sum()
}
pub fn satisfaction_rate(&self) -> f64 {
let total = self.total_satisfied() + self.total_violations();
if total == 0 { return 1.0; }
self.total_satisfied() as f64 / total as f64
}
pub fn node_count(&self) -> usize {
let mut nodes: std::collections::HashSet<&str> = std::collections::HashSet::new();
for k in self.counts.keys() { nodes.insert(k); }
for k in self.violations.keys() { nodes.insert(k); }
nodes.len()
}
pub fn node_satisfied(&self, node: &str) -> u64 {
self.counts.get(node).copied().unwrap_or(0)
}
pub fn node_violations(&self, node: &str) -> u64 {
self.violations.get(node).copied().unwrap_or(0)
}
}
impl Merge for ConstraintGCounter {
fn merge(&mut self, other: &Self) {
for (node, count) in &other.counts {
let entry = self.counts.entry(node.clone()).or_insert(0);
*entry = (*entry).max(*count);
}
for (node, count) in &other.violations {
let entry = self.violations.entry(node.clone()).or_insert(0);
*entry = (*entry).max(*count);
}
}
}
impl fmt::Display for ConstraintGCounter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ConstraintGCounter({}/{} satisfied, {} nodes, {:.1}% rate)",
self.total_satisfied(),
self.total_satisfied() + self.total_violations(),
self.node_count(),
self.satisfaction_rate() * 100.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::merge::laws;
#[test]
fn test_basic_counting() {
let mut c = ConstraintGCounter::new();
c.record_satisfied("node-a", 100);
c.record_violations("node-a", 5);
assert_eq!(c.total_satisfied(), 100);
assert_eq!(c.total_violations(), 5);
assert!((c.satisfaction_rate() - 0.9524).abs() < 0.01);
}
#[test]
fn test_merge_combines_nodes() {
let mut a = ConstraintGCounter::new();
a.record_satisfied("node-a", 100);
a.record_violations("node-a", 5);
let mut b = ConstraintGCounter::new();
b.record_satisfied("node-b", 200);
b.record_violations("node-b", 10);
let merged = a.merged(&b);
assert_eq!(merged.total_satisfied(), 300);
assert_eq!(merged.total_violations(), 15);
assert_eq!(merged.node_count(), 2);
}
#[test]
fn test_merge_takes_max_per_node() {
let mut a = ConstraintGCounter::new();
a.record_satisfied("node-a", 100);
let mut b = ConstraintGCounter::new();
b.record_satisfied("node-a", 150);
let merged = a.merged(&b);
assert_eq!(merged.node_satisfied("node-a"), 150);
}
#[test]
fn test_merge_commutative() {
let mut a = ConstraintGCounter::new();
a.record_satisfied("node-a", 100);
let mut b = ConstraintGCounter::new();
b.record_satisfied("node-b", 200);
assert!(laws::check_commutative(&a, &b));
}
#[test]
fn test_merge_associative() {
let mut a = ConstraintGCounter::new();
a.record_satisfied("a", 1);
let mut b = ConstraintGCounter::new();
b.record_satisfied("b", 2);
let mut c = ConstraintGCounter::new();
c.record_satisfied("c", 3);
assert!(laws::check_associative(&a, &b, &c));
}
#[test]
fn test_merge_idempotent() {
let mut a = ConstraintGCounter::new();
a.record_satisfied("a", 100);
a.record_violations("a", 5);
assert!(laws::check_idempotent(&a));
}
#[test]
fn test_subsumes() {
let mut a = ConstraintGCounter::new();
a.record_satisfied("a", 100);
let mut b = ConstraintGCounter::new();
b.record_satisfied("a", 50);
assert!(a.subsumes(&b));
}
#[test]
fn test_display() {
let mut c = ConstraintGCounter::new();
c.record_satisfied("a", 100);
c.record_violations("a", 5);
let s = format!("{}", c);
assert!(s.contains("100"));
assert!(s.contains("95.2%"));
}
}