1#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8use std::cmp::Ordering;
9use std::fmt;
10use std::hash::{Hash, Hasher};
11use std::ops::{Add, Div, Mul, Sub};
12
13#[derive(Copy, Clone, Eq, PartialEq)]
14#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
15#[repr(transparent)]
16pub struct DeterministicScore(i64);
17
18impl DeterministicScore {
19 const SCALE: f64 = 4_294_967_296.0; pub const MAX: Self = Self(i64::MAX);
22 pub const MIN: Self = Self(i64::MIN);
27 pub const NEG_INF: Self = Self(i64::MIN + 1);
30 pub const ZERO: Self = Self(0);
31
32 #[inline]
33 pub const fn from_raw(raw: i64) -> Self {
34 Self(raw)
35 }
36
37 #[inline]
38 pub const fn to_raw(self) -> i64 {
39 self.0
40 }
41
42 #[inline]
43 pub fn from_f64(val: f64) -> Self {
44 if val.is_nan() {
45 return Self::ZERO;
46 }
47 if val.is_infinite() {
48 return if val.is_sign_positive() {
49 Self::MAX
50 } else {
51 Self::NEG_INF
52 };
53 }
54
55 let scaled = (val * Self::SCALE).round();
56 Self::from_rounded_arithmetic(scaled)
57 }
58
59 #[inline]
60 pub fn from_f32(val: f32) -> Self {
61 Self::from_f64(val as f64)
62 }
63
64 #[inline]
65 pub fn to_f64(self) -> f64 {
66 if self.0 == Self::MAX.0 {
67 return f64::INFINITY;
68 }
69 if self.0 == Self::NEG_INF.0 {
70 return f64::NEG_INFINITY;
71 }
72 self.0 as f64 / Self::SCALE
73 }
74
75 #[inline]
76 pub const fn is_infinite(self) -> bool {
77 self.0 == Self::MAX.0 || self.0 == Self::NEG_INF.0
78 }
79
80 #[inline]
83 fn from_arithmetic_raw(raw: i128) -> Self {
84 if raw >= Self::MAX.0 as i128 {
85 Self::MAX
86 } else if raw <= Self::NEG_INF.0 as i128 {
87 Self::NEG_INF
88 } else {
89 Self(raw as i64)
90 }
91 }
92
93 #[inline]
96 fn from_rounded_arithmetic(raw: f64) -> Self {
97 if raw.is_nan() {
98 Self::ZERO
99 } else if raw.is_sign_positive() && !raw.is_finite() {
100 Self::MAX
101 } else if !raw.is_finite() {
102 Self::NEG_INF
103 } else if raw >= Self::MAX.0 as f64 {
104 Self::MAX
105 } else if raw <= Self::NEG_INF.0 as f64 {
106 Self::NEG_INF
107 } else {
108 Self(raw as i64)
109 }
110 }
111}
112
113impl Ord for DeterministicScore {
114 #[inline]
115 fn cmp(&self, other: &Self) -> Ordering {
116 self.0.cmp(&other.0)
117 }
118}
119
120impl PartialOrd for DeterministicScore {
121 #[inline]
122 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
123 Some(self.cmp(other))
124 }
125}
126
127impl Hash for DeterministicScore {
128 fn hash<H: Hasher>(&self, state: &mut H) {
129 self.0.hash(state);
130 }
131}
132
133impl Default for DeterministicScore {
134 fn default() -> Self {
135 Self::ZERO
136 }
137}
138
139impl Add for DeterministicScore {
140 type Output = Self;
141 #[inline]
142 fn add(self, rhs: Self) -> Self::Output {
143 Self::from_arithmetic_raw(self.0 as i128 + rhs.0 as i128)
144 }
145}
146
147impl Sub for DeterministicScore {
148 type Output = Self;
149 #[inline]
150 fn sub(self, rhs: Self) -> Self::Output {
151 Self::from_arithmetic_raw(self.0 as i128 - rhs.0 as i128)
152 }
153}
154
155impl Mul<i64> for DeterministicScore {
156 type Output = Self;
157 #[inline]
158 fn mul(self, rhs: i64) -> Self::Output {
159 let result = (self.0 as i128).saturating_mul(rhs as i128);
160 Self::from_arithmetic_raw(result)
161 }
162}
163
164impl Mul<f64> for DeterministicScore {
165 type Output = Self;
166 #[inline]
167 fn mul(self, rhs: f64) -> Self::Output {
168 if rhs.is_nan() {
169 return Self::ZERO;
170 }
171 let product = (self.0 as f64) * rhs;
172 Self::from_rounded_arithmetic(product.round())
173 }
174}
175
176impl Div<i64> for DeterministicScore {
177 type Output = Self;
178 #[inline]
179 fn div(self, rhs: i64) -> Self::Output {
180 if rhs == 0 {
181 return if self.0 == 0 {
182 Self::ZERO
183 } else if self.0 > 0 {
184 Self::MAX
185 } else {
186 Self::NEG_INF
187 };
188 }
189 Self::from_arithmetic_raw(self.0.saturating_div(rhs) as i128)
190 }
191}
192
193impl fmt::Debug for DeterministicScore {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 if *self == Self::MAX {
196 write!(f, "DeterministicScore(+Inf)")
197 } else if *self == Self::NEG_INF {
198 write!(f, "DeterministicScore(-Inf)")
199 } else {
200 write!(f, "DeterministicScore({:.9})", self.to_f64())
201 }
202 }
203}
204
205impl fmt::Display for DeterministicScore {
206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207 if *self == Self::MAX {
208 write!(f, "+Inf")
209 } else if *self == Self::NEG_INF {
210 write!(f, "-Inf")
211 } else {
212 write!(f, "{:.6}", self.to_f64())
213 }
214 }
215}
216
217impl From<f64> for DeterministicScore {
218 fn from(val: f64) -> Self {
219 Self::from_f64(val)
220 }
221}
222
223impl From<f32> for DeterministicScore {
224 fn from(val: f32) -> Self {
225 Self::from_f32(val)
226 }
227}
228
229impl From<DeterministicScore> for f64 {
230 fn from(score: DeterministicScore) -> Self {
231 score.to_f64()
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn roundtrip_f64() {
241 let s = DeterministicScore::from_f64(0.5);
242 assert!((s.to_f64() - 0.5).abs() < 1e-9);
243 }
244
245 #[test]
246 fn nan_maps_to_zero() {
247 let s = DeterministicScore::from_f64(f64::NAN);
248 assert_eq!(s, DeterministicScore::ZERO);
249 assert!((s.to_f64() - 0.0).abs() < 1e-15);
250 }
251
252 #[test]
253 fn infinity() {
254 assert_eq!(
255 DeterministicScore::from_f64(f64::INFINITY),
256 DeterministicScore::MAX
257 );
258 assert_eq!(
259 DeterministicScore::from_f64(f64::NEG_INFINITY),
260 DeterministicScore::NEG_INF
261 );
262 }
263
264 #[test]
265 fn ordering() {
266 let a = DeterministicScore::from_f64(0.1);
267 let b = DeterministicScore::from_f64(0.5);
268 assert!(a < b);
269 }
270
271 #[test]
272 fn arithmetic() {
273 let a = DeterministicScore::from_f64(0.3);
274 let b = DeterministicScore::from_f64(0.4);
275 let sum = a + b;
276 assert!((sum.to_f64() - 0.7).abs() < 1e-9);
277 }
278
279 #[test]
280 fn scaling_by_f64() {
281 let s = DeterministicScore::from_f64(0.5);
282 let scaled = s * 2.0;
283 assert!((scaled.to_f64() - 1.0).abs() < 1e-9);
284 }
285
286 #[test]
287 fn div_by_zero() {
288 let pos = DeterministicScore::from_f64(0.5);
289 assert_eq!(pos / 0, DeterministicScore::MAX);
290 let neg = DeterministicScore::from_f64(-0.5);
291 assert_eq!(neg / 0, DeterministicScore::NEG_INF);
292 assert_eq!(DeterministicScore::ZERO / 0, DeterministicScore::ZERO);
293 }
294
295 #[test]
296 fn raw_scale_known_value() {
297 assert_eq!(
298 DeterministicScore::from_f64(1.0).to_raw(),
299 4_294_967_296_i64
300 );
301 }
302
303 #[test]
304 fn display_formatting() {
305 let s = format!("{}", DeterministicScore::from_f64(0.1234567));
306 assert_eq!(s, "0.123457");
307 assert_eq!(format!("{}", DeterministicScore::MAX), "+Inf");
308 assert_eq!(format!("{}", DeterministicScore::NEG_INF), "-Inf");
309 }
310
311 #[test]
312 fn debug_formatting() {
313 assert_eq!(
314 format!("{:?}", DeterministicScore::MAX),
315 "DeterministicScore(+Inf)"
316 );
317 assert_eq!(
318 format!("{:?}", DeterministicScore::NEG_INF),
319 "DeterministicScore(-Inf)"
320 );
321 let s = format!("{:?}", DeterministicScore::from_f64(0.5));
322 assert!(
323 s.starts_with("DeterministicScore(0.5"),
324 "unexpected debug output: {s}"
325 );
326 }
327
328 #[test]
329 fn add_saturates_at_max() {
330 assert_eq!(
331 DeterministicScore::MAX + DeterministicScore::from_raw(1),
332 DeterministicScore::MAX
333 );
334 }
335
336 #[test]
337 fn sub_saturates_at_neg_inf() {
338 assert_eq!(
339 DeterministicScore::NEG_INF - DeterministicScore::from_raw(1),
340 DeterministicScore::NEG_INF
341 );
342 }
343
344 #[test]
345 fn mul_i64_saturates_at_max() {
346 let large = DeterministicScore::from_raw(i64::MAX / 2);
347 assert_eq!(large * 3_i64, DeterministicScore::MAX);
348 }
349
350 #[test]
351 fn mul_f64_nan_yields_zero() {
352 let s = DeterministicScore::from_f64(1.0);
353 assert_eq!(s * f64::NAN, DeterministicScore::ZERO);
354 }
355
356 #[test]
358 fn neg_inf_is_i64_min_plus_one() {
359 assert_eq!(DeterministicScore::NEG_INF.to_raw(), i64::MIN + 1);
360 }
361
362 #[test]
363 fn min_sentinel_is_i64_min() {
364 assert_eq!(DeterministicScore::MIN.to_raw(), i64::MIN);
365 }
366
367 #[test]
368 fn min_sentinel_distinct_from_neg_inf() {
369 assert_ne!(DeterministicScore::MIN, DeterministicScore::NEG_INF);
370 assert!(DeterministicScore::MIN < DeterministicScore::NEG_INF);
371 }
372
373 #[test]
374 fn neg_infinity_maps_to_neg_inf() {
375 assert_eq!(
376 DeterministicScore::from_f64(f64::NEG_INFINITY),
377 DeterministicScore::NEG_INF
378 );
379 }
380
381 #[test]
382 fn underflow_clamps_to_neg_inf_not_min() {
383 let result = DeterministicScore::from_raw(i64::MIN + 1) - DeterministicScore::from_raw(1);
385 assert_eq!(result, DeterministicScore::NEG_INF);
386 assert_ne!(result, DeterministicScore::MIN);
387 }
388}