Skip to content
Permalink
Browse files

Add `:round_mode` parameter support to number helpers

Support handling a `round_mode` in `ActiveSupport::NumberHelper::RoundingHelper`

Add default value to locale file

Update inline documentation with new parameter option

Update CHANGELOG

Add round_mode examples to all the tests

Add I18n test

Simplify logic

Further simpification
  • Loading branch information
tom-lord committed Jan 3, 2020
1 parent 1414910 commit 7905bdfd8b2ae50319cd7a9a74ee1f8c865d648d
@@ -1,3 +1,17 @@
* Support added for a `round_mode` parameter, in all number helpers. (See: `BigDecimal::mode`.)

```ruby
number_to_currency(1234567890.50, precision: 0, round_mode: :half_down) # => "$1,234,567,890"
number_to_percentage(302.24398923423, precision: 5, round_mode: :down) # => "302.24398%"
number_to_rounded(389.32314, precision: 0, round_mode: :ceil) # => "390"
number_to_human_size(483989, precision: 2, round_mode: :up) # => "480 KB"
number_to_human(489939, precision: 2, round_mode: :floor) # => "480 Thousand"
485000.to_s(:human, precision: 2, round_mode: :half_even) # => "480 Thousand"
```

*Tom Lord*

* `Array#to_sentence` no longer returns a frozen string.

Before:
@@ -27,10 +27,11 @@ module NumericWithFormat
# # => "+1.123.555.1234 x 1343"
#
# Currency:
# 1234567890.50.to_s(:currency) # => "$1,234,567,890.50"
# 1234567890.506.to_s(:currency) # => "$1,234,567,890.51"
# 1234567890.506.to_s(:currency, precision: 3) # => "$1,234,567,890.506"
# 1234567890.506.to_s(:currency, locale: :fr) # => "1 234 567 890,51 €"
# 1234567890.50.to_s(:currency) # => "$1,234,567,890.50"
# 1234567890.506.to_s(:currency) # => "$1,234,567,890.51"
# 1234567890.506.to_s(:currency, precision: 3) # => "$1,234,567,890.506"
# 1234567890.506.to_s(:currency, round_mode: :down) # => "$1,234,567,890.50"
# 1234567890.506.to_s(:currency, locale: :fr) # => "1 234 567 890,51 €"
# -1234567890.50.to_s(:currency, negative_format: '(%u%n)')
# # => "($1,234,567,890.50)"
# 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '')
@@ -43,6 +44,7 @@ module NumericWithFormat
# 100.to_s(:percentage, precision: 0) # => "100%"
# 1000.to_s(:percentage, delimiter: '.', separator: ',') # => "1.000,000%"
# 302.24398923423.to_s(:percentage, precision: 5) # => "302.24399%"
# 302.24398923423.to_s(:percentage, round_mode: :down) # => "302.243%"
# 1000.to_s(:percentage, locale: :fr) # => "1 000,000%"
# 100.to_s(:percentage, format: '%n %') # => "100.000 %"
#
@@ -59,6 +61,7 @@ module NumericWithFormat
# Rounded:
# 111.2345.to_s(:rounded) # => "111.235"
# 111.2345.to_s(:rounded, precision: 2) # => "111.23"
# 111.2345.to_s(:rounded, precision: 2, round_mode: :up) # => "111.24"
# 13.to_s(:rounded, precision: 5) # => "13.00000"
# 389.32314.to_s(:rounded, precision: 0) # => "389"
# 111.2345.to_s(:rounded, significant: true) # => "111"
@@ -72,19 +75,20 @@ module NumericWithFormat
# # => "1.111,23"
#
# Human-friendly size in Bytes:
# 123.to_s(:human_size) # => "123 Bytes"
# 1234.to_s(:human_size) # => "1.21 KB"
# 12345.to_s(:human_size) # => "12.1 KB"
# 1234567.to_s(:human_size) # => "1.18 MB"
# 1234567890.to_s(:human_size) # => "1.15 GB"
# 1234567890123.to_s(:human_size) # => "1.12 TB"
# 1234567890123456.to_s(:human_size) # => "1.1 PB"
# 1234567890123456789.to_s(:human_size) # => "1.07 EB"
# 1234567.to_s(:human_size, precision: 2) # => "1.2 MB"
# 483989.to_s(:human_size, precision: 2) # => "470 KB"
# 1234567.to_s(:human_size, precision: 2, separator: ',') # => "1,2 MB"
# 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB"
# 524288000.to_s(:human_size, precision: 5) # => "500 MB"
# 123.to_s(:human_size) # => "123 Bytes"
# 1234.to_s(:human_size) # => "1.21 KB"
# 12345.to_s(:human_size) # => "12.1 KB"
# 1234567.to_s(:human_size) # => "1.18 MB"
# 1234567890.to_s(:human_size) # => "1.15 GB"
# 1234567890123.to_s(:human_size) # => "1.12 TB"
# 1234567890123456.to_s(:human_size) # => "1.1 PB"
# 1234567890123456789.to_s(:human_size) # => "1.07 EB"
# 1234567.to_s(:human_size, precision: 2) # => "1.2 MB"
# 1234567.to_s(:human_size, precision: 2, round_mode: :up) # => "1.3 MB"
# 483989.to_s(:human_size, precision: 2) # => "470 KB"
# 1234567.to_s(:human_size, precision: 2, separator: ',') # => "1,2 MB"
# 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB"
# 524288000.to_s(:human_size, precision: 5) # => "500 MB"
#
# Human-friendly format:
# 123.to_s(:human) # => "123"
@@ -96,6 +100,7 @@ module NumericWithFormat
# 1234567890123456.to_s(:human) # => "1.23 Quadrillion"
# 1234567890123456789.to_s(:human) # => "1230 Quadrillion"
# 489939.to_s(:human, precision: 2) # => "490 Thousand"
# 489939.to_s(:human, precision: 2, round_mode: :down) # => "480 Thousand"
# 489939.to_s(:human, precision: 4) # => "489.9 Thousand"
# 1234567.to_s(:human, precision: 4,
# significant: false) # => "1.2346 Million"
@@ -44,6 +44,8 @@ en:
delimiter: ","
# Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00)
precision: 3
# Determine how rounding is performed (see BigDecimal::mode)
round_mode: !ruby/sym default
# If set to true, precision will mean the number of significant digits instead
# of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2)
significant: false
@@ -56,10 +58,11 @@ en:
# Where is the currency sign? %u is the currency unit, %n the number (default: $5.00)
format: "%u%n"
unit: "$"
# These five are to override number.format and are optional
# These six are to override number.format and are optional
separator: "."
delimiter: ","
precision: 2
# round_mode:
significant: false
strip_insignificant_zeros: false

@@ -87,10 +90,11 @@ en:
# Used in NumberHelper.number_to_human_size() and NumberHelper.number_to_human()
human:
format:
# These five are to override number.format and are optional
# These six are to override number.format and are optional
# separator:
delimiter: ""
precision: 3
# round_mode:
significant: true
strip_insignificant_zeros: true
# Used in number_to_human_size()
@@ -71,6 +71,8 @@ def number_to_phone(number, options = {})
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the level of precision (defaults
# to 2).
# * <tt>:round_mode</tt> - Determine how rounding is performed
# (defaults to :default. See BigDecimal::mode)
# * <tt>:unit</tt> - Sets the denomination of the currency
# (defaults to "$").
# * <tt>:separator</tt> - Sets the separator between the units
@@ -109,6 +111,8 @@ def number_to_phone(number, options = {})
# # => "1234567890,50 &pound;"
# number_to_currency(1234567890.50, strip_insignificant_zeros: true)
# # => "$1,234,567,890.5"
# number_to_currency(1234567890.50, precision: 0, round_mode: :up)
# # => "$1,234,567,891"
def number_to_currency(number, options = {})
NumberToCurrencyConverter.convert(number, options)
end
@@ -122,6 +126,8 @@ def number_to_currency(number, options = {})
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3). Keeps the number's precision if +nil+.
# * <tt>:round_mode</tt> - Determine how rounding is performed
# (defaults to :default. See BigDecimal::mode)
# * <tt>:significant</tt> - If +true+, precision will be the number
# of significant_digits. If +false+, the number of fractional
# digits (defaults to +false+).
@@ -137,15 +143,16 @@ def number_to_currency(number, options = {})
#
# ==== Examples
#
# number_to_percentage(100) # => "100.000%"
# number_to_percentage('98') # => "98.000%"
# number_to_percentage(100, precision: 0) # => "100%"
# number_to_percentage(1000, delimiter: '.', separator: ',') # => "1.000,000%"
# number_to_percentage(302.24398923423, precision: 5) # => "302.24399%"
# number_to_percentage(1000, locale: :fr) # => "1000,000%"
# number_to_percentage(1000, precision: nil) # => "1000%"
# number_to_percentage('98a') # => "98a%"
# number_to_percentage(100, format: '%n %') # => "100.000 %"
# number_to_percentage(100) # => "100.000%"
# number_to_percentage('98') # => "98.000%"
# number_to_percentage(100, precision: 0) # => "100%"
# number_to_percentage(1000, delimiter: '.', separator: ',') # => "1.000,000%"
# number_to_percentage(302.24398923423, precision: 5) # => "302.24399%"
# number_to_percentage(1000, locale: :fr) # => "1000,000%"
# number_to_percentage(1000, precision: nil) # => "1000%"
# number_to_percentage('98a') # => "98a%"
# number_to_percentage(100, format: '%n %') # => "100.000 %"
# number_to_percentage(302.24398923423, precision: 5, round_mode: :down) # => "302.24398%"
def number_to_percentage(number, options = {})
NumberToPercentageConverter.convert(number, options)
end
@@ -196,6 +203,8 @@ def number_to_delimited(number, options = {})
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3). Keeps the number's precision if +nil+.
# * <tt>:round_mode</tt> - Determine how rounding is performed
# (defaults to :default. See BigDecimal::mode)
# * <tt>:significant</tt> - If +true+, precision will be the number
# of significant_digits. If +false+, the number of fractional
# digits (defaults to +false+).
@@ -217,6 +226,7 @@ def number_to_delimited(number, options = {})
# number_to_rounded(111.2345, precision: 1, significant: true) # => "100"
# number_to_rounded(13, precision: 5, significant: true) # => "13.000"
# number_to_rounded(13, precision: nil) # => "13"
# number_to_rounded(389.32314, precision: 0, round_mode: :up) # => "390"
# number_to_rounded(111.234, locale: :fr) # => "111,234"
#
# number_to_rounded(13, precision: 5, significant: true, strip_insignificant_zeros: true)
@@ -243,6 +253,8 @@ def number_to_rounded(number, options = {})
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3).
# * <tt>:round_mode</tt> - Determine how rounding is performed
# (defaults to :default. See BigDecimal::mode)
# * <tt>:significant</tt> - If +true+, precision will be the number
# of significant_digits. If +false+, the number of fractional
# digits (defaults to +true+)
@@ -266,6 +278,7 @@ def number_to_rounded(number, options = {})
# number_to_human_size(1234567890123456789) # => "1.07 EB"
# number_to_human_size(1234567, precision: 2) # => "1.2 MB"
# number_to_human_size(483989, precision: 2) # => "470 KB"
# number_to_human_size(483989, precision: 2, round_mode: :up) # => "480 KB"
# number_to_human_size(1234567, precision: 2, separator: ',') # => "1,2 MB"
# number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB"
# number_to_human_size(524288000, precision: 5) # => "500 MB"
@@ -293,6 +306,8 @@ def number_to_human_size(number, options = {})
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3).
# * <tt>:round_mode</tt> - Determine how rounding is performed
# (defaults to :default. See BigDecimal::mode)
# * <tt>:significant</tt> - If +true+, precision will be the number
# of significant_digits. If +false+, the number of fractional
# digits (defaults to +true+)
@@ -330,6 +345,8 @@ def number_to_human_size(number, options = {})
# number_to_human(1234567890123456789) # => "1230 Quadrillion"
# number_to_human(489939, precision: 2) # => "490 Thousand"
# number_to_human(489939, precision: 4) # => "489.9 Thousand"
# number_to_human(489939, precision: 2
# , round_mode: :down) # => "480 Thousand"
# number_to_human(1234567, precision: 4,
# significant: false) # => "1.2346 Million"
# number_to_human(1234567, precision: 1,
@@ -20,14 +20,14 @@ def convert
end

formatted_string =
if BigDecimal === rounded_number && rounded_number.finite?
if rounded_number.nan? || rounded_number.infinite? || rounded_number == rounded_number.to_i
"%00.#{precision}f" % rounded_number
else
s = rounded_number.to_s("F")
s << "0" * precision
a, b = s.split(".", 2)
a << "."
a << b[0, precision]
else
"%00.#{precision}f" % rounded_number
end
else
formatted_string = rounded_number
@@ -10,57 +10,41 @@ def initialize(options)
end

def round(number)
precision = absolute_precision(number)
return number unless precision
number = convert_to_decimal(number)
if significant && precision > 0
round_significant(number)
else
round_without_significant(number)
end

rounded_number = convert_to_decimal(number).round(precision, options.fetch(:round_mode, :default))
rounded_number.zero? ? rounded_number.abs : rounded_number # prevent showing negative zeros
end

def digit_count(number)
return 1 if number.zero?
(Math.log10(absolute_number(number)) + 1).floor
(Math.log10(number.abs) + 1).floor
end

private
def round_without_significant(number)
number = number.round(precision)
number = number.to_i if precision == 0 && number.finite?
number = number.abs if number.zero? # prevent showing negative zeros
number
end

def round_significant(number)
return 0 if number.zero?
digits = digit_count(number)
multiplier = 10**(digits - precision)
(number / BigDecimal(multiplier.to_f.to_s)).round * multiplier
end

def convert_to_decimal(number)
case number
when Float, String
BigDecimal(number.to_s)
when Rational
BigDecimal(number, digit_count(number.to_i) + precision)
BigDecimal(number, digit_count(number.to_i) + options[:precision])
else
number.to_d
end
end

def precision
options[:precision]
def absolute_precision(number)
if significant && options[:precision] > 0
options[:precision] - digit_count(convert_to_decimal(number))
else
options[:precision]
end
end

def significant
options[:significant]
end

def absolute_number(number)
number.respond_to?(:abs) ? number.abs : number.to_d.abs
end
end
end
end
@@ -174,6 +174,7 @@ def test_to_s__currency
assert_equal("-$ 1,234,567,890.50", -1234567890.50.to_s(:currency, format: "%u %n"))
assert_equal("($1,234,567,890.50)", -1234567890.50.to_s(:currency, negative_format: "(%u%n)"))
assert_equal("$1,234,567,892", 1234567891.50.to_s(:currency, precision: 0))
assert_equal("$1,234,567,891", 1234567891.50.to_s(:currency, precision: 0, round_mode: :down))
assert_equal("$1,234,567,890.5", 1234567890.50.to_s(:currency, precision: 1))
assert_equal("&pound;1234567890,50", 1234567890.50.to_s(:currency, unit: "&pound;", separator: ",", delimiter: ""))
end
@@ -182,6 +183,7 @@ def test_to_s__rounded
assert_equal("-111.235", -111.2346.to_s(:rounded))
assert_equal("111.235", 111.2346.to_s(:rounded))
assert_equal("31.83", 31.825.to_s(:rounded, precision: 2))
assert_equal("31.82", 31.825.to_s(:rounded, precision: 2, round_mode: :down))
assert_equal("111.23", 111.2346.to_s(:rounded, precision: 2))
assert_equal("111.00", 111.to_s(:rounded, precision: 2))
assert_equal("3268", (32.6751 * 100.00).to_s(:rounded, precision: 0))
@@ -199,6 +201,7 @@ def test_to_s__percentage
assert_equal("100.000%", 100.to_s(:percentage))
assert_equal("100%", 100.to_s(:percentage, precision: 0))
assert_equal("302.06%", 302.0574.to_s(:percentage, precision: 2))
assert_equal("302.05%", 302.0574.to_s(:percentage, precision: 2, round_mode: :down))
assert_equal("123.4%", 123.400.to_s(:percentage, precision: 3, strip_insignificant_zeros: true))
assert_equal("1.000,000%", 1000.to_s(:percentage, delimiter: ".", separator: ","))
assert_equal("1000.000 %", 1000.to_s(:percentage, format: "%n %"))
@@ -248,6 +251,7 @@ def test_to_s__rounded__with_significant_digits
assert_equal "10.0", 9.995.to_s(:rounded, precision: 3, significant: true)
assert_equal "9.99", 9.994.to_s(:rounded, precision: 3, significant: true)
assert_equal "11.0", 10.995.to_s(:rounded, precision: 3, significant: true)
assert_equal "10.9", 10.995.to_s(:rounded, precision: 3, significant: true, round_mode: :down)
end

def test_to_s__rounded__with_strip_insignificant_zeros
@@ -300,6 +304,7 @@ def test_to_s__human_size_with_options_hash
assert_equal "10 MB", 9961472.to_s(:human_size, precision: 0)
assert_equal "40 KB", 41010.to_s(:human_size, precision: 1)
assert_equal "40 KB", 41100.to_s(:human_size, precision: 2)
assert_equal "50 KB", 41100.to_s(:human_size, precision: 1, round_mode: :up)
assert_equal "1.0 KB", kilobytes(1.0123).to_s(:human_size, precision: 2, strip_insignificant_zeros: false)
assert_equal "1.012 KB", kilobytes(1.0123).to_s(:human_size, precision: 3, significant: false)
assert_equal "1 KB", kilobytes(1.0123).to_s(:human_size, precision: 0, significant: true) # ignores significant it precision is 0
@@ -327,6 +332,7 @@ def test_number_to_human
assert_equal "490 Thousand", 489939.to_s(:human, precision: 2)
assert_equal "489.9 Thousand", 489939.to_s(:human, precision: 4)
assert_equal "489 Thousand", 489000.to_s(:human, precision: 4)
assert_equal "480 Thousand", 489939.to_s(:human, precision: 2, round_mode: :down)
assert_equal "489.0 Thousand", 489000.to_s(:human, precision: 4, strip_insignificant_zeros: false)
assert_equal "1.2346 Million", 1234567.to_s(:human, precision: 4, significant: false)
assert_equal "1,2 Million", 1234567.to_s(:human, precision: 1, significant: false, separator: ",")

0 comments on commit 7905bdf

Please sign in to comment.
You can’t perform that action at this time.