Skip to content

Commit

Permalink
Merge pull request #61 from druzn3k/as_human
Browse files Browse the repository at this point in the history
implemented Number#as_human
  • Loading branch information
druzn3k committed Aug 16, 2015
2 parents 4a08cfb + 76f7d38 commit 031e178
Showing 1 changed file with 256 additions and 0 deletions.
256 changes: 256 additions & 0 deletions lib/crutches/format/number.ex
Original file line number Diff line number Diff line change
Expand Up @@ -418,4 +418,260 @@ defmodule Crutches.Format.Number do
def as_percentage!(number, opts) do
as_percentage(number, opts)
end

@doc ~S"""
Formats and approximates `number` for human readability.
`1200000000` becomes `"1.2 Billion"`. This is useful as larger numbers become
harder to read.
See `as_human_size` if you want to print a file size.
You can also define you own unit-quantifier names if you want to use other
decimal units (eg.: 1500 becomes "1.5 kilometers", 0.150 becomes
“150 milliliters”, etc). You may define a wide range of unit quantifiers, even
fractional ones (centi, deci, mili, etc).
# Options
- `:locale` (atom) --- Locale to be used for formatting. *Default:*
current locale.
- `:precision` (integer) --- Precision of the number. *Default:* `3`
- `:significant` - If true, precision will be the # of significant_digits. If
false, the # of fractional digits (defaults to true)
- `:separator` (string) --- Separator between the fractional and integer
digits. *Default:* `"."`
- `:delimiter` (string) --- Thousands delimiter. *Default:* `""`
-`:units` (keyword list/string) --- Keyword list of unit quantifier names,
*or* a string containing an i18n scope pointing to it.
- `:format` (string) --- Format of the output string. `%u` is the quantifier,
`%n` is the number. *Default:* `"%n %u"`
# i18n
This function takes a keyword list of quantifier names to use for formatting.
It supports the following keys:
- `:unit`
- `:ten`
- `:hundred`
- `:thousand`
- `:million`
- `:billion`
- `:trillion`
- `:quadrillion`
- `:deci`
- `:centi`
- `:milli`
- `:micro`
- `:nano`
- `:pico`
- `:femto`
# Examples
iex> Number.as_human(123)
"123"
iex> Number.as_human(1234)
"1.23 Thousand"
iex> Number.as_human(12345)
"12.3 Thousand"
iex> Number.as_human(1234567)
"1.23 Million"
iex> Number.as_human(1234567890)
"1.23 Billion"
iex> Number.as_human(1234567890123)
"1.23 Trillion"
iex> Number.as_human(1234567890123456)
"1.23 Quadrillion"
iex> Number.as_human(1234567890123456789)
"1230 Quadrillion"
iex> Number.as_human(489939, precision: 2)
"490 Thousand"
iex> Number.as_human(489939, precision: 4)
"489.9 Thousand"
iex> Number.as_human(1234567, precision: 4, significant: false)
"1.2346 Million"
iex> Number.as_human(1234567, precision: 1, separator: ",", significant: false)
"1,2 Million"
iex> Number.as_human(500000000, precision: 5)
"500 Million"
iex> Number.as_human(12345012345, significant: false)
"12.345 Billion"
iex> Number.as_human!("abc")
** (ArithmeticError) bad argument in arithmetic expression
"""
@as_human [
valid: [:locale, :precision, :significant, :separator, :delimiter,
:strip_insignificant_zeros, :units, :format],
defaults: [
precision: 3,
significant: true,
separator: ".",
delimiter: "",
strip_insignificant_zeros: true,
units: [
quadrillion: "Quadrillion",
trillion: "Trillion",
billion: "Billion",
million: "Million",
thousand: "Thousand",
hundred: "",
ten: "",
unit: "",
deci: "deci",
centi: "centi",
milli: "milli",
micro: "micro",
nano: "nano",
pico: "pico",
femto: "femto"
],
format: "%n %u"
]
]

def as_human(number, opts \\ [])
def as_human(number, opts) when is_number(number) do
opts = Option.combine!(opts, @as_human)
{exp, unit, sign} = closest_size_and_sign(number)

precision = opts[:precision]

if precision < 0 do
precision = 0
end

delimited_opts = Keyword.take(opts, @as_delimited[:valid])

fract_num =
abs(number) / :math.pow(10, exp)
|> rounded_or_significant(opts[:significant], precision)
|> strip_trailing_zeros(opts[:strip_insignificant_zeros])
|> as_delimited(delimited_opts)

if sign < 0 do
fract_num = "-" <> fract_num
end

format_as_human(fract_num, opts[:units][unit], opts[:format])
end

defp closest_size_and_sign(number) when is_number(number) do
tenth_exp =
number
|> abs
|> :math.log10
|> trunc

sign = number_sign(number)

cond do
tenth_exp >= 15 -> {15, :quadrillion, sign}
tenth_exp >= 12 -> {12, :trillion, sign}
tenth_exp >= 9 -> {9, :billion, sign}
tenth_exp >= 6 -> {6, :million, sign}
tenth_exp >= 3 -> {3, :thousand, sign}
tenth_exp >= 2 -> {0, :hundred, sign}
tenth_exp >= 1 -> {0, :ten, sign}
tenth_exp >= 0 -> {0, :unit, sign}
tenth_exp < 0 && tenth_exp >= -1 -> {-1, :deci, sign}
tenth_exp < -1 && tenth_exp >= -2 -> {-2, :centi, sign}
tenth_exp < -2 && tenth_exp >= -3 -> {-3, :milli, sign}
tenth_exp < -3 && tenth_exp >= -6 -> {-6, :micro, sign}
tenth_exp < -6 && tenth_exp >= -9 -> {-9, :nano, sign}
tenth_exp < -9 && tenth_exp >= -12 -> {-12, :pico, sign}
tenth_exp < -12 -> {-15, :femto, sign}
end
end

defp number_sign(number) when is_number(number) do
cond do
number >= 0 -> 1
true -> -1
end
end

defp rounded_or_significant(number, significant, precision) do
case significant do
true ->
make_significant(number, precision)
false ->
number = Float.round(number, precision)

if precision > 0 do
:io_lib.format("~.#{precision}f", [number]) |> List.to_string
else
number |> trunc |> Integer.to_string
end
end
end

defp make_significant(number, precision) do
digits = (:math.log10(number) + 1) |> Float.floor |> trunc
multiplier = :math.pow(10, digits - precision)
extra_precision = precision - digits

result = Float.round(number / multiplier) * multiplier

if extra_precision > 0 do
:io_lib.format("~.#{extra_precision}f", [result]) |> List.to_string
else
result |> trunc |> Integer.to_string
end
end

defp strip_trailing_zeros(number, strip) do
if strip do
strip_trailing_zeros(number)
else
number
end
end

defp strip_trailing_zeros(number) do
if String.contains?(number, ".") do
case String.reverse(number) do
"0" <> number -> String.reverse(number) |> strip_trailing_zeros
"." <> number -> String.reverse(number)
number -> String.reverse(number)
end
else
number
end
end

defp format_as_human(binary, unit, format) when is_binary(binary) do
str = String.replace(format, "%n", binary, global: false)
String.replace(str, "%u", unit, global: false) |> String.strip
end

@doc ~S"""
Throwing version of `as_human`, raises if the input is not a valid number.
"""
def as_human!(number, opts \\ [])
def as_human!(number, opts) when is_binary(number) do
case Float.parse(number) do
{num, ""} -> as_human(num, opts)
_ -> raise(ArithmeticError, message: "bad argument in arithmetic expression")
end
end

def as_human!(number, opts) when is_number(number) do
as_human(number, opts)
end
end

0 comments on commit 031e178

Please sign in to comment.