-
Notifications
You must be signed in to change notification settings - Fork 33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
implemented Number#as_human #61
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I dunno, I like function heads better. What do you think? defp number_sign(number) when is_number(number) and number < 0, do: -1
defp number_sign(number) when is_number(number), do: 1 |
||
cond do | ||
number >= 0 -> 1 | ||
true -> -1 | ||
end | ||
end | ||
|
||
defp rounded_or_significant(number, significant, precision) do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think of function heads as a replacement for conditionals? defp rounded_or_significant(number, true, precision), do: make_significant(number, precision)
defp rounded_or_significant(number, false, precision) when precision <= 0 do
number |> Float.round(precision) |> trunc |> Integer.to_string
end
defp rounded_or_significant(number, false, precision) do
number |> Float.round(precision) |> :io_lib.format("~.#{precision}f", [number]) |> List.to_string
end Also, now that I've implemented |
||
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.