1use std::cell::Cell;
4use std::cmp::Ordering;
5use std::fmt;
6use std::ops::{Add, Neg, Sub};
7
8use solverforge_core::score::{ParseableScore, Score, ScoreLevel, ScoreParseError};
9
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
12pub enum DynamicScoreFamily {
13 Soft,
14 HardSoft,
15 HardSoftDecimal,
16 #[default]
17 HardMediumSoft,
18}
19
20thread_local! {
21 static ACTIVE_SCORE_FAMILY: Cell<DynamicScoreFamily> =
22 const { Cell::new(DynamicScoreFamily::HardMediumSoft) };
23}
24
25pub fn scoped_dynamic_score_family<T>(
31 family: DynamicScoreFamily,
32 callback: impl FnOnce() -> T,
33) -> T {
34 ACTIVE_SCORE_FAMILY.with(|active| {
35 let _guard = ActiveScoreFamilyGuard {
36 previous: active.replace(family),
37 active,
38 };
39 callback()
40 })
41}
42
43fn active_score_family() -> DynamicScoreFamily {
44 ACTIVE_SCORE_FAMILY.with(Cell::get)
45}
46
47struct ActiveScoreFamilyGuard<'a> {
48 previous: DynamicScoreFamily,
49 active: &'a Cell<DynamicScoreFamily>,
50}
51
52impl Drop for ActiveScoreFamilyGuard<'_> {
53 fn drop(&mut self) {
54 self.active.set(self.previous);
55 }
56}
57
58#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
66pub struct DynamicScore {
67 pub hard: i64,
68 pub medium: i64,
69 pub soft: i64,
70 pub family: DynamicScoreFamily,
71}
72
73impl DynamicScore {
74 pub const ZERO: Self = Self {
75 hard: 0,
76 medium: 0,
77 soft: 0,
78 family: DynamicScoreFamily::HardMediumSoft,
79 };
80
81 pub const fn of(hard: i64, medium: i64, soft: i64) -> Self {
82 Self {
83 hard,
84 medium,
85 soft,
86 family: DynamicScoreFamily::HardMediumSoft,
87 }
88 }
89
90 pub const fn with_family(
91 hard: i64,
92 medium: i64,
93 soft: i64,
94 family: DynamicScoreFamily,
95 ) -> Self {
96 Self {
97 hard,
98 medium,
99 soft,
100 family,
101 }
102 }
103
104 pub const fn soft(soft: i64) -> Self {
105 Self::with_family(0, 0, soft, DynamicScoreFamily::Soft)
106 }
107
108 pub const fn hard_soft(hard: i64, soft: i64) -> Self {
109 Self::with_family(hard, 0, soft, DynamicScoreFamily::HardSoft)
110 }
111
112 pub const fn hard_soft_decimal(hard_scaled: i64, soft_scaled: i64) -> Self {
113 Self::with_family(
114 hard_scaled,
115 0,
116 soft_scaled,
117 DynamicScoreFamily::HardSoftDecimal,
118 )
119 }
120
121 pub const fn hard_medium_soft(hard: i64, medium: i64, soft: i64) -> Self {
122 Self::with_family(hard, medium, soft, DynamicScoreFamily::HardMediumSoft)
123 }
124
125 pub const fn zero_for_family(family: DynamicScoreFamily) -> Self {
126 Self::with_family(0, 0, 0, family)
127 }
128
129 pub fn family_levels(self, family: DynamicScoreFamily) -> Vec<i64> {
130 match family {
131 DynamicScoreFamily::Soft => vec![self.soft],
132 DynamicScoreFamily::HardSoft | DynamicScoreFamily::HardSoftDecimal => {
133 vec![self.hard, self.soft]
134 }
135 DynamicScoreFamily::HardMediumSoft => vec![self.hard, self.medium, self.soft],
136 }
137 }
138
139 fn is_zero(self) -> bool {
140 self.hard == 0 && self.medium == 0 && self.soft == 0
141 }
142
143 fn combined_family(self, rhs: Self) -> DynamicScoreFamily {
144 if self.family == rhs.family {
145 return self.family;
146 }
147 if self.is_zero() {
148 return rhs.family;
149 }
150 if rhs.is_zero() {
151 return self.family;
152 }
153 DynamicScoreFamily::HardMediumSoft
154 }
155}
156
157impl fmt::Debug for DynamicScore {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 write!(
160 f,
161 "DynamicScore({}, {}, {}, {:?})",
162 self.hard, self.medium, self.soft, self.family
163 )
164 }
165}
166
167impl fmt::Display for DynamicScore {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 match self.family {
170 DynamicScoreFamily::Soft => write!(f, "{}", self.soft),
171 DynamicScoreFamily::HardSoft => write!(f, "{}hard/{}soft", self.hard, self.soft),
172 DynamicScoreFamily::HardSoftDecimal => write!(
173 f,
174 "{}hard/{}soft",
175 format_decimal_score_part(self.hard),
176 format_decimal_score_part(self.soft)
177 ),
178 DynamicScoreFamily::HardMediumSoft => write!(
179 f,
180 "{}hard/{}medium/{}soft",
181 self.hard, self.medium, self.soft
182 ),
183 }
184 }
185}
186
187impl Ord for DynamicScore {
188 fn cmp(&self, other: &Self) -> Ordering {
189 (self.hard, self.medium, self.soft).cmp(&(other.hard, other.medium, other.soft))
190 }
191}
192
193impl PartialOrd for DynamicScore {
194 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
195 Some(self.cmp(other))
196 }
197}
198
199impl Add for DynamicScore {
200 type Output = Self;
201
202 fn add(self, rhs: Self) -> Self::Output {
203 Self::with_family(
204 self.hard + rhs.hard,
205 self.medium + rhs.medium,
206 self.soft + rhs.soft,
207 self.combined_family(rhs),
208 )
209 }
210}
211
212impl Sub for DynamicScore {
213 type Output = Self;
214
215 fn sub(self, rhs: Self) -> Self::Output {
216 Self::with_family(
217 self.hard - rhs.hard,
218 self.medium - rhs.medium,
219 self.soft - rhs.soft,
220 self.combined_family(rhs),
221 )
222 }
223}
224
225impl Neg for DynamicScore {
226 type Output = Self;
227
228 fn neg(self) -> Self::Output {
229 Self::with_family(-self.hard, -self.medium, -self.soft, self.family)
230 }
231}
232
233impl Score for DynamicScore {
234 fn is_feasible(&self) -> bool {
235 self.hard >= 0
236 }
237
238 fn zero() -> Self {
239 Self::zero_for_family(active_score_family())
240 }
241
242 fn levels_count() -> usize {
243 3
244 }
245
246 fn level_number(&self, index: usize) -> i64 {
247 match index {
248 0 => self.hard,
249 1 => self.medium,
250 2 => self.soft,
251 _ => panic!("DynamicScore has 3 levels, got index {index}"),
252 }
253 }
254
255 fn from_level_numbers(levels: &[i64]) -> Self {
256 match (active_score_family(), levels) {
257 (DynamicScoreFamily::Soft, [soft]) => Self::soft(*soft),
258 (DynamicScoreFamily::Soft, [_, _, soft]) => Self::soft(*soft),
259 (DynamicScoreFamily::HardSoft, [hard, soft]) => Self::hard_soft(*hard, *soft),
260 (DynamicScoreFamily::HardSoft, [hard, _, soft]) => Self::hard_soft(*hard, *soft),
261 (DynamicScoreFamily::HardSoftDecimal, [hard, soft]) => {
262 Self::hard_soft_decimal(*hard, *soft)
263 }
264 (DynamicScoreFamily::HardSoftDecimal, [hard, _, soft]) => {
265 Self::hard_soft_decimal(*hard, *soft)
266 }
267 (DynamicScoreFamily::HardMediumSoft, [soft]) => Self::soft(*soft),
268 (DynamicScoreFamily::HardMediumSoft, [hard, soft]) => Self::hard_soft(*hard, *soft),
269 (DynamicScoreFamily::HardMediumSoft, [hard, medium, soft]) => {
270 Self::hard_medium_soft(*hard, *medium, *soft)
271 }
272 _ => panic!("DynamicScore requires 1, 2, or 3 levels"),
273 }
274 }
275
276 fn multiply(&self, multiplicand: f64) -> Self {
277 Self::with_family(
278 (self.hard as f64 * multiplicand).round() as i64,
279 (self.medium as f64 * multiplicand).round() as i64,
280 (self.soft as f64 * multiplicand).round() as i64,
281 self.family,
282 )
283 }
284
285 fn divide(&self, divisor: f64) -> Self {
286 self.multiply(1.0 / divisor)
287 }
288
289 fn abs(&self) -> Self {
290 Self::with_family(
291 self.hard.abs(),
292 self.medium.abs(),
293 self.soft.abs(),
294 self.family,
295 )
296 }
297
298 fn to_scalar(&self) -> f64 {
299 self.hard as f64 * 1_000_000.0 + self.medium as f64 * 1_000.0 + self.soft as f64
300 }
301
302 fn level_label(index: usize) -> ScoreLevel {
303 match index {
304 0 => ScoreLevel::Hard,
305 1 => ScoreLevel::Medium,
306 2 => ScoreLevel::Soft,
307 _ => panic!("DynamicScore has 3 levels, got index {index}"),
308 }
309 }
310}
311
312impl ParseableScore for DynamicScore {
313 fn parse(s: &str) -> Result<Self, ScoreParseError> {
314 let mut hard = 0;
315 let mut medium = 0;
316 let mut soft = 0;
317 let mut has_hard = false;
318 let mut has_medium = false;
319 let mut has_soft = false;
320 let mut hard_is_decimal = false;
321 let mut soft_is_decimal = false;
322 let mut is_decimal = false;
323 for part in s.split('/') {
324 if let Some(raw) = part.strip_suffix("hard") {
325 has_hard = true;
326 let (value, decimal) = parse_score_part(raw, "hard")?;
327 hard = value;
328 hard_is_decimal = decimal;
329 is_decimal |= decimal;
330 } else if let Some(raw) = part.strip_suffix("medium") {
331 has_medium = true;
332 let (value, decimal) = parse_score_part(raw, "medium")?;
333 medium = value;
334 is_decimal |= decimal;
335 } else if let Some(raw) = part.strip_suffix("soft") {
336 has_soft = true;
337 let (value, decimal) = parse_score_part(raw, "soft")?;
338 soft = value;
339 soft_is_decimal = decimal;
340 is_decimal |= decimal;
341 } else if let Ok(value) = part.parse::<i64>() {
342 soft = value;
343 } else {
344 return Err(ScoreParseError {
345 message: format!("invalid dynamic score `{s}`"),
346 });
347 }
348 }
349 if has_medium {
350 Ok(Self::hard_medium_soft(hard, medium, soft))
351 } else if has_hard || has_soft {
352 if is_decimal {
353 if has_hard && !hard_is_decimal {
354 hard *= DECIMAL_SCALE;
355 }
356 if has_soft && !soft_is_decimal {
357 soft *= DECIMAL_SCALE;
358 }
359 Ok(Self::hard_soft_decimal(hard, soft))
360 } else {
361 Ok(Self::hard_soft(hard, soft))
362 }
363 } else {
364 Ok(Self::soft(soft))
365 }
366 }
367
368 fn to_string_repr(&self) -> String {
369 self.to_string()
370 }
371}
372
373const DECIMAL_SCALE: i64 = 100_000;
374
375fn format_decimal_score_part(scaled: i64) -> String {
376 if scaled % DECIMAL_SCALE == 0 {
377 return (scaled / DECIMAL_SCALE).to_string();
378 }
379 let value = scaled as f64 / DECIMAL_SCALE as f64;
380 format!("{value:.6}")
381 .trim_end_matches('0')
382 .trim_end_matches('.')
383 .to_string()
384}
385
386fn parse_score_part(raw: &str, label: &str) -> Result<(i64, bool), ScoreParseError> {
387 if raw.contains('.') {
388 let value = raw.parse::<f64>().map_err(|_| ScoreParseError {
389 message: format!("invalid {label} score `{raw}`"),
390 })?;
391 return Ok(((value * DECIMAL_SCALE as f64).round() as i64, true));
392 }
393 raw.parse::<i64>()
394 .map(|value| (value, false))
395 .map_err(|_| ScoreParseError {
396 message: format!("invalid {label} score `{raw}`"),
397 })
398}