From 39fbfa757de399b62abba28954c3cc0b84f750b5 Mon Sep 17 00:00:00 2001 From: Andrey Akulov Date: Tue, 11 Jun 2024 13:18:22 +0300 Subject: [PATCH 1/2] Add custom types support Removes &to_string/1 in &number_to_delimited/1 because it's a reason for a failure of custom types what implement `Number.Conversion` conversion protocol but do not implement `String.Chars` (or implements it not the format that Decimal expects). And simply there is no need in it at all. But the &number_to_phone/1 kept untouched because it heavily relies on string representation and if someone introduce a special type for the phone number representation it's doubtful that he would use any other functions from the package for that type. --- lib/number/currency.ex | 4 +++- lib/number/delimit.ex | 4 +++- lib/number/human.ex | 27 ++++++++++++---------- lib/number/percentage.ex | 3 +++ lib/number/phone.ex | 3 +++ lib/number/si.ex | 3 +++ mix.exs | 4 ++++ test/your_custom_number_conversion_type.ex | 16 +++++++++++++ 8 files changed, 50 insertions(+), 14 deletions(-) create mode 100644 test/your_custom_number_conversion_type.ex diff --git a/lib/number/currency.ex b/lib/number/currency.ex index 614114b..f475fdd 100644 --- a/lib/number/currency.ex +++ b/lib/number/currency.ex @@ -93,6 +93,8 @@ defmodule Number.Currency do iex> Number.Currency.number_to_currency(Decimal.from_float(-100.01), unit: "$", separator: ",", delimiter: ".", negative_format: "- %u %n") "- $ 100,01" + iex> Number.Currency.number_to_currency(%YourCustomNumberConversionType{}) + "$1,000.00" """ @spec number_to_currency(Number.t(), Keyword.t()) :: String.t() def number_to_currency(number, options \\ []) @@ -109,7 +111,7 @@ defmodule Number.Currency do end defp get_format(number, options) do - number = if is_float(number), do: Decimal.from_float(number), else: Decimal.new(number) + number = Number.Conversion.to_decimal(number) case Number.Decimal.compare(number, Decimal.new(0)) do :lt -> {Decimal.abs(number), options[:negative_format] || "-#{options[:format]}"} diff --git a/lib/number/delimit.ex b/lib/number/delimit.ex index 15a06fc..ca24caa 100644 --- a/lib/number/delimit.ex +++ b/lib/number/delimit.ex @@ -76,6 +76,9 @@ defmodule Number.Delimit do iex> Number.Delimit.number_to_delimited Decimal.new("123456789555555555555555555555555") "123,456,789,555,555,555,555,555,555,555,555.00" + + iex> Number.Delimit.number_to_delimited(%YourCustomNumberConversionType{}) + "1,000.00" """ @spec number_to_delimited(nil, Keyword.t()) :: nil @spec number_to_delimited(Number.t() | String.t(), Keyword.t()) :: String.t() @@ -101,7 +104,6 @@ defmodule Number.Delimit do {:error, other} -> other - |> to_string |> Number.Conversion.to_decimal() |> delimit_decimal(options[:delimiter], options[:separator], options[:precision]) end diff --git a/lib/number/human.ex b/lib/number/human.ex index 41eeb4f..cef869b 100644 --- a/lib/number/human.ex +++ b/lib/number/human.ex @@ -41,6 +41,9 @@ defmodule Number.Human do iex> Number.Human.number_to_human(Decimal.new("5000.0")) "5.00 Thousand" + iex> Number.Human.number_to_human(%YourCustomNumberConversionType{}) + "1.00 Thousand" + iex> Number.Human.number_to_human('charlist') ** (ArgumentError) number must be a float, integer or implement `Number.Conversion` protocol, was ~c"charlist" @@ -49,18 +52,7 @@ defmodule Number.Human do def number_to_human(number, options \\ []) def number_to_human(nil, _options), do: nil - def number_to_human(number, options) when not is_map(number) do - if Number.Conversion.impl_for(number) do - number - |> Number.Conversion.to_decimal() - |> number_to_human(options) - else - raise ArgumentError, - "number must be a float, integer or implement `Number.Conversion` protocol, was #{inspect(number)}" - end - end - - def number_to_human(number, options) do + def number_to_human(%Decimal{} = number, options) do cond do compare(number, ~d(999)) == :gt && compare(number, ~d(1_000_000)) == :lt -> delimit(number, ~d(1_000), "Thousand", options) @@ -84,6 +76,17 @@ defmodule Number.Human do end end + def number_to_human(number, options) do + if Number.Conversion.impl_for(number) do + number + |> Number.Conversion.to_decimal() + |> number_to_human(options) + else + raise ArgumentError, + "number must be a float, integer or implement `Number.Conversion` protocol, was #{inspect(number)}" + end + end + @doc """ Adds ordinal suffix (st, nd, rd or th) for the number ## Examples diff --git a/lib/number/percentage.ex b/lib/number/percentage.ex index 5b67ecd..941911b 100644 --- a/lib/number/percentage.ex +++ b/lib/number/percentage.ex @@ -58,6 +58,9 @@ defmodule Number.Percentage do iex> Number.Percentage.number_to_percentage(Decimal.from_float(59.236), precision: 2) "59.24%" + + iex> Number.Percentage.number_to_percentage(%YourCustomNumberConversionType{}) + "1,000.000%" """ @spec number_to_percentage(Number.t(), Keyword.t()) :: String.t() def number_to_percentage(number, options \\ []) diff --git a/lib/number/phone.ex b/lib/number/phone.ex index c0fe129..3bb2943 100644 --- a/lib/number/phone.ex +++ b/lib/number/phone.ex @@ -71,6 +71,9 @@ defmodule Number.Phone do iex> Number.Phone.number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: ".") "+1.123.555.1234 x 1343" + + iex> Number.Phone.number_to_phone(%YourCustomNumberConversionType{}) + "1000" """ @spec number_to_phone(Number.t() | String.t(), Keyword.t()) :: String.t() def number_to_phone(number, options \\ []) diff --git a/lib/number/si.ex b/lib/number/si.ex index cf41660..c244ec9 100644 --- a/lib/number/si.ex +++ b/lib/number/si.ex @@ -95,6 +95,9 @@ defmodule Number.SI do iex> Number.SI.number_to_si(Decimal.new(1210000000)) "1.21G" + iex> Number.SI.number_to_si(%YourCustomNumberConversionType{}) + "1.00k" + iex> Number.SI.number_to_si('charlist') ** (ArgumentError) number must be a float, integer or implement `Number.Conversion` protocol, was ~c"charlist" """ diff --git a/mix.exs b/mix.exs index d8bb410..93d2e68 100644 --- a/mix.exs +++ b/mix.exs @@ -10,6 +10,7 @@ defmodule Number.Mixfile do description: "Convert numbers to various string formats, such as currency", version: @version, elixir: "~> 1.0", + elixirc_paths: elixirc_paths(Mix.env()), build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, test_coverage: [tool: ExCoveralls], @@ -49,6 +50,9 @@ defmodule Number.Mixfile do ] end + defp elixirc_paths(:test), do: ["lib", "test/your_custom_number_conversion_type.ex"] + defp elixirc_paths(_), do: ["lib"] + defp package do [ files: ["lib", "mix.exs", "README.md", "CHANGELOG.md", "LICENSE"], diff --git a/test/your_custom_number_conversion_type.ex b/test/your_custom_number_conversion_type.ex new file mode 100644 index 0000000..53092e5 --- /dev/null +++ b/test/your_custom_number_conversion_type.ex @@ -0,0 +1,16 @@ +defmodule YourCustomNumberConversionType do + defstruct [] +end + +defimpl Number.Conversion, for: YourCustomNumberConversionType do + @moduledoc false + + def to_float(_value), do: 1_000.0 + + def to_decimal(_value), do: Decimal.new("1000.0") +end + +# For the number_to_phone function +defimpl String.Chars, for: YourCustomNumberConversionType do + def to_string(_value), do: "1000" +end From 2455a512871478aac31cf21c2e541b732f0e4db8 Mon Sep 17 00:00:00 2001 From: Andrey Akulov Date: Tue, 11 Jun 2024 13:59:30 +0300 Subject: [PATCH 2/2] Make sure that number functions rely only on Conversion protocol --- lib/number/phone.ex | 2 +- test/your_custom_number_conversion_type.ex | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/number/phone.ex b/lib/number/phone.ex index 3bb2943..296e311 100644 --- a/lib/number/phone.ex +++ b/lib/number/phone.ex @@ -73,7 +73,7 @@ defmodule Number.Phone do "+1.123.555.1234 x 1343" iex> Number.Phone.number_to_phone(%YourCustomNumberConversionType{}) - "1000" + "a0000" """ @spec number_to_phone(Number.t() | String.t(), Keyword.t()) :: String.t() def number_to_phone(number, options \\ []) diff --git a/test/your_custom_number_conversion_type.ex b/test/your_custom_number_conversion_type.ex index 53092e5..4db870f 100644 --- a/test/your_custom_number_conversion_type.ex +++ b/test/your_custom_number_conversion_type.ex @@ -10,7 +10,9 @@ defimpl Number.Conversion, for: YourCustomNumberConversionType do def to_decimal(_value), do: Decimal.new("1000.0") end -# For the number_to_phone function +# This protocol must be used only in &number_to_phone/2 +# Such string (that differs from above and can't be converted into a number) is used +# in order to ensure that other number functions would not rely on this procotol defimpl String.Chars, for: YourCustomNumberConversionType do - def to_string(_value), do: "1000" + def to_string(_value), do: "a0000" end