elixir-cldr/cldr_numbers

Feature request: drop insignificant zeros by default

Closed this issue · 10 comments

When displaying prices, it's common to only display subunits if they are present.

E.g. it would be preferable to display this value

iex(8)> Cldr.Number.to_string!(12345, locale: "hi-IN", currency: "INR")
"₹12,345.00"

as ₹12,345.

We could get this effect by rounding the number, but then it's not clear when we are throwing away information and when not. So I would argue that the subunits should be shown if present, so that the number can be rounded explicitly if that is the desired behaviour.

The formats are all driven by CLDR as you know. And preserving information is important as you say. The most straight forward is to format using :fractional_digits:

# Format the number without fractional digits
iex> Cldr.Number.to_string!(12345, locale: "en-IN", currency: "INR", fractional_digits: 0)
"₹12,345"

# If using `ex_money` then you can test if the amount is an integer and format accordingly
iex> m = Money.new(:INR, "12345.67")
iex> options = if Money.integer?(m), do: [fractional_digits: 0], else: []
iex> Money.to_string m, options

Of course a similar check for whether a number or Decimal is an integral value can also be calculated other ways.

There are also:

  • Money.put_fraction/2 which allows forcing the fractional part of a number. For example, pricing often ends up being fractional 99 ( like $2.99) so this can be forced.
  • Money.round/2 which has several different kinds of rounding.

Net, since I didn't answer your question, is that I believe ex_cldr_numbers should faithfully implement the CLDR formats as documented. If we look at what the format is for a currency for :en-IN we see:

iex> MyApp.Cldr.Number.Format.formats_for!(:"en-IN") |> Map.get(:currency)
"¤#,##,##0.00"

The fractional .00 zeros indicate, by definition, that these are required digits. If we want those digits to be optional we can use a different format (not built in):

# Use a format where the fractional digits are optional
iex> Cldr.Number.to_string!(12345, locale: "en-IN", currency: "INR", format: "¤#,##,##0.##")
# BUG? I didn't expect to see decimal digits in this case - I need to check the spec
"₹12,345.00"
# Because non-currency formatting with this format does do what I expected:
iex> Cldr.Number.to_string!(12345, locale: "en-IN", format: "#,##,##0.##")
"12,345"
iex> Cldr.Number.to_string!(12345.00, locale: "en-IN", format: "#,##,##0.##")
"12,345"
iex> Cldr.Number.to_string!(12345.67, locale: "en-IN", format: "#,##,##0.##")
"12,345.67"

I'll look into this tonight or tomorrow and see what the spec expects - but it feels like a bug to me.

Okay thanks, that makes sense.

How would you feel about a PR to ex_money to add a show_decimal_if_zero option to Money.to_string? Then we can look for that flag and force the fractional_digits to zero if the number is an integer, like you did above.

The other Money package (https://hexdocs.pm/money/readme.html) uses a flag named strip_insignificant_fractional_unit to the same effect.

I'm fine to add that. Thanks for offering the PR but I'll tackle this one later this evening my time.

I've pushed a commit that adds the :no_fraction_if_integer option to Money.to_string/2. Here's the changelog entry. Let me know if this does what you're after?

Enhancements

  • Adds an option :no_fraction_if_integer to Money.to_string/2. If truthy this option will set fractional_digits: 0 if money is an integer value. This may be helpful in cases where integer money amounts such as Money.new(:USD, 1234) should be formatted as $1,234 rather than $1,234.00.

I'll still work on the bug fix as well and close this issue after that is done.

That's great, thanks!

Bug is fixed. Now when the fractional digits are optional, the format is a currency format and the number is an integer then no fractional digits will be formatted.

iex> Cldr.Number.to_string!(12345, locale: "en-IN", currency: "INR", format: "¤#,##,##0.##")
"₹12,345"

Published ex_cldr_numbers version 2.31.3. Closing as completed.