#[derive(Debug, Clone, PartialEq)]
pub struct SankeyFlow {
pub source: String,
pub target: String,
pub value: f64,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Sankey {
pub flows: Vec<SankeyFlow>,
}
impl Sankey {
pub fn total_volume(&self) -> f64 {
self.flows.iter().map(|f| f.value).sum()
}
pub fn unique_node_names(&self) -> Vec<String> {
let mut seen = Vec::new();
for flow in &self.flows {
if !seen.contains(&flow.source) {
seen.push(flow.source.clone());
}
if !seen.contains(&flow.target) {
seen.push(flow.target.clone());
}
}
seen
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_flow(src: &str, tgt: &str, val: f64) -> SankeyFlow {
SankeyFlow {
source: src.to_string(),
target: tgt.to_string(),
value: val,
}
}
#[test]
fn total_volume_empty_is_zero() {
let s = Sankey::default();
assert!((s.total_volume() - 0.0).abs() < 1e-9);
}
#[test]
fn total_volume_sums_all_flows() {
let s = Sankey {
flows: vec![
make_flow("A", "B", 10.0),
make_flow("B", "C", 5.5),
make_flow("A", "C", 2.25),
],
};
assert!((s.total_volume() - 17.75).abs() < 1e-9);
}
#[test]
fn unique_node_names_empty() {
let s = Sankey::default();
assert!(s.unique_node_names().is_empty());
}
#[test]
fn unique_node_names_deduplicates_and_preserves_order() {
let s = Sankey {
flows: vec![
make_flow("Coal", "Solid", 75.0),
make_flow("Coal", "Gas", 10.0),
make_flow("Gas", "Solid", 5.0),
],
};
assert_eq!(s.unique_node_names(), vec!["Coal", "Solid", "Gas"]);
}
#[test]
fn unique_node_names_single_flow() {
let s = Sankey {
flows: vec![make_flow("Source", "Target", 42.0)],
};
assert_eq!(s.unique_node_names(), vec!["Source", "Target"]);
}
#[test]
fn partial_eq_holds_for_identical_sankeys() {
let a = Sankey {
flows: vec![make_flow("A", "B", 1.0)],
};
let b = a.clone();
assert_eq!(a, b);
let c = Sankey {
flows: vec![make_flow("A", "B", 2.0)],
};
assert_ne!(a, c);
}
}