diff --git a/bench/to_string.exs b/bench/to_string.exs index 6904691..49a1507 100644 --- a/bench/to_string.exs +++ b/bench/to_string.exs @@ -1,10 +1,12 @@ {:ok, options} = Cldr.Number.Format.Options.validate_options([], MyApp.Cldr, locale: Cldr.get_locale()) +number = 100000.55 Benchee.run( %{ - "Number to_string" => fn -> MyApp.Cldr.Number.to_string 10000.55 end, - "Number to_string preformatted options" => fn -> MyApp.Cldr.Number.to_string 10000.55, options end, - "Float to_string" => fn -> Float.to_string 10000.55 end + "Number to_string" => fn -> MyApp.Cldr.Number.to_string number end, + "Number to_string preformatted options" => fn -> MyApp.Cldr.Number.to_string number, options end, + "Float to_string" => fn -> Float.to_string 10000.55 end, + ":erlang.float_to_binary" => fn -> :erlang.float_to_binary(number, [:compact, decimals: 3]) end }, time: 10, memory_time: 2 diff --git a/lib/cldr/number.ex b/lib/cldr/number.ex index cd86864..7723afe 100644 --- a/lib/cldr/number.ex +++ b/lib/cldr/number.ex @@ -214,12 +214,16 @@ defmodule Cldr.Number do locale currently in affect for this `Process` and which is set by `Cldr.put_locale/1`. - * `:fractional_digits` is set to a positive integer value then the number - will be rounded to that number of digits and displayed accordingly overriding + * If `:fractional_digits` is set to a positive integer value then the number + will be rounded to that number of digits and displayed accordingly - overriding settings that would be applied by default. For example, currencies have fractional digits defined reflecting each currencies minor unit. Setting `:fractional_digits` will override that setting. + * If `:round_nearest` is set to a positive integer value then the number + will be rounded to nearest increment of that value - overriding + settings that would be applied by default. + * `:minimum_grouping_digits` overrides the CLDR definition of minimum grouping digits. For example in the locale `es` the number `1234` is formatted by default as `1345` because the locale defines the `minimium_grouping_digits` as `2`. If diff --git a/lib/cldr/number/backend/number.ex b/lib/cldr/number/backend/number.ex index 96ef2a5..bd6a98f 100644 --- a/lib/cldr/number/backend/number.ex +++ b/lib/cldr/number/backend/number.ex @@ -197,12 +197,16 @@ defmodule Cldr.Number.Backend.Number do locale currently in affect for this `Process` and which is set by `Cldr.put_locale/1`. - * `:fractional_digits` is set to a positive integer value then the number - will be rounded to that number of digits and displayed accordingly overriding + * If `:fractional_digits` is set to a positive integer value then the number + will be rounded to that number of digits and displayed accordingly - overriding settings that would be applied by default. For example, currencies have fractional digits defined reflecting each currencies minor unit. Setting `:fractional_digits` will override that setting. + * If `:round_nearest` is set to a positive integer value then the number + will be rounded to nearest increment of that value - overriding + settings that would be applied by default. + * `:minimum_grouping_digits` overrides the CLDR definition of minimum grouping digits. For example in the locale `es` the number `1234` is formatted by default as `1345` because the locale defines the `minimium_grouping_digits` as `2`. If diff --git a/lib/cldr/number/format/compiler.ex b/lib/cldr/number/format/compiler.ex index 880c0ce..555c45d 100644 --- a/lib/cldr/number/format/compiler.ex +++ b/lib/cldr/number/format/compiler.ex @@ -125,7 +125,7 @@ defmodule Cldr.Number.Format.Compiler do # Default rounding increment (not the same as rounding decimal # digits. `0` means no rounding increment to be applied. - @default_rounding 0 + @default_round_nearest 0 @doc """ Returns a number placeholder symbol. @@ -359,7 +359,7 @@ defmodule Cldr.Number.Format.Compiler do exponent_sign: exponent_sign(format_parts), scientific_rounding: scientific_rounding(format_parts), grouping: grouping(format_parts), - rounding: rounding(format_parts), + round_nearest: round_nearest(format_parts), padding_length: padding_length(format[:positive][:pad], format), padding_char: padding_char(format), multiplier: multiplier(format), @@ -712,19 +712,19 @@ defmodule Cldr.Number.Format.Compiler do # * In a pattern, digits '1' through '9' specify rounding, but otherwise # behave identically to digit '0'. - defp rounding(%{"integer" => integer_format, "fraction" => fraction_format}) do + defp round_nearest(%{"integer" => integer_format, "fraction" => fraction_format}) do format = (integer_format <> @decimal_separator <> fraction_format) |> String.replace(@rounding_pattern, "") |> String.trim_trailing(@decimal_separator) case Float.parse(format) do - :error -> @default_rounding + :error -> @default_round_nearest {rounding, ""} -> rounding end end - defp rounding(_), do: @default_rounding + defp round_nearest(_), do: @default_round_nearest @doc """ A regular expression that can be used to split either a number format diff --git a/lib/cldr/number/format/meta.ex b/lib/cldr/number/format/meta.ex index ab931aa..592560f 100644 --- a/lib/cldr/number/format/meta.ex +++ b/lib/cldr/number/format/meta.ex @@ -116,7 +116,7 @@ defmodule Cldr.Number.Format.Meta do fraction: %{first: 0, rest: 0}, integer: %{first: 0, rest: 0} }, - rounding: 0, + round_nearest: 0, padding_length: 0, padding_char: " ", multiplier: 1, @@ -198,14 +198,14 @@ defmodule Cldr.Number.Format.Meta do end @doc """ - Set the number of digits to which the number should + Set the increment to which the number should be rounded. """ - @spec put_integer_digits(t(), non_neg_integer) :: t() - def put_rounding_digits(%__MODULE__{} = meta, digits) when is_integer(digits) do + @spec put_round_nearest_digits(t(), non_neg_integer) :: t() + def put_round_nearest_digits(%__MODULE__{} = meta, digits) when is_integer(digits) do meta - |> Map.put(:rounding, digits) + |> Map.put(:round_nearest, digits) end @doc """ diff --git a/lib/cldr/number/format/options.ex b/lib/cldr/number/format/options.ex index df5ff50..bca98ab 100644 --- a/lib/cldr/number/format/options.ex +++ b/lib/cldr/number/format/options.ex @@ -23,7 +23,8 @@ defmodule Cldr.Number.Format.Options do :minimum_grouping_digits, :pattern, :rounding_mode, - :fractional_digits + :fractional_digits, + :round_nearest ] # These are the options that can be supplied @@ -84,6 +85,7 @@ defmodule Cldr.Number.Format.Options do pattern: String.t(), rounding_mode: Decimal.rounding(), fractional_digits: pos_integer(), + round_nearest: pos_integer() } defstruct @options @@ -384,6 +386,21 @@ defmodule Cldr.Number.Format.Options do ":fractional_digits must be a positive integer or nil. Found #{inspect other}"}} end + def validate_option(:round_nearest, _options, _backend, nil) do + {:ok, nil} + end + + def validate_option(:round_nearest, _options, _backend, int) + when is_integer(int) and int > 0 do + {:ok, int} + end + + def validate_option(:round_nearest, _options, _backend, other) do + {:error, + {ArgumentError, + ":round_nearest must be a positive integer or nil. Found #{inspect other}"}} + end + def validate_option(:rounding_mode, _options, _backend, nil) do {:ok, :half_even} end diff --git a/lib/cldr/number/formatter/decimal_formatter.ex b/lib/cldr/number/formatter/decimal_formatter.ex index 37addbd..bdf63d7 100644 --- a/lib/cldr/number/formatter/decimal_formatter.ex +++ b/lib/cldr/number/formatter/decimal_formatter.ex @@ -66,6 +66,7 @@ defmodule Cldr.Number.Formatter.Decimal do |> adjust_fraction_for_currency(options.currency, options.currency_digits, backend) |> adjust_fraction_for_significant_digits(number) |> adjust_for_fractional_digits(options.fractional_digits) + |> adjust_for_round_nearest(options.round_nearest) |> Map.put(:number, number) end @@ -154,12 +155,12 @@ defmodule Cldr.Number.Formatter.Decimal do # Round to nearest rounds a number to the nearest increment specified. For example # if `rounding: 5` then we round to the nearest multiple of 5. The appropriate rounding # mode is used. - def round_to_nearest(number, %{rounding: rounding}, _backend, %{rounding_mode: _rounding_mode}) + def round_to_nearest(number, %{round_nearest: rounding}, _backend, %{rounding_mode: _rounding_mode}) when rounding == 0 do number end - def round_to_nearest(%Decimal{} = number, %{rounding: rounding}, _backend, %{ + def round_to_nearest(%Decimal{} = number, %{round_nearest: rounding}, _backend, %{ rounding_mode: rounding_mode }) do rounding = Decimal.new(rounding) @@ -170,7 +171,7 @@ defmodule Cldr.Number.Formatter.Decimal do |> Decimal.mult(rounding) end - def round_to_nearest(number, %{rounding: rounding}, _backend, %{rounding_mode: rounding_mode}) + def round_to_nearest(number, %{round_nearest: rounding}, _backend, %{rounding_mode: rounding_mode}) when is_float(number) do number |> Kernel./(rounding) @@ -178,7 +179,7 @@ defmodule Cldr.Number.Formatter.Decimal do |> Kernel.*(rounding) end - def round_to_nearest(number, %{rounding: rounding}, _backend, %{rounding_mode: rounding_mode}) + def round_to_nearest(number, %{round_nearest: rounding}, _backend, %{rounding_mode: rounding_mode}) when is_integer(number) do number |> Kernel./(rounding) @@ -705,7 +706,7 @@ defmodule Cldr.Number.Formatter.Decimal do def do_adjust_fraction(meta, digits, rounding) do rounding = power_of_10(-digits) * rounding - %{meta | fractional_digits: %{max: digits, min: digits}, rounding: rounding} + %{meta | fractional_digits: %{max: digits, min: digits}, round_nearest: rounding} end # @@ -749,6 +750,7 @@ defmodule Cldr.Number.Formatter.Decimal do end # To allow overriding fractional digits + # This causes rounding of the number def adjust_for_fractional_digits(meta, nil) do meta end @@ -757,6 +759,18 @@ defmodule Cldr.Number.Formatter.Decimal do %{meta | fractional_digits: %{max: digits, min: digits}} end + # To allow overriding round nearest + # which impacts the precision of the number + # and is commonly required for currency + # formatting + def adjust_for_round_nearest(meta, nil) do + meta + end + + def adjust_for_round_nearest(meta, digits) do + %{meta | round_nearest: digits} + end + @doc false def define_to_string(backend) do config = Module.get_attribute(backend, :config) diff --git a/test/meta_test.exs b/test/meta_test.exs index 7e066c8..1c7f9c8 100644 --- a/test/meta_test.exs +++ b/test/meta_test.exs @@ -17,7 +17,7 @@ defmodule Cldr.Number.Format.Meta.Test do number: 0, padding_char: " ", padding_length: 0, - rounding: 0, + round_nearest: 0, scientific_rounding: 0, significant_digits: %{max: 0, min: 0} } @@ -41,7 +41,7 @@ defmodule Cldr.Number.Format.Meta.Test do |> Meta.put_exponent_digits(5) |> Meta.put_exponent_sign(true) |> Meta.put_scientific_rounding_digits(6) - |> Meta.put_rounding_digits(7) + |> Meta.put_round_nearest_digits(7) |> Meta.put_padding_length(8) |> Meta.put_padding_char("Z") |> Meta.put_multiplier(9) @@ -49,7 +49,7 @@ defmodule Cldr.Number.Format.Meta.Test do assert meta.exponent_digits == 5 assert meta.exponent_sign == true assert meta.scientific_rounding == 6 - assert meta.rounding == 7 + assert meta.round_nearest == 7 assert meta.padding_length == 8 assert meta.padding_char == "Z" assert meta.multiplier == 9