1#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5use std::cmp::Ordering;
6use std::fmt;
7use std::hash::{Hash, Hasher};
8use std::ops::{Add, Div, Mul, Sub};
9
10#[derive(Copy, Clone, Eq, PartialEq)]
12#[cfg_attr(feature = "serde", derive(Serialize))]
13#[repr(transparent)]
14pub struct DeterministicScore(i64);
15
16#[cfg(feature = "serde")]
17impl<'de> Deserialize<'de> for DeterministicScore {
18 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
19 where
20 D: serde::Deserializer<'de>,
21 {
22 let raw = i64::deserialize(deserializer)?;
23 DeterministicScore::from_raw_checked(raw).ok_or_else(|| {
24 serde::de::Error::custom(
25 "DeterministicScore raw value i64::MIN is the reserved sentinel and cannot be stored as a runtime value"
26 )
27 })
28 }
29}
30
31impl DeterministicScore {
32 const SCALE: f64 = 4_294_967_296.0; pub const MAX: Self = Self(i64::MAX);
36 pub const MIN: Self = Self(i64::MIN);
38 pub const NEG_INF: Self = Self(i64::MIN + 1);
40 pub const ZERO: Self = Self(0);
42
43 #[inline]
45 pub const fn from_raw(raw: i64) -> Self {
46 Self(raw)
47 }
48
49 #[inline]
51 pub const fn from_raw_saturating(raw: i64) -> Self {
52 if raw == i64::MIN {
53 Self::NEG_INF
54 } else {
55 Self(raw)
56 }
57 }
58
59 #[inline]
61 pub const fn from_raw_checked(raw: i64) -> Option<Self> {
62 if raw == i64::MIN {
63 None
64 } else {
65 Some(Self(raw))
66 }
67 }
68
69 #[inline]
71 pub const fn to_raw(self) -> i64 {
72 self.0
73 }
74
75 #[inline]
77 pub fn from_f64(val: f64) -> Self {
78 if val.is_nan() {
79 return Self::ZERO;
80 }
81 if val.is_infinite() {
82 return if val.is_sign_positive() {
83 Self::MAX
84 } else {
85 Self::NEG_INF
86 };
87 }
88
89 let scaled = (val * Self::SCALE).round();
90 Self::from_rounded_arithmetic(scaled)
91 }
92
93 #[inline]
95 pub fn from_f32(val: f32) -> Self {
96 Self::from_f64(val as f64)
97 }
98
99 #[inline]
101 pub fn to_f64(self) -> f64 {
102 if self.0 == Self::MAX.0 {
103 return f64::INFINITY;
104 }
105 if self.0 == Self::NEG_INF.0 {
106 return f64::NEG_INFINITY;
107 }
108 self.0 as f64 / Self::SCALE
109 }
110
111 #[inline]
113 pub const fn is_infinite(self) -> bool {
114 self.0 == Self::MAX.0 || self.0 == Self::NEG_INF.0
115 }
116
117 #[inline]
119 fn from_arithmetic_raw(raw: i128) -> Self {
120 if raw >= Self::MAX.0 as i128 {
121 Self::MAX
122 } else if raw <= Self::NEG_INF.0 as i128 {
123 Self::NEG_INF
124 } else {
125 Self(raw as i64)
126 }
127 }
128
129 #[inline]
131 fn from_rounded_arithmetic(raw: f64) -> Self {
132 if raw.is_nan() {
133 Self::ZERO
134 } else if raw.is_sign_positive() && !raw.is_finite() {
135 Self::MAX
136 } else if !raw.is_finite() {
137 Self::NEG_INF
138 } else if raw >= Self::MAX.0 as f64 {
139 Self::MAX
140 } else if raw <= Self::NEG_INF.0 as f64 {
141 Self::NEG_INF
142 } else {
143 Self(raw as i64)
144 }
145 }
146}
147
148impl Ord for DeterministicScore {
149 #[inline]
150 fn cmp(&self, other: &Self) -> Ordering {
151 self.0.cmp(&other.0)
152 }
153}
154
155impl PartialOrd for DeterministicScore {
156 #[inline]
157 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
158 Some(self.cmp(other))
159 }
160}
161
162impl Hash for DeterministicScore {
163 fn hash<H: Hasher>(&self, state: &mut H) {
164 self.0.hash(state);
165 }
166}
167
168impl Default for DeterministicScore {
169 fn default() -> Self {
170 Self::ZERO
171 }
172}
173
174impl Add for DeterministicScore {
175 type Output = Self;
176 #[inline]
177 fn add(self, rhs: Self) -> Self::Output {
178 Self::from_arithmetic_raw(self.0 as i128 + rhs.0 as i128)
179 }
180}
181
182impl Sub for DeterministicScore {
183 type Output = Self;
184 #[inline]
185 fn sub(self, rhs: Self) -> Self::Output {
186 Self::from_arithmetic_raw(self.0 as i128 - rhs.0 as i128)
187 }
188}
189
190impl Mul<i64> for DeterministicScore {
191 type Output = Self;
192 #[inline]
193 fn mul(self, rhs: i64) -> Self::Output {
194 let result = (self.0 as i128).saturating_mul(rhs as i128);
195 Self::from_arithmetic_raw(result)
196 }
197}
198
199impl Mul<f64> for DeterministicScore {
200 type Output = Self;
201 #[inline]
202 fn mul(self, rhs: f64) -> Self::Output {
203 if rhs.is_nan() {
204 return Self::ZERO;
205 }
206 let product = (self.0 as f64) * rhs;
207 Self::from_rounded_arithmetic(product.round())
208 }
209}
210
211impl Div<i64> for DeterministicScore {
212 type Output = Self;
213 #[inline]
214 fn div(self, rhs: i64) -> Self::Output {
215 if rhs == 0 {
216 return if self.0 == 0 {
217 Self::ZERO
218 } else if self.0 > 0 {
219 Self::MAX
220 } else {
221 Self::NEG_INF
222 };
223 }
224 Self::from_arithmetic_raw(self.0.saturating_div(rhs) as i128)
225 }
226}
227
228impl fmt::Debug for DeterministicScore {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 if *self == Self::MAX {
231 write!(f, "DeterministicScore(+Inf)")
232 } else if *self == Self::NEG_INF {
233 write!(f, "DeterministicScore(-Inf)")
234 } else {
235 write!(f, "DeterministicScore({:.9})", self.to_f64())
236 }
237 }
238}
239
240impl fmt::Display for DeterministicScore {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 if *self == Self::MAX {
243 write!(f, "+Inf")
244 } else if *self == Self::NEG_INF {
245 write!(f, "-Inf")
246 } else {
247 write!(f, "{:.6}", self.to_f64())
248 }
249 }
250}
251
252impl From<f64> for DeterministicScore {
253 fn from(val: f64) -> Self {
254 Self::from_f64(val)
255 }
256}
257
258impl From<f32> for DeterministicScore {
259 fn from(val: f32) -> Self {
260 Self::from_f32(val)
261 }
262}
263
264impl From<DeterministicScore> for f64 {
265 fn from(score: DeterministicScore) -> Self {
266 score.to_f64()
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn roundtrip_f64() {
276 let s = DeterministicScore::from_f64(0.5);
277 assert!((s.to_f64() - 0.5).abs() < 1e-9);
278 }
279
280 #[test]
281 fn nan_maps_to_zero() {
282 let s = DeterministicScore::from_f64(f64::NAN);
283 assert_eq!(s, DeterministicScore::ZERO);
284 assert!((s.to_f64() - 0.0).abs() < 1e-15);
285 }
286
287 #[test]
288 fn infinity() {
289 assert_eq!(
290 DeterministicScore::from_f64(f64::INFINITY),
291 DeterministicScore::MAX
292 );
293 assert_eq!(
294 DeterministicScore::from_f64(f64::NEG_INFINITY),
295 DeterministicScore::NEG_INF
296 );
297 }
298
299 #[test]
300 fn ordering() {
301 let a = DeterministicScore::from_f64(0.1);
302 let b = DeterministicScore::from_f64(0.5);
303 assert!(a < b);
304 }
305
306 #[test]
307 fn arithmetic() {
308 let a = DeterministicScore::from_f64(0.3);
309 let b = DeterministicScore::from_f64(0.4);
310 let sum = a + b;
311 assert!((sum.to_f64() - 0.7).abs() < 1e-9);
312 }
313
314 #[test]
315 fn scaling_by_f64() {
316 let s = DeterministicScore::from_f64(0.5);
317 let scaled = s * 2.0;
318 assert!((scaled.to_f64() - 1.0).abs() < 1e-9);
319 }
320
321 #[test]
322 fn div_by_zero() {
323 let pos = DeterministicScore::from_f64(0.5);
324 assert_eq!(pos / 0, DeterministicScore::MAX);
325 let neg = DeterministicScore::from_f64(-0.5);
326 assert_eq!(neg / 0, DeterministicScore::NEG_INF);
327 assert_eq!(DeterministicScore::ZERO / 0, DeterministicScore::ZERO);
328 }
329
330 #[test]
331 fn raw_scale_known_value() {
332 assert_eq!(
333 DeterministicScore::from_f64(1.0).to_raw(),
334 4_294_967_296_i64
335 );
336 }
337
338 #[test]
339 fn display_formatting() {
340 let s = format!("{}", DeterministicScore::from_f64(0.1234567));
341 assert_eq!(s, "0.123457");
342 assert_eq!(format!("{}", DeterministicScore::MAX), "+Inf");
343 assert_eq!(format!("{}", DeterministicScore::NEG_INF), "-Inf");
344 }
345
346 #[test]
347 fn debug_formatting() {
348 assert_eq!(
349 format!("{:?}", DeterministicScore::MAX),
350 "DeterministicScore(+Inf)"
351 );
352 assert_eq!(
353 format!("{:?}", DeterministicScore::NEG_INF),
354 "DeterministicScore(-Inf)"
355 );
356 let s = format!("{:?}", DeterministicScore::from_f64(0.5));
357 assert!(
358 s.starts_with("DeterministicScore(0.5"),
359 "unexpected debug output: {s}"
360 );
361 }
362
363 #[test]
364 fn add_saturates_at_max() {
365 assert_eq!(
366 DeterministicScore::MAX + DeterministicScore::from_raw(1),
367 DeterministicScore::MAX
368 );
369 }
370
371 #[test]
372 fn sub_saturates_at_neg_inf() {
373 assert_eq!(
374 DeterministicScore::NEG_INF - DeterministicScore::from_raw(1),
375 DeterministicScore::NEG_INF
376 );
377 }
378
379 #[test]
380 fn mul_i64_saturates_at_max() {
381 let large = DeterministicScore::from_raw(i64::MAX / 2);
382 assert_eq!(large * 3_i64, DeterministicScore::MAX);
383 }
384
385 #[test]
386 fn mul_f64_nan_yields_zero() {
387 let s = DeterministicScore::from_f64(1.0);
388 assert_eq!(s * f64::NAN, DeterministicScore::ZERO);
389 }
390
391 #[test]
393 fn neg_inf_is_i64_min_plus_one() {
394 assert_eq!(DeterministicScore::NEG_INF.to_raw(), i64::MIN + 1);
395 }
396
397 #[test]
398 fn min_sentinel_is_i64_min() {
399 assert_eq!(DeterministicScore::MIN.to_raw(), i64::MIN);
400 }
401
402 #[test]
403 fn min_sentinel_distinct_from_neg_inf() {
404 assert_ne!(DeterministicScore::MIN, DeterministicScore::NEG_INF);
405 assert!(DeterministicScore::MIN < DeterministicScore::NEG_INF);
406 }
407
408 #[test]
409 fn neg_infinity_maps_to_neg_inf() {
410 assert_eq!(
411 DeterministicScore::from_f64(f64::NEG_INFINITY),
412 DeterministicScore::NEG_INF
413 );
414 }
415
416 #[test]
417 fn underflow_clamps_to_neg_inf_not_min() {
418 let result = DeterministicScore::from_raw(i64::MIN + 1) - DeterministicScore::from_raw(1);
420 assert_eq!(result, DeterministicScore::NEG_INF);
421 assert_ne!(result, DeterministicScore::MIN);
422 }
423
424 #[test]
427 fn from_raw_saturating_min_maps_to_neg_inf() {
428 let s = DeterministicScore::from_raw_saturating(i64::MIN);
429 assert_eq!(
430 s,
431 DeterministicScore::NEG_INF,
432 "i64::MIN must saturate to NEG_INF, not the reserved MIN sentinel"
433 );
434 }
435
436 #[test]
437 fn from_raw_saturating_neg_inf_raw_is_identity() {
438 let s = DeterministicScore::from_raw_saturating(i64::MIN + 1);
439 assert_eq!(s, DeterministicScore::NEG_INF);
440 }
441
442 #[test]
443 fn from_raw_saturating_normal_value_is_identity() {
444 let s = DeterministicScore::from_raw_saturating(42);
445 assert_eq!(s.to_raw(), 42);
446 }
447
448 #[test]
449 fn from_raw_checked_min_returns_none() {
450 assert!(
451 DeterministicScore::from_raw_checked(i64::MIN).is_none(),
452 "i64::MIN should be rejected by from_raw_checked"
453 );
454 }
455
456 #[test]
457 fn from_raw_checked_valid_value_returns_some() {
458 let s = DeterministicScore::from_raw_checked(i64::MIN + 1).unwrap();
459 assert_eq!(s, DeterministicScore::NEG_INF);
460 }
461
462 #[test]
463 fn from_raw_checked_zero_returns_some() {
464 let s = DeterministicScore::from_raw_checked(0).unwrap();
465 assert_eq!(s, DeterministicScore::ZERO);
466 }
467
468 #[cfg(feature = "serde")]
471 #[test]
472 fn serde_deserialize_rejects_i64_min() {
473 let raw_json = format!("{}", i64::MIN);
474 let result: Result<DeterministicScore, _> = serde_json::from_str(&raw_json);
475 assert!(
476 result.is_err(),
477 "deserializing i64::MIN must fail: got {:?}",
478 result
479 );
480 }
481
482 #[cfg(feature = "serde")]
483 #[test]
484 fn serde_deserialize_accepts_neg_inf_raw() {
485 let raw_json = format!("{}", i64::MIN + 1);
486 let s: DeterministicScore = serde_json::from_str(&raw_json).unwrap();
487 assert_eq!(s, DeterministicScore::NEG_INF);
488 }
489
490 #[cfg(feature = "serde")]
491 #[test]
492 fn serde_roundtrip_zero() {
493 let original = DeterministicScore::ZERO;
494 let json = serde_json::to_string(&original).unwrap();
495 let restored: DeterministicScore = serde_json::from_str(&json).unwrap();
496 assert_eq!(original, restored);
497 }
498}