summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPgBiel <9021226+PgBiel@users.noreply.github.com>2024-10-11 05:19:58 -0300
committerGitHub <noreply@github.com>2024-10-11 08:19:58 +0000
commit16736feb13eec87eb9ca114deaeb4f7eeb7409d2 (patch)
tree6d7144ef8e91803079401929827b5f61147d6414
parentb5b92e21e9ae345cd900602ddbc6e3a980f538f3 (diff)
Fix hashing of equal decimals with different scales (#5179)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
-rw-r--r--crates/typst/src/foundations/decimal.rs43
-rw-r--r--tests/suite/foundations/decimal.typ7
2 files changed, 49 insertions, 1 deletions
diff --git a/crates/typst/src/foundations/decimal.rs b/crates/typst/src/foundations/decimal.rs
index 6329b445..f2cef59b 100644
--- a/crates/typst/src/foundations/decimal.rs
+++ b/crates/typst/src/foundations/decimal.rs
@@ -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));
+ }
+}
diff --git a/tests/suite/foundations/decimal.typ b/tests/suite/foundations/decimal.typ
index d5fd9444..bae0d2e6 100644
--- a/tests/suite/foundations/decimal.typ
+++ b/tests/suite/foundations/decimal.typ
@@ -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\")")