1#![forbid(unsafe_code)]
2#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum ScoreError {
16 EmptyInput,
17 MismatchedLengths,
18 NonFiniteInput,
19}
20
21pub fn weighted_sum(values: &[f64], weights: &[f64]) -> Result<f64, ScoreError> {
22 if values.is_empty() || weights.is_empty() {
23 return Err(ScoreError::EmptyInput);
24 }
25
26 if values.len() != weights.len() {
27 return Err(ScoreError::MismatchedLengths);
28 }
29
30 if values.iter().any(|value| !value.is_finite())
31 || weights.iter().any(|weight| !weight.is_finite())
32 {
33 return Err(ScoreError::NonFiniteInput);
34 }
35
36 Ok(values
37 .iter()
38 .zip(weights.iter())
39 .map(|(value, weight)| value * weight)
40 .sum())
41}
42
43pub fn normalize_min_max(values: &[f64]) -> Option<Vec<f64>> {
44 if values.is_empty() || values.iter().any(|value| !value.is_finite()) {
45 return None;
46 }
47
48 let minimum = values.iter().copied().min_by(f64::total_cmp)?;
49 let maximum = values.iter().copied().max_by(f64::total_cmp)?;
50 let range = maximum - minimum;
51
52 if range == 0.0 {
53 return Some(vec![0.0; values.len()]);
54 }
55
56 Some(
57 values
58 .iter()
59 .map(|value| (*value - minimum) / range)
60 .collect(),
61 )
62}
63
64pub fn rank_descending(values: &[f64]) -> Vec<usize> {
65 let mut indices: Vec<usize> = (0..values.len()).collect();
66 indices.sort_by(|left, right| {
67 values[*right]
68 .total_cmp(&values[*left])
69 .then_with(|| left.cmp(right))
70 });
71 indices
72}
73
74pub fn rank_ascending(values: &[f64]) -> Vec<usize> {
75 let mut indices: Vec<usize> = (0..values.len()).collect();
76 indices.sort_by(|left, right| {
77 values[*left]
78 .total_cmp(&values[*right])
79 .then_with(|| left.cmp(right))
80 });
81 indices
82}
83
84pub fn penalize(score: f64, penalty: f64) -> f64 {
85 score - penalty
86}
87
88pub fn reward(score: f64, reward_value: f64) -> f64 {
89 score + reward_value
90}
91
92#[cfg(test)]
93mod tests {
94 use super::{
95 ScoreError, normalize_min_max, penalize, rank_ascending, rank_descending, reward,
96 weighted_sum,
97 };
98
99 #[test]
100 fn computes_weighted_scores() {
101 assert_eq!(weighted_sum(&[3.0, 4.0], &[0.25, 0.75]).unwrap(), 3.75);
102 assert_eq!(weighted_sum(&[9.0], &[2.0]).unwrap(), 18.0);
103 }
104
105 #[test]
106 fn rejects_invalid_weighted_score_inputs() {
107 assert_eq!(weighted_sum(&[], &[]), Err(ScoreError::EmptyInput));
108 assert_eq!(
109 weighted_sum(&[1.0, 2.0], &[1.0]),
110 Err(ScoreError::MismatchedLengths)
111 );
112 assert_eq!(
113 weighted_sum(&[1.0, f64::NAN], &[0.5, 0.5]),
114 Err(ScoreError::NonFiniteInput)
115 );
116 }
117
118 #[test]
119 fn normalizes_values_with_min_max_scaling() {
120 assert_eq!(
121 normalize_min_max(&[2.0, 4.0, 6.0]),
122 Some(vec![0.0, 0.5, 1.0])
123 );
124 assert_eq!(normalize_min_max(&[5.0]), Some(vec![0.0]));
125 assert_eq!(normalize_min_max(&[3.0, 3.0]), Some(vec![0.0, 0.0]));
126 assert_eq!(normalize_min_max(&[]), None);
127 assert_eq!(normalize_min_max(&[1.0, f64::INFINITY]), None);
128 }
129
130 #[test]
131 fn ranks_values_in_both_directions() {
132 assert_eq!(rank_descending(&[1.0, 3.0, 2.0]), vec![1, 2, 0]);
133 assert_eq!(rank_ascending(&[1.0, 3.0, 2.0]), vec![0, 2, 1]);
134 }
135
136 #[test]
137 fn adjusts_scores_with_penalties_and_rewards() {
138 assert_eq!(penalize(10.0, 1.5), 8.5);
139 assert_eq!(reward(10.0, 1.5), 11.5);
140 }
141}