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