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