Skip to content

Update to Decimal 3.0, update dependencies, fix Elixir 1.19 warnings - #67

Open
jeremyowensboggs wants to merge 3 commits into
danielberkompas:masterfrom
jeremyowensboggs:update-decimal-fix-elixir-1.19-warnings
Open

Update to Decimal 3.0, update dependencies, fix Elixir 1.19 warnings#67
jeremyowensboggs wants to merge 3 commits into
danielberkompas:masterfrom
jeremyowensboggs:update-decimal-fix-elixir-1.19-warnings

Conversation

@jeremyowensboggs

@jeremyowensboggs jeremyowensboggs commented May 8, 2026

Copy link
Copy Markdown
  • Update decimal dependency to ~> 3.0 (no longer backward compat, see below)
  • Update other dependencies in mix.lock
  • Move preferred_cli_env to def cli (deprecated in Mix 1.19)
  • Replace single-quoted charlists with ~c sigil in doctests
  • Fix compile-time type checker warning in conversion test

Problem

Decimal 3.x made Decimal.new/1 strict about the number of significant digits it accepts (default max: 28). This causes Number.Delimit.number_to_delimited/2 and Number.Currency.number_to_currency/2 to crash with Decimal.Error when given float-derived strings with more than 28 significant digits (e.g. "0.02053473047423571351409743977530517" — 35 digits, typical of IEEE 754 double-precision float-to-string conversion).

The crash path: number_to_delimited converts a non-integer input to a string via to_string/1, then passes it through Number.Conversion.to_decimal/1, which calls Decimal.new/1 on the raw string.

Fix

Replace Decimal.new(string) with Decimal.parse(string, max_digits: 100) in the BitString implementation of Number.Conversion.to_decimal/1. This is the single chokepoint where arbitrary strings become Decimals in the library. 100 digits provides generous headroom beyond any realistic float-derived input while still bounding the parse.

Since Decimal.parse/2 with the max_digits option is a 3.x-only API, this also narrows the decimal dependency to ~> 3.0. If backwards compatibility with decimal 1.x/2.x is desired, a compile-time check for function_exported?(Decimal, :parse, 2) could be added — happy to make that change if needed.

- Update decimal dependency to ~> 3.0 (with backwards compat for ~> 1.5 and ~> 2.0)
- Update other dependencies in mix.lock
- Move preferred_cli_env to def cli (deprecated in Mix 1.19)
- Replace single-quoted charlists with ~c sigil in doctests
- Fix compile-time type checker warning in conversion test
@pwcsquared

pwcsquared commented May 12, 2026

Copy link
Copy Markdown

@danielberkompas There is a moderate vulnerability in the :decimal package fixed in v3.0, so this PR is fairly important to us!

Decimal.new/1 in 3.x rejects strings exceeding the default max_digits
(28). Use Decimal.parse/2 with max_digits: 100 in the BitString
implementation of Number.Conversion.to_decimal/1.

Also narrows the decimal dep spec to ~> 3.0 since parse/2 with
max_digits is a 3.x-only API.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@jeremyowensboggs

Copy link
Copy Markdown
Author

Problem

Decimal 3.x made Decimal.new/1 strict about the number of significant digits it accepts (default max: 28). This causes Number.Delimit.number_to_delimited/2 and Number.Currency.number_to_currency/2 to crash with Decimal.Error when given float-derived strings with more than 28 significant digits (e.g. "0.02053473047423571351409743977530517" — 35 digits, typical of IEEE 754 double-precision float-to-string conversion).

The crash path: number_to_delimited converts a non-integer input to a string via to_string/1, then passes it through Number.Conversion.to_decimal/1, which calls Decimal.new/1 on the raw string.

Fix

Replace Decimal.new(string) with Decimal.parse(string, max_digits: 100) in the BitString implementation of Number.Conversion.to_decimal/1. This is the single chokepoint where arbitrary strings become Decimals in the library. 100 digits provides generous headroom beyond any realistic float-derived input while still bounding the parse.

Since Decimal.parse/2 with the max_digits option is a 3.x-only API, this also narrows the decimal dependency to ~> 3.0. If backwards compatibility with decimal 1.x/2.x is desired, a compile-time check for function_exported?(Decimal, :parse, 2) could be added — happy to make that change if needed.

@DerTim1 DerTim1 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use decimal ~> 3.1 to avoid a bug in 3.0.0.

@eriknaslund

Copy link
Copy Markdown

@danielberkompas or @DerTim1 Any update on this? Seems like the CI step didn't complete as expected?

It'd be great to land this change. My dependency chain is :backpex -> :number -> :decimal 1.5-2.0...and that's starting to cause some issues now that a lot of other packages have switched to decimal ~> 3.0.

@Ch4s3

Ch4s3 commented Jul 20, 2026

Copy link
Copy Markdown

@danielberkompas or @DerTim1 Any update on this? Seems like the CI step didn't complete as expected?

It'd be great to land this change. My dependency chain is :backpex -> :number -> :decimal 1.5-2.0...and that's starting to cause some issues now that a lot of other packages have switched to decimal ~> 3.0.

I'm forking the library. hang tight

Ch4s3 added a commit to Ch4s3/number_formatter that referenced this pull request Jul 20, 2026
Decimal 3.x's Decimal.new/1 caps parsing at 28 significant digits by
default and raises on longer float-derived strings (e.g. to_string of
a float can yield 35+ digits), which crashed number_to_delimited/2 and
number_to_currency/2. Switch the BitString Number.Conversion impl to
Decimal.parse/2 with max_digits: 100, which is a 3.x-only API and is
why the dep is narrowed to ~> 3.0 (no 1.x/2.x backward compat).

Drop the DECIMAL_VERSION-driven dual-lockfile setup (mix-decimal1.lock,
lockfile/0) since it now only ever resolves the same decimal version.

Ports the fix from danielberkompas/number#67.
Ch4s3 added a commit to Ch4s3/number_formatter that referenced this pull request Jul 20, 2026
- preferred_cli_env in project/0 is deprecated in Mix 1.19; move it
  into a def cli/0 with preferred_envs instead.
- Single-quoted charlist literals ('...') are deprecated in favor of
  the ~c sigil.
- Elixir's type checker now statically flags
  to_float(%{hello: "world"}) as unreachable (it can prove the
  BitString-only guard clauses can't match a map) and fails the build
  under --warnings-as-errors. Use :erlang.apply/3 to bypass static
  analysis so the runtime Protocol.UndefinedError test still exercises
  the intended behavior.

Ports the fixes from danielberkompas/number#67.
@Ch4s3

Ch4s3 commented Jul 20, 2026

Copy link
Copy Markdown

I've forked Number here https://github.com/Ch4s3/number_formatter and released it to hex https://hex.pm/packages/number_formatter/1.0.0

@eriknaslund

Copy link
Copy Markdown

@Ch4s3 Just FYI, the :backpex maintainers realized they weren't actually using :number any more, and they removed it as a dependency (naymspace/backpex@02dd429). This solved my particular problem, but it's still of course awesome that you forked :number to keep it maintained and up to date!

@Ch4s3

Ch4s3 commented Jul 21, 2026

Copy link
Copy Markdown

@eriknaslund we use it, so I'm happy to pick it up and the surface area is small. Backpex looks cool, I'd have lived that 4 years ago!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants