Fix hashing of equal decimals with different scales (#5179)

Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
PgBiel 2024-10-11 05:19:58 -03:00 committed by GitHub
parent b5b92e21e9
commit 16736feb13
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 49 additions and 1 deletions

View File

@ -1,4 +1,5 @@
use std::fmt::{self, Display, Formatter};
use std::hash::{Hash, Hasher};
use std::ops::Neg;
use std::str::FromStr;
@ -88,7 +89,7 @@ use crate::World;
/// to rounding. When those two operations do not surpass the digit limits, they
/// are fully precise.
#[ty(scope, cast)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Decimal(rust_decimal::Decimal);
impl Decimal {
@ -370,6 +371,22 @@ impl Neg for Decimal {
}
}
impl Hash for Decimal {
fn hash<H: Hasher>(&self, state: &mut H) {
// `rust_decimal`'s Hash implementation normalizes decimals before
// hashing them. This means decimals with different scales but
// equivalent value not only compare equal but also hash equally. Here,
// we hash all bytes explicitly to ensure the scale is also considered.
// This means that 123.314 == 123.31400, but 123.314.hash() !=
// 123.31400.hash().
//
// Note that this implies that equal decimals can have different hashes,
// which might generate problems with certain data structures, such as
// HashSet and HashMap.
self.0.serialize().hash(state);
}
}
/// A value that can be cast to a decimal.
pub enum ToDecimal {
/// A string with the decimal's representation.
@ -386,3 +403,27 @@ cast! {
v: f64 => Self::Float(v),
v: Str => Self::Str(EcoString::from(v)),
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::Decimal;
use crate::utils::hash128;
#[test]
fn test_decimals_with_equal_scales_hash_identically() {
let a = Decimal::from_str("3.14").unwrap();
let b = Decimal::from_str("3.14").unwrap();
assert_eq!(a, b);
assert_eq!(hash128(&a), hash128(&b));
}
#[test]
fn test_decimals_with_different_scales_hash_differently() {
let a = Decimal::from_str("3.140").unwrap();
let b = Decimal::from_str("3.14000").unwrap();
assert_eq!(a, b);
assert_ne!(hash128(&a), hash128(&b));
}
}

View File

@ -31,6 +31,13 @@
// Error: 10-19 float is not a valid decimal: float.nan
#decimal(float.nan)
--- decimal-scale-is-observable ---
// Ensure equal decimals with different scales produce different strings.
#let f1(x) = str(x)
#let f2(x) = f1(x)
#test(f2(decimal("3.140")), "3.140")
#test(f2(decimal("3.14000")), "3.14000")
--- decimal-repr ---
// Test the `repr` function with decimals.
#test(repr(decimal("12.0")), "decimal(\"12.0\")")