Skip to content

Commit

Permalink
Merge pull request #814 from timothee-haudebourg/hash-numbers-only
Browse files Browse the repository at this point in the history
Implement `Hash` for `Number`.
  • Loading branch information
dtolnay committed Nov 5, 2021
2 parents 8b35517 + f53ae31 commit a28b1b1
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 2 deletions.
37 changes: 35 additions & 2 deletions src/number.rs
Expand Up @@ -16,13 +16,13 @@ use serde::de::{IntoDeserializer, MapAccess};
pub(crate) const TOKEN: &str = "$serde_json::private::Number";

/// Represents a JSON number, whether integer or floating point.
#[derive(Clone, Eq, PartialEq)]
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Number {
n: N,
}

#[cfg(not(feature = "arbitrary_precision"))]
#[derive(Copy, Clone, PartialEq)]
#[derive(Copy, Clone)]
enum N {
PosInt(u64),
/// Always less than zero.
Expand All @@ -31,10 +31,43 @@ enum N {
Float(f64),
}

#[cfg(not(feature = "arbitrary_precision"))]
impl PartialEq for N {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(N::PosInt(a), N::PosInt(b)) => a == b,
(N::NegInt(a), N::NegInt(b)) => a == b,
(N::Float(a), N::Float(b)) => a == b,
_ => false,
}
}
}

// Implementing Eq is fine since any float values are always finite.
#[cfg(not(feature = "arbitrary_precision"))]
impl Eq for N {}

#[cfg(not(feature = "arbitrary_precision"))]
impl core::hash::Hash for N {
fn hash<H: core::hash::Hasher>(&self, h: &mut H) {
match self {
N::PosInt(i) => i.hash(h),
N::NegInt(i) => i.hash(h),
N::Float(f) => {
// Using `f64::to_bits` here is fine since any float values are never `NaN`.
if *f == 0.0f64 {
// The IEEE 754 standard has two representations for zero, +0 and -0,
// such that +0 == -0.
// In both cases we use the +0 hash so that hash(+0) == hash(-0).
0.0f64.to_bits().hash(h);
} else {
f.to_bits().hash(h);
}
}
}
}
}

#[cfg(feature = "arbitrary_precision")]
type N = String;

Expand Down
16 changes: 16 additions & 0 deletions tests/test.rs
Expand Up @@ -2284,3 +2284,19 @@ fn test_value_into_deserializer() {
let outer = Outer::deserialize(map.into_deserializer()).unwrap();
assert_eq!(outer.inner.string, "Hello World");
}

#[test]
fn hash_positive_and_negative_zero() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

fn hash(obj: impl Hash) -> u64 {
let mut hasher = DefaultHasher::new();
obj.hash(&mut hasher);
hasher.finish()
}

let k1 = serde_json::from_str::<Number>("0.0").unwrap();
let k2 = serde_json::from_str::<Number>("-0.0").unwrap();
assert!(k1 != k2 || hash(k1) == hash(k2));
}

0 comments on commit a28b1b1

Please sign in to comment.