Skip to content

Commit

Permalink
Merge pull request #19 from danielberkompas/bugfix/18/precision-loss
Browse files Browse the repository at this point in the history
[#18] Improve precision using Decimal
  • Loading branch information
danielberkompas authored Sep 16, 2016
2 parents 31f8dad + 4d61f4d commit bdabb62
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 65 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
language: elixir
elixir:
- 1.2.0
- 1.3.0
otp_release:
- 18.1
before_script:
Expand Down
20 changes: 20 additions & 0 deletions lib/number/conversion.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ defprotocol Number.Conversion do

@doc "Converts a value to a Float."
def to_float(value)

@doc "Converts a value to a Decimal."
def to_decimal(value)
end

defimpl Number.Conversion, for: BitString do
Expand All @@ -12,14 +15,27 @@ defimpl Number.Conversion, for: BitString do
:error -> raise ArgumentError, "could not convert #{inspect value} to float"
end
end

def to_decimal(value) do
string = String.Chars.to_string(value)
Decimal.new(string)
end
end

defimpl Number.Conversion, for: Float do
def to_float(value), do: value

def to_decimal(value) do
Decimal.new(value)
end
end

defimpl Number.Conversion, for: Integer do
def to_float(value), do: value * 1.0

def to_decimal(value) do
Decimal.new(value)
end
end

if Code.ensure_loaded?(Decimal) do
Expand All @@ -31,5 +47,9 @@ if Code.ensure_loaded?(Decimal) do
|> Float.parse
float
end

def to_decimal(value) do
value
end
end
end
6 changes: 3 additions & 3 deletions lib/number/currency.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ defmodule Number.Currency do
nil
iex> Number.Currency.number_to_currency(1000)
"$1,000"
"$1,000.00"
iex> Number.Currency.number_to_currency(1000, unit: "£")
"£1,000"
"£1,000.00"
iex> Number.Currency.number_to_currency(-1000)
"-$1,000"
"-$1,000.00"
iex> Number.Currency.number_to_currency(-234234.23)
"-$234,234.23"
Expand Down
90 changes: 67 additions & 23 deletions lib/number/delimit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,16 @@ defmodule Number.Delimit do
"-234,234.23"
iex> Number.Delimit.number_to_delimited(12345678)
"12,345,678"
"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"
"12.345.678.00"
iex> Number.Delimit.number_to_delimited(12345678, delimiter: ",")
"12,345,678"
"12,345,678.00"
iex> Number.Delimit.number_to_delimited(12345678.05, separator: " ")
"12,345,678 05"
Expand All @@ -69,6 +69,12 @@ defmodule Number.Delimit do
iex> Number.Delimit.number_to_delimited(Decimal.new(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(number, list) :: String.t
def number_to_delimited(number, options \\ [])
Expand All @@ -78,18 +84,50 @@ defmodule Number.Delimit do
options = Dict.merge(config, options)
prefix = if float < 0, do: "-", else: ""
delimited =
case is_integer(number) do
true ->
delimit_integer(number, options[:delimiter])
false ->
float
|> delimit_float(options[:delimiter], options[:separator], options[:precision])
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_char_list
Expand All @@ -103,21 +141,27 @@ defmodule Number.Delimit do
:lists.reverse(list) ++ acc
end

defp delimit_float(number, delimiter, separator, precision) do
number = Float.round(number, precision)
decimals = isolate_decimals(number, precision)
integer = number |> trunc |> delimit_integer(delimiter)
separator = if precision == 0, do: '', else: separator
:lists.flatten([integer, separator, decimals])
end
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)

defp isolate_decimals(_number, precision) when precision == 0, do: ''
defp isolate_decimals(number, precision) do
[decimals] = :io_lib.format("~.*f", [precision, number - trunc(number)])
decimals
|> String.Chars.to_string
|> String.replace(~r/^.*\./, "")
|> String.to_char_list
separator = if precision == 0, do: "", else: separator
Enum.join([integer, separator, decimals])
end

defp config do
Expand Down
60 changes: 30 additions & 30 deletions lib/number/human.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ defmodule Number.Human do
"""

import Number.Delimit, only: [number_to_delimited: 2]
import Decimal, only: [cmp: 2]

@doc """
Formats and labels a number with the appropriate English word.
## Examples
iex> Number.Human.number_to_human(123)
"123"
"123.00"
iex> Number.Human.number_to_human(1234)
"1.23 Thousand"
Expand All @@ -34,15 +35,15 @@ defmodule Number.Human do
iex> Number.Human.number_to_human(1234567890123456789)
"1,234.57 Quadrillion"
iex> Number.Human.number_to_human(Decimal.new(5000.0))
iex> Number.Human.number_to_human(Decimal.new("5000.0"))
"5.00 Thousand"
"""
def number_to_human(number, options \\ [])

def number_to_human(number, options) when not is_number(number) do
def number_to_human(number, options) when not is_map(number) do
if Number.Conversion.impl_for(number) do
number
|> Number.Conversion.to_float
|> Number.Conversion.to_decimal
|> number_to_human(options)
else
raise ArgumentError, """
Expand All @@ -52,37 +53,36 @@ defmodule Number.Human do
end
end

def number_to_human(number, options)
when number > 999 and number < 1_000_000 do
delimit(number, 1_000, "Thousand", options)
end

def number_to_human(number, options)
when number >= 1_000_000 and number < 1_000_000_000 do
delimit(number, 1_000_000, "Million", options)
end

def number_to_human(number, options)
when number >= 1_000_000_000 and number < 1_000_000_000_000 do
delimit(number, 1_000_000_000, "Billion", options)
end

def number_to_human(number, options)
when number >= 1_000_000_000_000 and number < 1_000_000_000_000_000 do
delimit(number, 1_000_000_000_000, "Trillion", options)
end

def number_to_human(number, options)
when number >= 1_000_000_000_000_000 do
delimit(number, 1_000_000_000_000_000, "Quadrillion", options)
def number_to_human(number, options) do
cond do
cmp(number, ~d(999)) == :gt && cmp(number, ~d(1_000_000)) == :lt ->
delimit(number, ~d(1_000), "Thousand", options)
cmp(number, ~d(1_000_000)) in [:gt, :eq] and cmp(number, ~d(1_000_000_000)) == :lt ->
delimit(number, ~d(1_000_000), "Million", options)
cmp(number, ~d(1_000_000_000)) in [:gt, :eq] and cmp(number, ~d(1_000_000_000_000)) == :lt ->
delimit(number, ~d(1_000_000_000), "Billion", options)
cmp(number, ~d(1_000_000_000_000)) in [:gt, :eq] and cmp(number, ~d(1_000_000_000_000_000)) == :lt ->
delimit(number, ~d(1_000_000_000_000), "Trillion", options)
cmp(number, ~d(1_000_000_000_000_000)) in [:gt, :eq] ->
delimit(number, ~d(1_000_000_000_000_000), "Quadrillion", options)
true ->
number_to_delimited(number, options)
end
end

def number_to_human(number, _options) do
to_string(number)
defp sigil_d(number, _modifiers) do
number
|> String.replace("_", "")
|> String.to_integer
|> Decimal.new
end

defp delimit(number, divisor, label, options) do
number = number_to_delimited(number / divisor, options)
number =
number
|> Decimal.div(divisor)
|> number_to_delimited(options)

number <> " " <> label
end
end
7 changes: 1 addition & 6 deletions lib/number/percentage.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ defmodule Number.Percentage do
"""

import Number.Delimit, only: [number_to_delimited: 2]
import Number.Conversion, only: [to_float: 1]

@doc """
Formats a number into a percentage string.
Expand Down Expand Up @@ -60,11 +59,7 @@ defmodule Number.Percentage do
def number_to_percentage(number, options \\ [])
def number_to_percentage(number, options) do
options = Dict.merge(config, options)

number = number
|> to_float
|> number_to_delimited(options)

number = number_to_delimited(number, options)
number <> "%"
end

Expand Down
4 changes: 2 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Currency.Mixfile do
defmodule Number.Mixfile do
use Mix.Project

def project do
Expand All @@ -19,7 +19,7 @@ defmodule Currency.Mixfile do

defp deps do
[
{:decimal, "~> 1.0", optional: true},
{:decimal, "~> 1.0"},
{:ex_doc, "~> 0.11", only: :docs},
{:inch_ex, "~> 0.3", only: :docs}
]
Expand Down

0 comments on commit bdabb62

Please sign in to comment.