solverforge_core/score/
bendable.rs

1//! BendableScore - Runtime-configurable multi-level score
2
3use std::cmp::Ordering;
4use std::fmt;
5use std::ops::{Add, Neg, Sub};
6
7use super::traits::{ParseableScore, Score, ScoreParseError};
8
9/// A score with a configurable number of hard and soft levels.
10///
11/// Unlike `HardSoftScore`, the number of levels is determined at runtime.
12/// This is useful when the constraint structure varies between problem instances.
13///
14/// # Examples
15///
16/// ```
17/// use solverforge_core::score::{BendableScore, Score};
18///
19/// // Create a score with 2 hard levels and 3 soft levels
20/// let score = BendableScore::of(vec![-1, -2], vec![-10, -20, -30]);
21///
22/// assert_eq!(score.hard_levels_count(), 2);
23/// assert_eq!(score.soft_levels_count(), 3);
24/// assert!(!score.is_feasible());  // Negative hard scores
25/// ```
26#[derive(Clone, PartialEq, Eq, Hash)]
27pub struct BendableScore {
28    hard_scores: Vec<i64>,
29    soft_scores: Vec<i64>,
30}
31
32impl BendableScore {
33    /// Creates a new BendableScore with the given hard and soft score vectors.
34    pub fn of(hard_scores: Vec<i64>, soft_scores: Vec<i64>) -> Self {
35        BendableScore {
36            hard_scores,
37            soft_scores,
38        }
39    }
40
41    /// Creates a zero score with the specified number of levels.
42    pub fn zero_with_levels(hard_levels: usize, soft_levels: usize) -> Self {
43        BendableScore {
44            hard_scores: vec![0; hard_levels],
45            soft_scores: vec![0; soft_levels],
46        }
47    }
48
49    /// Returns the number of hard score levels.
50    pub fn hard_levels_count(&self) -> usize {
51        self.hard_scores.len()
52    }
53
54    /// Returns the number of soft score levels.
55    pub fn soft_levels_count(&self) -> usize {
56        self.soft_scores.len()
57    }
58
59    /// Returns the hard score at the given level.
60    ///
61    /// # Panics
62    /// Panics if the level is out of bounds.
63    pub fn hard_score(&self, level: usize) -> i64 {
64        self.hard_scores[level]
65    }
66
67    /// Returns the soft score at the given level.
68    ///
69    /// # Panics
70    /// Panics if the level is out of bounds.
71    pub fn soft_score(&self, level: usize) -> i64 {
72        self.soft_scores[level]
73    }
74
75    /// Returns all hard scores as a slice.
76    pub fn hard_scores(&self) -> &[i64] {
77        &self.hard_scores
78    }
79
80    /// Returns all soft scores as a slice.
81    pub fn soft_scores(&self) -> &[i64] {
82        &self.soft_scores
83    }
84
85    /// Creates a score with a single hard level penalty at the given index.
86    pub fn one_hard(hard_levels: usize, soft_levels: usize, level: usize) -> Self {
87        let mut hard_scores = vec![0; hard_levels];
88        hard_scores[level] = 1;
89        BendableScore {
90            hard_scores,
91            soft_scores: vec![0; soft_levels],
92        }
93    }
94
95    /// Creates a score with a single soft level penalty at the given index.
96    pub fn one_soft(hard_levels: usize, soft_levels: usize, level: usize) -> Self {
97        let mut soft_scores = vec![0; soft_levels];
98        soft_scores[level] = 1;
99        BendableScore {
100            hard_scores: vec![0; hard_levels],
101            soft_scores,
102        }
103    }
104
105    fn ensure_compatible(&self, other: &Self) {
106        assert_eq!(
107            self.hard_scores.len(),
108            other.hard_scores.len(),
109            "Incompatible hard levels: {} vs {}",
110            self.hard_scores.len(),
111            other.hard_scores.len()
112        );
113        assert_eq!(
114            self.soft_scores.len(),
115            other.soft_scores.len(),
116            "Incompatible soft levels: {} vs {}",
117            self.soft_scores.len(),
118            other.soft_scores.len()
119        );
120    }
121}
122
123impl Default for BendableScore {
124    fn default() -> Self {
125        // Default to 1 hard + 1 soft level (like HardSoftScore)
126        BendableScore::zero_with_levels(1, 1)
127    }
128}
129
130impl Score for BendableScore {
131    fn is_feasible(&self) -> bool {
132        self.hard_scores.iter().all(|&s| s >= 0)
133    }
134
135    fn zero() -> Self {
136        BendableScore::default()
137    }
138
139    fn levels_count() -> usize {
140        // This is a bit awkward for BendableScore since levels are runtime-determined
141        // Return 0 to indicate "variable"
142        0
143    }
144
145    fn to_level_numbers(&self) -> Vec<i64> {
146        let mut levels = self.hard_scores.clone();
147        levels.extend(self.soft_scores.iter());
148        levels
149    }
150
151    fn from_level_numbers(levels: &[i64]) -> Self {
152        // Assume half hard, half soft if not otherwise specified
153        let mid = levels.len() / 2;
154        BendableScore::of(levels[..mid].to_vec(), levels[mid..].to_vec())
155    }
156
157    fn multiply(&self, multiplicand: f64) -> Self {
158        BendableScore {
159            hard_scores: self
160                .hard_scores
161                .iter()
162                .map(|&s| (s as f64 * multiplicand).round() as i64)
163                .collect(),
164            soft_scores: self
165                .soft_scores
166                .iter()
167                .map(|&s| (s as f64 * multiplicand).round() as i64)
168                .collect(),
169        }
170    }
171
172    fn divide(&self, divisor: f64) -> Self {
173        BendableScore {
174            hard_scores: self
175                .hard_scores
176                .iter()
177                .map(|&s| (s as f64 / divisor).round() as i64)
178                .collect(),
179            soft_scores: self
180                .soft_scores
181                .iter()
182                .map(|&s| (s as f64 / divisor).round() as i64)
183                .collect(),
184        }
185    }
186
187    fn abs(&self) -> Self {
188        BendableScore {
189            hard_scores: self.hard_scores.iter().map(|&s| s.abs()).collect(),
190            soft_scores: self.soft_scores.iter().map(|&s| s.abs()).collect(),
191        }
192    }
193}
194
195impl Ord for BendableScore {
196    fn cmp(&self, other: &Self) -> Ordering {
197        self.ensure_compatible(other);
198
199        // Compare hard scores first (highest priority first)
200        for (a, b) in self.hard_scores.iter().zip(other.hard_scores.iter()) {
201            match a.cmp(b) {
202                Ordering::Equal => continue,
203                other => return other,
204            }
205        }
206
207        // Then compare soft scores
208        for (a, b) in self.soft_scores.iter().zip(other.soft_scores.iter()) {
209            match a.cmp(b) {
210                Ordering::Equal => continue,
211                other => return other,
212            }
213        }
214
215        Ordering::Equal
216    }
217}
218
219impl PartialOrd for BendableScore {
220    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
221        Some(self.cmp(other))
222    }
223}
224
225impl Add for BendableScore {
226    type Output = Self;
227
228    fn add(self, other: Self) -> Self {
229        self.ensure_compatible(&other);
230        BendableScore {
231            hard_scores: self
232                .hard_scores
233                .iter()
234                .zip(other.hard_scores.iter())
235                .map(|(a, b)| a + b)
236                .collect(),
237            soft_scores: self
238                .soft_scores
239                .iter()
240                .zip(other.soft_scores.iter())
241                .map(|(a, b)| a + b)
242                .collect(),
243        }
244    }
245}
246
247impl Sub for BendableScore {
248    type Output = Self;
249
250    fn sub(self, other: Self) -> Self {
251        self.ensure_compatible(&other);
252        BendableScore {
253            hard_scores: self
254                .hard_scores
255                .iter()
256                .zip(other.hard_scores.iter())
257                .map(|(a, b)| a - b)
258                .collect(),
259            soft_scores: self
260                .soft_scores
261                .iter()
262                .zip(other.soft_scores.iter())
263                .map(|(a, b)| a - b)
264                .collect(),
265        }
266    }
267}
268
269impl Neg for BendableScore {
270    type Output = Self;
271
272    fn neg(self) -> Self {
273        BendableScore {
274            hard_scores: self.hard_scores.iter().map(|&s| -s).collect(),
275            soft_scores: self.soft_scores.iter().map(|&s| -s).collect(),
276        }
277    }
278}
279
280impl fmt::Debug for BendableScore {
281    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282        write!(
283            f,
284            "BendableScore(hard: {:?}, soft: {:?})",
285            self.hard_scores, self.soft_scores
286        )
287    }
288}
289
290impl fmt::Display for BendableScore {
291    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292        // Format: "[0/0]hard/[-10/-20/-30]soft"
293        let hard_str: Vec<String> = self.hard_scores.iter().map(|s| s.to_string()).collect();
294        let soft_str: Vec<String> = self.soft_scores.iter().map(|s| s.to_string()).collect();
295
296        write!(
297            f,
298            "[{}]hard/[{}]soft",
299            hard_str.join("/"),
300            soft_str.join("/")
301        )
302    }
303}
304
305impl ParseableScore for BendableScore {
306    fn parse(s: &str) -> Result<Self, ScoreParseError> {
307        let s = s.trim();
308
309        // Format: "[0/0]hard/[-10/-20/-30]soft"
310        let parts: Vec<&str> = s.split("hard/").collect();
311        if parts.len() != 2 {
312            return Err(ScoreParseError {
313                message: format!(
314                    "Invalid BendableScore format '{}': expected '[...]hard/[...]soft'",
315                    s
316                ),
317            });
318        }
319
320        let hard_part = parts[0]
321            .trim()
322            .strip_prefix('[')
323            .and_then(|s| s.strip_suffix(']'))
324            .ok_or_else(|| ScoreParseError {
325                message: format!("Hard score part '{}' must be wrapped in brackets", parts[0]),
326            })?;
327
328        let soft_part = parts[1]
329            .trim()
330            .strip_suffix("soft")
331            .and_then(|s| s.strip_prefix('['))
332            .and_then(|s| s.strip_suffix(']'))
333            .ok_or_else(|| ScoreParseError {
334                message: format!(
335                    "Soft score part '{}' must be wrapped in brackets and end with 'soft'",
336                    parts[1]
337                ),
338            })?;
339
340        let hard_scores: Result<Vec<i64>, _> = hard_part
341            .split('/')
342            .filter(|s| !s.is_empty())
343            .map(|s| {
344                s.trim().parse::<i64>().map_err(|e| ScoreParseError {
345                    message: format!("Invalid hard score '{}': {}", s, e),
346                })
347            })
348            .collect();
349
350        let soft_scores: Result<Vec<i64>, _> = soft_part
351            .split('/')
352            .filter(|s| !s.is_empty())
353            .map(|s| {
354                s.trim().parse::<i64>().map_err(|e| ScoreParseError {
355                    message: format!("Invalid soft score '{}': {}", s, e),
356                })
357            })
358            .collect();
359
360        Ok(BendableScore::of(hard_scores?, soft_scores?))
361    }
362
363    fn to_string_repr(&self) -> String {
364        format!("{}", self)
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[test]
373    fn test_creation() {
374        let score = BendableScore::of(vec![-1, -2], vec![-10, -20, -30]);
375        assert_eq!(score.hard_levels_count(), 2);
376        assert_eq!(score.soft_levels_count(), 3);
377        assert_eq!(score.hard_score(0), -1);
378        assert_eq!(score.hard_score(1), -2);
379        assert_eq!(score.soft_score(2), -30);
380    }
381
382    #[test]
383    fn test_feasibility() {
384        let feasible = BendableScore::of(vec![0, 0], vec![-10, -20]);
385        let infeasible = BendableScore::of(vec![0, -1], vec![0, 0]);
386
387        assert!(feasible.is_feasible());
388        assert!(!infeasible.is_feasible());
389    }
390
391    #[test]
392    fn test_comparison() {
393        // First hard level dominates
394        let s1 = BendableScore::of(vec![-1, 0], vec![0]);
395        let s2 = BendableScore::of(vec![0, -100], vec![-1000]);
396        assert!(s2 > s1);
397
398        // Second hard level matters when first is equal
399        let s3 = BendableScore::of(vec![0, -10], vec![0]);
400        let s4 = BendableScore::of(vec![0, -5], vec![-100]);
401        assert!(s4 > s3);
402    }
403
404    #[test]
405    fn test_arithmetic() {
406        let s1 = BendableScore::of(vec![-1], vec![-10, -20]);
407        let s2 = BendableScore::of(vec![-2], vec![-5, -10]);
408
409        let sum = s1.clone() + s2.clone();
410        assert_eq!(sum.hard_scores(), &[-3]);
411        assert_eq!(sum.soft_scores(), &[-15, -30]);
412
413        let neg = -s1;
414        assert_eq!(neg.hard_scores(), &[1]);
415        assert_eq!(neg.soft_scores(), &[10, 20]);
416    }
417
418    #[test]
419    fn test_parse() {
420        let score = BendableScore::parse("[0/-1]hard/[-10/-20/-30]soft").unwrap();
421        assert_eq!(score.hard_scores(), &[0, -1]);
422        assert_eq!(score.soft_scores(), &[-10, -20, -30]);
423    }
424
425    #[test]
426    fn test_display() {
427        let score = BendableScore::of(vec![0, -1], vec![-10, -20]);
428        assert_eq!(format!("{}", score), "[0/-1]hard/[-10/-20]soft");
429    }
430}