use super::Objective;
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
pub struct MooraResult {
pub scores: Vec<f64>,
pub ranking: Vec<usize>,
}
impl MooraResult {
pub fn winner(&self) -> Option<usize> {
self.ranking.first().copied()
}
}
pub fn moora(
matrix: &[Vec<f64>],
weights: &[f64],
types: &[Objective],
) -> Result<MooraResult, String> {
let m = matrix.len();
if m == 0 {
return Err("MOORA: empty decision matrix".into());
}
let n = weights.len();
if n == 0 {
return Err("MOORA: no criteria".into());
}
if types.len() != n {
return Err(format!(
"MOORA: {} weights but {} criterion types",
n,
types.len()
));
}
for (i, row) in matrix.iter().enumerate() {
if row.len() != n {
return Err(format!(
"MOORA: alternative {i} has {} values but there are {n} criteria",
row.len()
));
}
if row.iter().any(|v| !v.is_finite()) {
return Err(format!("MOORA: alternative {i} has a non-finite value"));
}
}
let mut norm = vec![vec![0.0f64; n]; m];
for j in 0..n {
let l2 = matrix.iter().map(|r| r[j] * r[j]).sum::<f64>().sqrt();
if l2 <= 0.0 {
return Err(format!("MOORA: criterion {j} has zero L2 norm"));
}
for i in 0..m {
norm[i][j] = matrix[i][j] / l2;
}
}
let scores: Vec<f64> = norm
.iter()
.map(|row| {
(0..n)
.map(|j| {
let term = weights[j] * row[j];
match types[j] {
Objective::Max => term,
Objective::Min => -term,
}
})
.sum::<f64>()
})
.collect();
let ranking = super::topsis::rank_desc(&scores);
Ok(MooraResult { scores, ranking })
}
impl super::wsm::DecisionMatrix {
pub fn moora(&self) -> Result<MooraResult, String> {
self.validate()?;
let weights = self.normalized_weights();
let types: Vec<Objective> = self
.criteria
.iter()
.map(|c| match c.direction {
super::wsm::Direction::Benefit => Objective::Max,
super::wsm::Direction::Cost => Objective::Min,
})
.collect();
let matrix: Vec<Vec<f64>> = self.alternatives.iter().map(|a| a.values.clone()).collect();
moora(&matrix, &weights, &types)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ref_matrix() -> Vec<Vec<f64>> {
vec![
vec![250.0, 16.0, 12.0],
vec![200.0, 16.0, 8.0],
vec![300.0, 32.0, 16.0],
vec![275.0, 24.0, 10.0],
]
}
#[test]
fn ratio_scores_ranked() {
let w = [0.40, 0.35, 0.25];
let t = [Objective::Min, Objective::Max, Objective::Max];
let r = moora(&ref_matrix(), &w, &t).unwrap();
assert_eq!(r.winner(), Some(2));
assert_eq!(r.ranking, vec![2, 3, 0, 1]);
}
#[test]
fn zero_column_is_an_error() {
let w = [0.5, 0.5];
let t = [Objective::Max, Objective::Min];
let bad = vec![vec![0.0, 1.0], vec![0.0, 2.0]];
assert!(moora(&bad, &w, &t).is_err());
}
#[test]
fn shape_mismatch_is_an_error() {
let w = [0.5, 0.5];
let t = [Objective::Max, Objective::Max];
assert!(moora(&[vec![1.0, 2.0], vec![3.0]], &w, &t).is_err());
}
}