1use std::cell::Cell;
4use std::fmt;
5use std::ops::{Add, Neg, Sub};
6
7use solverforge_core::score::{ParseableScore, Score, ScoreLevel, ScoreParseError};
8
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
11pub enum DynamicScoreFamily {
12 Soft,
13 HardSoft,
14 HardSoftDecimal,
15 #[default]
16 HardMediumSoft,
17}
18
19thread_local! {
20 static ACTIVE_SCORE_FAMILY: Cell<DynamicScoreFamily> =
21 const { Cell::new(DynamicScoreFamily::HardMediumSoft) };
22}
23
24pub fn scoped_dynamic_score_family<T>(
33 family: DynamicScoreFamily,
34 callback: impl FnOnce() -> T,
35) -> T {
36 ACTIVE_SCORE_FAMILY.with(|active| {
37 let _guard = ActiveScoreFamilyGuard {
38 previous: active.replace(family),
39 active,
40 };
41 callback()
42 })
43}
44
45pub fn active_dynamic_score_family() -> DynamicScoreFamily {
47 ACTIVE_SCORE_FAMILY.with(Cell::get)
48}
49
50struct ActiveScoreFamilyGuard<'a> {
51 previous: DynamicScoreFamily,
52 active: &'a Cell<DynamicScoreFamily>,
53}
54
55impl Drop for ActiveScoreFamilyGuard<'_> {
56 fn drop(&mut self) {
57 self.active.set(self.previous);
58 }
59}
60
61#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
69pub struct DynamicScore {
70 pub hard: i64,
71 pub medium: i64,
72 pub soft: i64,
73}
74
75impl DynamicScore {
76 pub const ZERO: Self = Self::of(0, 0, 0);
77
78 pub const fn of(hard: i64, medium: i64, soft: i64) -> Self {
79 Self { hard, medium, soft }
80 }
81
82 pub const fn soft(soft: i64) -> Self {
83 Self::of(0, 0, soft)
84 }
85
86 pub const fn hard_soft(hard: i64, soft: i64) -> Self {
87 Self::of(hard, 0, soft)
88 }
89
90 pub fn family_levels(self, family: DynamicScoreFamily) -> Vec<i64> {
91 match family {
92 DynamicScoreFamily::Soft => vec![self.soft],
93 DynamicScoreFamily::HardSoft | DynamicScoreFamily::HardSoftDecimal => {
94 vec![self.hard, self.soft]
95 }
96 DynamicScoreFamily::HardMediumSoft => vec![self.hard, self.medium, self.soft],
97 }
98 }
99}
100
101impl fmt::Debug for DynamicScore {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 write!(
104 f,
105 "DynamicScore({}, {}, {})",
106 self.hard, self.medium, self.soft
107 )
108 }
109}
110
111impl fmt::Display for DynamicScore {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 match active_dynamic_score_family() {
114 DynamicScoreFamily::Soft => write!(f, "{}", self.soft),
115 DynamicScoreFamily::HardSoft => write!(f, "{}hard/{}soft", self.hard, self.soft),
116 DynamicScoreFamily::HardSoftDecimal => write!(
117 f,
118 "{}hard/{}soft",
119 format_decimal_score_part(self.hard),
120 format_decimal_score_part(self.soft)
121 ),
122 DynamicScoreFamily::HardMediumSoft => write!(
123 f,
124 "{}hard/{}medium/{}soft",
125 self.hard, self.medium, self.soft
126 ),
127 }
128 }
129}
130
131impl Add for DynamicScore {
132 type Output = Self;
133
134 fn add(self, rhs: Self) -> Self::Output {
135 Self::of(
136 self.hard + rhs.hard,
137 self.medium + rhs.medium,
138 self.soft + rhs.soft,
139 )
140 }
141}
142
143impl Sub for DynamicScore {
144 type Output = Self;
145
146 fn sub(self, rhs: Self) -> Self::Output {
147 Self::of(
148 self.hard - rhs.hard,
149 self.medium - rhs.medium,
150 self.soft - rhs.soft,
151 )
152 }
153}
154
155impl Neg for DynamicScore {
156 type Output = Self;
157
158 fn neg(self) -> Self::Output {
159 Self::of(-self.hard, -self.medium, -self.soft)
160 }
161}
162
163impl Score for DynamicScore {
164 fn is_feasible(&self) -> bool {
165 self.hard >= 0
166 }
167
168 fn zero() -> Self {
169 Self::ZERO
170 }
171
172 fn levels_count() -> usize {
173 3
174 }
175
176 fn level_number(&self, index: usize) -> i64 {
177 match index {
178 0 => self.hard,
179 1 => self.medium,
180 2 => self.soft,
181 _ => panic!("DynamicScore has 3 levels, got index {index}"),
182 }
183 }
184
185 fn from_level_numbers(levels: &[i64]) -> Self {
186 match levels {
187 [soft] => Self::soft(*soft),
188 [hard, soft] => Self::hard_soft(*hard, *soft),
189 [hard, medium, soft] => Self::of(*hard, *medium, *soft),
190 _ => panic!("DynamicScore requires 1, 2, or 3 levels"),
191 }
192 }
193
194 fn multiply(&self, multiplicand: f64) -> Self {
195 Self::of(
196 (self.hard as f64 * multiplicand).round() as i64,
197 (self.medium as f64 * multiplicand).round() as i64,
198 (self.soft as f64 * multiplicand).round() as i64,
199 )
200 }
201
202 fn divide(&self, divisor: f64) -> Self {
203 self.multiply(1.0 / divisor)
204 }
205
206 fn abs(&self) -> Self {
207 Self::of(self.hard.abs(), self.medium.abs(), self.soft.abs())
208 }
209
210 fn to_scalar(&self) -> f64 {
211 self.hard as f64 * 1_000_000.0 + self.medium as f64 * 1_000.0 + self.soft as f64
212 }
213
214 fn level_label(index: usize) -> ScoreLevel {
215 match index {
216 0 => ScoreLevel::Hard,
217 1 => ScoreLevel::Medium,
218 2 => ScoreLevel::Soft,
219 _ => panic!("DynamicScore has 3 levels, got index {index}"),
220 }
221 }
222}
223
224impl ParseableScore for DynamicScore {
225 fn parse(s: &str) -> Result<Self, ScoreParseError> {
229 let decimal_family = active_dynamic_score_family() == DynamicScoreFamily::HardSoftDecimal;
230 let mut hard = (0, false);
231 let mut medium = None;
232 let mut soft = (0, false);
233 let mut has_decimal_literal = false;
234 for part in s.split('/') {
235 if let Some(raw) = part.strip_suffix("hard") {
236 hard = parse_score_part(raw, "hard")?;
237 has_decimal_literal |= hard.1;
238 } else if let Some(raw) = part.strip_suffix("medium") {
239 let (value, decimal) = parse_score_part(raw, "medium")?;
240 medium = Some(value);
241 has_decimal_literal |= decimal;
242 } else if let Some(raw) = part.strip_suffix("soft") {
243 soft = parse_score_part(raw, "soft")?;
244 has_decimal_literal |= soft.1;
245 } else if let Ok(value) = part.parse::<i64>() {
246 soft = (value, false);
247 } else {
248 return Err(ScoreParseError {
249 message: format!("invalid dynamic score `{s}`"),
250 });
251 }
252 }
253 if let Some(medium) = medium {
254 return Ok(Self::of(hard.0, medium, soft.0));
255 }
256 let scale = |(value, decimal): (i64, bool)| {
257 if (decimal_family || has_decimal_literal) && !decimal {
258 value * DECIMAL_SCALE
259 } else {
260 value
261 }
262 };
263 Ok(Self::hard_soft(scale(hard), scale(soft)))
264 }
265
266 fn to_string_repr(&self) -> String {
267 self.to_string()
268 }
269}
270
271const DECIMAL_SCALE: i64 = 100_000;
272
273fn format_decimal_score_part(scaled: i64) -> String {
274 if scaled % DECIMAL_SCALE == 0 {
275 return (scaled / DECIMAL_SCALE).to_string();
276 }
277 let value = scaled as f64 / DECIMAL_SCALE as f64;
278 format!("{value:.6}")
279 .trim_end_matches('0')
280 .trim_end_matches('.')
281 .to_string()
282}
283
284fn parse_score_part(raw: &str, label: &str) -> Result<(i64, bool), ScoreParseError> {
285 if raw.contains('.') {
286 let value = raw.parse::<f64>().map_err(|_| ScoreParseError {
287 message: format!("invalid {label} score `{raw}`"),
288 })?;
289 return Ok(((value * DECIMAL_SCALE as f64).round() as i64, true));
290 }
291 raw.parse::<i64>()
292 .map(|value| (value, false))
293 .map_err(|_| ScoreParseError {
294 message: format!("invalid {label} score `{raw}`"),
295 })
296}