Skip to main content

use_objective/
lib.rs

1#![forbid(unsafe_code)]
2//! Objective direction and objective value helpers.
3//!
4//! The crate stays intentionally small and focuses on common helpers for
5//! choosing better `f64` values without implementing `Ord` over floating-point
6//! inputs.
7//!
8//! # Examples
9//!
10//! ```rust
11//! use use_objective::{best_value, is_better, ObjectiveDirection, ObjectiveValue};
12//!
13//! assert!(is_better(3.0, 5.0, ObjectiveDirection::Minimize));
14//! assert_eq!(best_value(&[1.0, 4.0, 2.0], ObjectiveDirection::Maximize), Some(4.0));
15//!
16//! let candidate = ObjectiveValue::new(3.0, ObjectiveDirection::Minimize).unwrap();
17//! let incumbent = ObjectiveValue::new(4.0, ObjectiveDirection::Minimize).unwrap();
18//! assert_eq!(candidate.is_better_than(&incumbent), Some(true));
19//! ```
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ObjectiveDirection {
23    Minimize,
24    Maximize,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct ObjectiveValue {
29    value: f64,
30    direction: ObjectiveDirection,
31}
32
33impl ObjectiveValue {
34    pub fn new(value: f64, direction: ObjectiveDirection) -> Option<Self> {
35        if !value.is_finite() {
36            return None;
37        }
38
39        Some(Self { value, direction })
40    }
41
42    pub fn value(self) -> f64 {
43        self.value
44    }
45
46    pub fn direction(self) -> ObjectiveDirection {
47        self.direction
48    }
49
50    pub fn is_better_than(&self, other: &Self) -> Option<bool> {
51        if self.direction != other.direction {
52            return None;
53        }
54
55        Some(is_better(self.value, other.value, self.direction))
56    }
57
58    pub fn better(self, other: Self) -> Option<Self> {
59        self.is_better_than(&other)
60            .map(|self_is_better| if self_is_better { self } else { other })
61    }
62}
63
64pub fn is_better(candidate: f64, incumbent: f64, direction: ObjectiveDirection) -> bool {
65    if !candidate.is_finite() || !incumbent.is_finite() {
66        return false;
67    }
68
69    match direction {
70        ObjectiveDirection::Minimize => candidate < incumbent,
71        ObjectiveDirection::Maximize => candidate > incumbent,
72    }
73}
74
75pub fn best_value(values: &[f64], direction: ObjectiveDirection) -> Option<f64> {
76    if values.is_empty() || values.iter().any(|value| !value.is_finite()) {
77        return None;
78    }
79
80    let mut best = values[0];
81    for value in values.iter().copied().skip(1) {
82        if is_better(value, best, direction) {
83            best = value;
84        }
85    }
86
87    Some(best)
88}
89
90#[cfg(test)]
91mod tests {
92    use super::{ObjectiveDirection, ObjectiveValue, best_value, is_better};
93
94    #[test]
95    fn compares_candidates_by_direction() {
96        assert!(is_better(2.0, 5.0, ObjectiveDirection::Minimize));
97        assert!(is_better(5.0, 2.0, ObjectiveDirection::Maximize));
98        assert!(!is_better(2.0, 2.0, ObjectiveDirection::Maximize));
99    }
100
101    #[test]
102    fn chooses_best_value() {
103        assert_eq!(
104            best_value(&[4.0, 2.0, 3.0], ObjectiveDirection::Minimize),
105            Some(2.0)
106        );
107        assert_eq!(
108            best_value(&[4.0, 2.0, 3.0], ObjectiveDirection::Maximize),
109            Some(4.0)
110        );
111        assert_eq!(best_value(&[9.0], ObjectiveDirection::Minimize), Some(9.0));
112    }
113
114    #[test]
115    fn returns_none_for_invalid_values() {
116        assert_eq!(best_value(&[], ObjectiveDirection::Minimize), None);
117        assert_eq!(
118            best_value(&[1.0, f64::NAN], ObjectiveDirection::Maximize),
119            None
120        );
121        assert!(!is_better(f64::INFINITY, 2.0, ObjectiveDirection::Minimize));
122    }
123
124    #[test]
125    fn objective_value_helpers_stay_explicit() {
126        let candidate = ObjectiveValue::new(2.0, ObjectiveDirection::Minimize).unwrap();
127        let incumbent = ObjectiveValue::new(5.0, ObjectiveDirection::Minimize).unwrap();
128        let other_direction = ObjectiveValue::new(5.0, ObjectiveDirection::Maximize).unwrap();
129
130        assert_eq!(candidate.value(), 2.0);
131        assert_eq!(candidate.direction(), ObjectiveDirection::Minimize);
132        assert_eq!(candidate.is_better_than(&incumbent), Some(true));
133        assert_eq!(candidate.better(incumbent), Some(candidate));
134        assert_eq!(candidate.is_better_than(&other_direction), None);
135        assert_eq!(
136            ObjectiveValue::new(f64::NEG_INFINITY, ObjectiveDirection::Maximize),
137            None
138        );
139    }
140}