#![forbid(unsafe_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScoreError {
EmptyInput,
MismatchedLengths,
NonFiniteInput,
}
pub fn weighted_sum(values: &[f64], weights: &[f64]) -> Result<f64, ScoreError> {
if values.is_empty() || weights.is_empty() {
return Err(ScoreError::EmptyInput);
}
if values.len() != weights.len() {
return Err(ScoreError::MismatchedLengths);
}
if values.iter().any(|value| !value.is_finite())
|| weights.iter().any(|weight| !weight.is_finite())
{
return Err(ScoreError::NonFiniteInput);
}
Ok(values
.iter()
.zip(weights.iter())
.map(|(value, weight)| value * weight)
.sum())
}
pub fn normalize_min_max(values: &[f64]) -> Option<Vec<f64>> {
if values.is_empty() || values.iter().any(|value| !value.is_finite()) {
return None;
}
let minimum = values.iter().copied().min_by(f64::total_cmp)?;
let maximum = values.iter().copied().max_by(f64::total_cmp)?;
let range = maximum - minimum;
if range == 0.0 {
return Some(vec![0.0; values.len()]);
}
Some(
values
.iter()
.map(|value| (*value - minimum) / range)
.collect(),
)
}
pub fn rank_descending(values: &[f64]) -> Vec<usize> {
let mut indices: Vec<usize> = (0..values.len()).collect();
indices.sort_by(|left, right| {
values[*right]
.total_cmp(&values[*left])
.then_with(|| left.cmp(right))
});
indices
}
pub fn rank_ascending(values: &[f64]) -> Vec<usize> {
let mut indices: Vec<usize> = (0..values.len()).collect();
indices.sort_by(|left, right| {
values[*left]
.total_cmp(&values[*right])
.then_with(|| left.cmp(right))
});
indices
}
pub fn penalize(score: f64, penalty: f64) -> f64 {
score - penalty
}
pub fn reward(score: f64, reward_value: f64) -> f64 {
score + reward_value
}
#[cfg(test)]
mod tests {
use super::{
ScoreError, normalize_min_max, penalize, rank_ascending, rank_descending, reward,
weighted_sum,
};
#[test]
fn computes_weighted_scores() {
assert_eq!(weighted_sum(&[3.0, 4.0], &[0.25, 0.75]).unwrap(), 3.75);
assert_eq!(weighted_sum(&[9.0], &[2.0]).unwrap(), 18.0);
}
#[test]
fn rejects_invalid_weighted_score_inputs() {
assert_eq!(weighted_sum(&[], &[]), Err(ScoreError::EmptyInput));
assert_eq!(
weighted_sum(&[1.0, 2.0], &[1.0]),
Err(ScoreError::MismatchedLengths)
);
assert_eq!(
weighted_sum(&[1.0, f64::NAN], &[0.5, 0.5]),
Err(ScoreError::NonFiniteInput)
);
}
#[test]
fn normalizes_values_with_min_max_scaling() {
assert_eq!(
normalize_min_max(&[2.0, 4.0, 6.0]),
Some(vec![0.0, 0.5, 1.0])
);
assert_eq!(normalize_min_max(&[5.0]), Some(vec![0.0]));
assert_eq!(normalize_min_max(&[3.0, 3.0]), Some(vec![0.0, 0.0]));
assert_eq!(normalize_min_max(&[]), None);
assert_eq!(normalize_min_max(&[1.0, f64::INFINITY]), None);
}
#[test]
fn ranks_values_in_both_directions() {
assert_eq!(rank_descending(&[1.0, 3.0, 2.0]), vec![1, 2, 0]);
assert_eq!(rank_ascending(&[1.0, 3.0, 2.0]), vec![0, 2, 1]);
}
#[test]
fn adjusts_scores_with_penalties_and_rewards() {
assert_eq!(penalize(10.0, 1.5), 8.5);
assert_eq!(reward(10.0, 1.5), 11.5);
}
}