defmodule Number.Delimit do
@moduledoc """
Provides functions to delimit numbers into strings.
"""
@doc """
Formats a number into a string with grouped thousands using `delimiter`.
## Parameters
* `number` - A float or integer to convert.
* `options` - A keyword list of options. See the documentation of all
available options below for more information.
## Options
* `:precision` - The number of decimal places to include. Default: 2
* `:delimiter` - The character to use to delimit the number by thousands.
Default: ","
* `:separator` - The character to use to separate the number from the decimal
places. Default: "."
Default configuration for these options can be specified in the `Number`
application configuration.
config :number,
delimit: [
precision: 3,
delimiter: ",",
separator: "."
]
## Examples
iex> Number.Delimit.number_to_delimited(nil)
nil
iex> Number.Delimit.number_to_delimited(998.999)
"999.00"
iex> Number.Delimit.number_to_delimited(-234234.234)
"-234,234.23"
iex> Number.Delimit.number_to_delimited("998.999")
"999.00"
iex> Number.Delimit.number_to_delimited("-234234.234")
"-234,234.23"
iex> Number.Delimit.number_to_delimited(12345678)
"12,345,678.00"
iex> Number.Delimit.number_to_delimited(12345678.05)
"12,345,678.05"
iex> Number.Delimit.number_to_delimited(12345678, delimiter: ".")
"12.345.678.00"
iex> Number.Delimit.number_to_delimited(12345678, delimiter: ",")
"12,345,678.00"
iex> Number.Delimit.number_to_delimited(12345678.05, separator: " ")
"12,345,678 05"
iex> Number.Delimit.number_to_delimited(98765432.98, delimiter: " ", separator: ",")
"98 765 432,98"
iex> Number.Delimit.number_to_delimited(Decimal.from_float(9998.2))
"9,998.20"
iex> Number.Delimit.number_to_delimited "123456789555555555555555555555555"
"123,456,789,555,555,555,555,555,555,555,555.00"
iex> Number.Delimit.number_to_delimited Decimal.new("123456789555555555555555555555555")
"123,456,789,555,555,555,555,555,555,555,555.00"
"""
@spec number_to_delimited(nil, Keyword.t()) :: nil
@spec number_to_delimited(Number.t() | String.t(), Keyword.t()) :: String.t()
def number_to_delimited(number, options \\ [])
def number_to_delimited(nil, _options), do: nil
def number_to_delimited(number, options) do
float = number |> Number.Conversion.to_float()
options = Keyword.merge(config(), options)
prefix = if float < 0, do: "-", else: ""
delimited =
case to_integer(number) do
{:ok, number} ->
number = delimit_integer(number, options[:delimiter])
if options[:precision] > 0 do
decimals = String.pad_trailing("", options[:precision], "0")
Enum.join([to_string(number), options[:separator], decimals])
else
number
end
{:error, other} ->
other
|> to_string
|> Number.Conversion.to_decimal()
|> delimit_decimal(options[:delimiter], options[:separator], options[:precision])
end
delimited = String.Chars.to_string(delimited)
prefix <> delimited
end
defp to_integer(integer) when is_integer(integer) do
{:ok, integer}
end
defp to_integer(%{__struct__: Decimal} = decimal) do
try do
{:ok, Decimal.to_integer(decimal)}
rescue
_ ->
{:error, decimal}
end
end
defp to_integer(string) when is_binary(string) do
try do
{:ok, String.to_integer(string)}
rescue
_ ->
{:error, string}
end
end
defp to_integer(other) do
{:error, other}
end
defp delimit_integer(number, delimiter) do
abs(number)
|> Integer.to_charlist()
|> :lists.reverse()
|> delimit_integer(delimiter, [])
end
defp delimit_integer([a, b, c, d | tail], delimiter, acc) do
delimit_integer([d | tail], delimiter, [delimiter, c, b, a | acc])
end
defp delimit_integer(list, _, acc) do
:lists.reverse(list) ++ acc
end
@doc false
def delimit_decimal(decimal, delimiter, separator, precision) do
string =
decimal
|> Decimal.round(precision)
|> Decimal.to_string(:normal)
[number, decimals] =
case String.split(string, ".") do
[number, decimals] -> [number, decimals]
[number] -> [number, ""]
end
decimals = String.pad_trailing(decimals, precision, "0")
integer =
number
|> String.to_integer()
|> delimit_integer(delimiter)
separator = if precision == 0, do: "", else: separator
Enum.join([integer, separator, decimals])
end
defp config do
defaults = [
delimiter: ",",
separator: ".",
precision: 2
]
Keyword.merge(defaults, Application.get_env(:number, :delimit, []))
end
end