1#![forbid(unsafe_code)]
2#[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}