Skip to content

Commit a28b1b1

Browse files
authored
Merge pull request #814 from timothee-haudebourg/hash-numbers-only
Implement `Hash` for `Number`.
2 parents 8b35517 + f53ae31 commit a28b1b1

File tree

2 files changed

+51
-2
lines changed

2 files changed

+51
-2
lines changed

src/number.rs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ use serde::de::{IntoDeserializer, MapAccess};
1616
pub(crate) const TOKEN: &str = "$serde_json::private::Number";
1717

1818
/// Represents a JSON number, whether integer or floating point.
19-
#[derive(Clone, Eq, PartialEq)]
19+
#[derive(Clone, PartialEq, Eq, Hash)]
2020
pub struct Number {
2121
n: N,
2222
}
2323

2424
#[cfg(not(feature = "arbitrary_precision"))]
25-
#[derive(Copy, Clone, PartialEq)]
25+
#[derive(Copy, Clone)]
2626
enum N {
2727
PosInt(u64),
2828
/// Always less than zero.
@@ -31,10 +31,43 @@ enum N {
3131
Float(f64),
3232
}
3333

34+
#[cfg(not(feature = "arbitrary_precision"))]
35+
impl PartialEq for N {
36+
fn eq(&self, other: &Self) -> bool {
37+
match (self, other) {
38+
(N::PosInt(a), N::PosInt(b)) => a == b,
39+
(N::NegInt(a), N::NegInt(b)) => a == b,
40+
(N::Float(a), N::Float(b)) => a == b,
41+
_ => false,
42+
}
43+
}
44+
}
45+
3446
// Implementing Eq is fine since any float values are always finite.
3547
#[cfg(not(feature = "arbitrary_precision"))]
3648
impl Eq for N {}
3749

50+
#[cfg(not(feature = "arbitrary_precision"))]
51+
impl core::hash::Hash for N {
52+
fn hash<H: core::hash::Hasher>(&self, h: &mut H) {
53+
match self {
54+
N::PosInt(i) => i.hash(h),
55+
N::NegInt(i) => i.hash(h),
56+
N::Float(f) => {
57+
// Using `f64::to_bits` here is fine since any float values are never `NaN`.
58+
if *f == 0.0f64 {
59+
// The IEEE 754 standard has two representations for zero, +0 and -0,
60+
// such that +0 == -0.
61+
// In both cases we use the +0 hash so that hash(+0) == hash(-0).
62+
0.0f64.to_bits().hash(h);
63+
} else {
64+
f.to_bits().hash(h);
65+
}
66+
}
67+
}
68+
}
69+
}
70+
3871
#[cfg(feature = "arbitrary_precision")]
3972
type N = String;
4073

tests/test.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2284,3 +2284,19 @@ fn test_value_into_deserializer() {
22842284
let outer = Outer::deserialize(map.into_deserializer()).unwrap();
22852285
assert_eq!(outer.inner.string, "Hello World");
22862286
}
2287+
2288+
#[test]
2289+
fn hash_positive_and_negative_zero() {
2290+
use std::collections::hash_map::DefaultHasher;
2291+
use std::hash::{Hash, Hasher};
2292+
2293+
fn hash(obj: impl Hash) -> u64 {
2294+
let mut hasher = DefaultHasher::new();
2295+
obj.hash(&mut hasher);
2296+
hasher.finish()
2297+
}
2298+
2299+
let k1 = serde_json::from_str::<Number>("0.0").unwrap();
2300+
let k2 = serde_json::from_str::<Number>("-0.0").unwrap();
2301+
assert!(k1 != k2 || hash(k1) == hash(k2));
2302+
}

0 commit comments

Comments
 (0)