Decimal `==` is not value equality

almirsarajcic

almirsarajcic

51 minutes ago

0 comments

Decimal.new("1.0") and Decimal.new("1.00") represent the same number, but Elixir’s == says they are different:

a = Decimal.new("1.0")
b = Decimal.new("1.00")
c = Decimal.new("1.0")

a == b
# => false

a == c
# => true

Decimal.equal?(a, b)
# => true

Decimal.compare(a, b)
# => :eq

a in [b]
# => false

Enum.uniq([a, b])
# => [Decimal.new("1.0"), Decimal.new("1.00")]

a == c is true because c was built from the exact same string as a, so the two structs are field-for-field identical. a == b is false even though a and b are mathematically equal, because they carry different precision.

Why

Decimal is a plain struct:

%Decimal{sign: 1, coef: 10, exp: -1}

== in Elixir doesn’t know anything about decimal arithmetic. It falls back to Erlang term comparison, which compares the struct like any other map: by its fields. Decimal.new("1.0") stores coef: 10, exp: -1 (1.0 = 10 × 10⁻¹), while Decimal.new("1.00") stores coef: 100, exp: -2 (1.00 = 100 × 10⁻²). Same value, different coefficient and exponent, so the terms don’t match.

This is exactly why in, Enum.member?/2, and Enum.uniq/1 all misbehave — they use == under the hood. a in [b] is false, and Enum.uniq([a, b]) keeps both entries instead of collapsing them to one. The same trap applies to using a Decimal as a bare map key: %{a => :x, b => :y} keeps two entries, not one, because map key matching also uses term equality.

Enum.sort/1 doesn’t crash, but it doesn’t tie-break on numeric value either — it orders by the same struct-field comparison, so equal-valued decimals with different precision can still end up in a specific, coefficient-driven order rather than being genuinely tied.

Use the right function

  • Decimal.equal?/2 for equality that respects numeric value, not representation.
  • Decimal.compare/2 for ordering — returns :lt, :eq, or :gt.
  • Decimal.lt?/2, Decimal.gt?/2, Decimal.lte?/2, Decimal.gte?/2 for direct comparisons.

None of these care whether the operands came from "1.0" or "1.00" — they compare the actual numbers.

This matters most for money and other exact-decimal fields loaded from a database or parsed from user input, where trailing zeros routinely differ between two values that should be treated as equal.

Decimal.equal?/2 docs

Comments (0)

Sign in with GitHub to join the discussion