Skip to main content

use_score/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive scoring and ranking helpers for optimization.
3//!
4//! # Examples
5//!
6//! ```rust
7//! use use_score::{normalize_min_max, rank_descending, weighted_sum};
8//!
9//! assert_eq!(weighted_sum(&[3.0, 4.0], &[0.25, 0.75]).unwrap(), 3.75);
10//! assert_eq!(normalize_min_max(&[2.0, 4.0, 6.0]).unwrap(), vec![0.0, 0.5, 1.0]);
11//! assert_eq!(rank_descending(&[1.0, 3.0, 2.0]), vec![1, 2, 0]);
12//! ```
13
14#[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}