diff --git a/lib/cldr/format/decimal.rb b/lib/cldr/format/decimal.rb index dfc4dcd7..9f8404fb 100644 --- a/lib/cldr/format/decimal.rb +++ b/lib/cldr/format/decimal.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "bigdecimal" + module Cldr module Format class Decimal @@ -15,7 +17,7 @@ def initialize(format, symbols = {}) end def apply(number, options = {}) - number = Float(number) + number = BigDecimal(number) unless number.is_a?(::Numeric) format = number.abs == number ? positive : negative format.apply(number, options) rescue TypeError, ArgumentError diff --git a/lib/cldr/format/decimal/number.rb b/lib/cldr/format/decimal/number.rb index 0f95de9d..fe2a549b 100644 --- a/lib/cldr/format/decimal/number.rb +++ b/lib/cldr/format/decimal/number.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "bigdecimal" + module Cldr module Format class Decimal @@ -33,12 +35,13 @@ def parse_format(format, symbols = {}) def parse_number(number, options = {}) precision = options[:precision] || fraction_format.precision number = round_to(number, precision) - number.abs.to_s.split(".") + number.abs.to_s("F").split(".") end + # BigDecimal keeps full precision, avoids Float scientific notation, and + # rounds half-even (ties to even) as CLDR / UTS #35 mandates. def round_to(number, precision) - factor = 10**precision - (number * factor).round.to_f / factor + BigDecimal(number.to_r, 0).round(precision, half: :even) end end end diff --git a/test/format/currency_test.rb b/test/format/currency_test.rb index e69de29b..8d393b94 100644 --- a/test/format/currency_test.rb +++ b/test/format/currency_test.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require File.expand_path(File.join(File.dirname(__FILE__) + "/../test_helper")) + +class TestCldrCurrencyFormat < Test::Unit::TestCase + test "interpolates the currency symbol" do + assert_equal "$123.45", Cldr::Format::Currency.new("¤0.00").apply(123.45, currency: "$") + end + + test "rounds half-even like the decimal formatter" do + assert_equal "$0.12", Cldr::Format::Currency.new("¤0.00").apply(0.125, currency: "$") + assert_equal "$2", Cldr::Format::Currency.new("¤0").apply(2.5, currency: "$") + end + + test "formats a large amount without scientific notation" do + assert_equal "$12,345,678,901,234,567,890", Cldr::Format::Currency.new("¤#,##0").apply(12345678901234567890, currency: "$") + end +end diff --git a/test/format/decimal/number_test.rb b/test/format/decimal/number_test.rb index e4f08402..7d5bb0ba 100644 --- a/test/format/decimal/number_test.rb +++ b/test/format/decimal/number_test.rb @@ -74,4 +74,34 @@ class TestCldrDecimalNumberFormat < Test::Unit::TestCase test "cldr example 00000.0000 => 01234,5670" do assert_equal "01234,5670", Cldr::Format::Decimal::Number.new("00000.0000", decimal: ",", group: " ").apply(1234.567) end + + # CLDR / UTS #35 rounds half-even; expected values cross-checked against ICU and Babel 2.17. + test "rounds exact halves to even at precision 2" do + number = Cldr::Format::Decimal::Number.new("0.00") + assert_equal "0.12", number.apply(0.125) + assert_equal "0.62", number.apply(0.625) + assert_equal "0.38", number.apply(0.375) + assert_equal "0.88", number.apply(0.875) + assert_equal "2.12", number.apply(2.125) + end + + test "rounds exact halves to even at precision 0" do + number = Cldr::Format::Decimal::Number.new("0") + assert_equal "0", number.apply(0.5) + assert_equal "2", number.apply(1.5) + assert_equal "2", number.apply(2.5) + assert_equal "4", number.apply(3.5) + assert_equal "4", number.apply(4.5) + end + + test "formats large integers without scientific notation" do + number = Cldr::Format::Decimal::Number.new("#,##0") + assert_equal "1,000,000,000,000,000", number.apply(10**15) + assert_equal "10,000,000,000,000,000", number.apply(10**16) + assert_equal "12,345,678,901,234,567,890", number.apply(12345678901234567890) + end + + test "keeps full precision for integers above 2**53" do + assert_equal "9,007,199,254,740,993", Cldr::Format::Decimal::Number.new("#,##0").apply(2**53 + 1) + end end diff --git a/test/format/decimal_test.rb b/test/format/decimal_test.rb index c4232b9a..9fbdcf52 100644 --- a/test/format/decimal_test.rb +++ b/test/format/decimal_test.rb @@ -18,4 +18,18 @@ class TestCldrDecimalFormat < Test::Unit::TestCase test "positive/negative patterns, negative number" do assert_equal "-123", Cldr::Format::Decimal.new("#;-#").apply(-123) end + + test "rounds half-even through the public formatter" do + assert_equal "2", Cldr::Format::Decimal.new("0").apply(2.5) + assert_equal "-2", Cldr::Format::Decimal.new("0").apply(-2.5) + assert_equal "0.12", Cldr::Format::Decimal.new("0.00").apply(0.125) + end + + test "formats a large integer without precision loss" do + assert_equal "12,345,678,901,234,567,890", Cldr::Format::Decimal.new("#,##0").apply(12345678901234567890) + end + + test "leaves a non-numeric argument untouched" do + assert_equal "abc", Cldr::Format::Decimal.new("#").apply("abc") + end end diff --git a/test/format/percent_test.rb b/test/format/percent_test.rb index e69de29b..cbf42d93 100644 --- a/test/format/percent_test.rb +++ b/test/format/percent_test.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require File.expand_path(File.join(File.dirname(__FILE__) + "/../test_helper")) + +class TestCldrPercentFormat < Test::Unit::TestCase + test "interpolates the percent sign" do + assert_equal "12.34 %", Cldr::Format::Percent.new("0.00 %").apply(12.34) + end + + test "rounds half-even like the decimal formatter" do + assert_equal "0.12 %", Cldr::Format::Percent.new("0.00 %").apply(0.125) + assert_equal "2 %", Cldr::Format::Percent.new("0 %").apply(2.5) + end +end