diff --git a/spec/std/float_printer/ryu_printf_spec.cr b/spec/std/float_printer/ryu_printf_spec.cr index 02ac2944a37d..3318c26f3265 100644 --- a/spec/std/float_printer/ryu_printf_spec.cr +++ b/spec/std/float_printer/ryu_printf_spec.cr @@ -32,66 +32,11 @@ require "./ryu_printf_test_cases" struct BigFloat def to_s_with_range(*, point_range : Range = -3..15) - String.build do |io| - to_s_with_range(io, point_range: point_range) - end + to_s_impl(point_range: point_range, int_trailing_zeros: false) end def to_s_with_range(io : IO, *, point_range : Range = -3..15) : Nil - cstr = LibGMP.mpf_get_str(nil, out decimal_exponent, 10, 0, self) - length = LibC.strlen(cstr) - buffer = Slice.new(cstr, length) - - # add negative sign - if buffer[0]? == 45 # '-' - io << '-' - buffer = buffer[1..] - length -= 1 - end - - point = decimal_exponent - exp = point - exp_mode = !point_range.includes?(point) - point = 1 if exp_mode - - # add leading zero - io << '0' if point < 1 - - # add integer part digits - if decimal_exponent > 0 && !exp_mode - # whole number but not big enough to be exp form - io.write_string buffer[0, {decimal_exponent, length}.min] - buffer = buffer[{decimal_exponent, length}.min...] - (point - length).times { io << '0' } - elsif point > 0 - io.write_string buffer[0, point] - buffer = buffer[point...] - end - - # skip `.0000...` - unless buffer.all?(&.=== '0') - io << '.' - - # add leading zeros after point - if point < 0 - (-point).times { io << '0' } - end - - # add fractional part digits - io.write_string buffer - - # print trailing 0 if whole number or exp notation of power of ten - if (decimal_exponent >= length && !exp_mode) || ((exp != point || exp_mode) && length == 1) - io << '0' - end - end - - # exp notation - if exp_mode - io << 'e' - io << '+' if exp > 0 - (exp - 1).to_s(io) - end + to_s_impl(io, point_range: point_range, int_trailing_zeros: false) end end diff --git a/spec/std/humanize_spec.cr b/spec/std/humanize_spec.cr index 1505bcab19d0..abf4213b7887 100644 --- a/spec/std/humanize_spec.cr +++ b/spec/std/humanize_spec.cr @@ -93,6 +93,18 @@ describe Number do it { assert_prints (-Float64::INFINITY).format, "-Infinity" } it { assert_prints Float64::NAN.format, "NaN" } + it { assert_prints "12345678.90123".to_big_f.format, "12,345,678.90123" } + it { assert_prints "12345678.90123".to_big_f.format(decimal_places: 10), "12,345,678.9012300000" } + it { assert_prints "12345678.90123".to_big_f.format(decimal_places: -4), "12,350,000" } + + it { assert_prints (2.to_big_f ** 58).format, "288,230,376,151,711,744.0" } + it { assert_prints (2.to_big_f ** 58).format(decimal_places: 10), "288,230,376,151,711,744.0000000000" } + it { assert_prints (2.to_big_f ** 58).format(decimal_places: -5), "288,230,376,151,700,000" } + + it { assert_prints (2.to_big_f ** -16).format, "0.0000152587890625" } + it { assert_prints (2.to_big_f ** -16).format(decimal_places: 20), "0.00001525878906250000" } + it { assert_prints (2.to_big_f ** -16).format(decimal_places: 10), "0.0000152588" } + it { assert_prints "12345.67890123456789012345".to_big_d.format, "12,345.67890123456789012345" } it "extracts integer part correctly (#12997)" do diff --git a/src/big/big_float.cr b/src/big/big_float.cr index 5a57500fbdd7..74ce7451a0b5 100644 --- a/src/big/big_float.cr +++ b/src/big/big_float.cr @@ -362,6 +362,15 @@ struct BigFloat < Float end def to_s(io : IO) : Nil + to_s_impl(io, point_range: -3..15, int_trailing_zeros: true) + end + + protected def to_s_impl(*, point_range : Range, int_trailing_zeros : Bool) : String + String.build { |io| to_s_impl(io, point_range: point_range, int_trailing_zeros: int_trailing_zeros) } + end + + # TODO: refactor into `Float::Printer.shortest` + protected def to_s_impl(io : IO, *, point_range : Range, int_trailing_zeros : Bool) : Nil cstr = LibGMP.mpf_get_str(nil, out orig_decimal_exponent, 10, 0, self) length = LibC.strlen(cstr) buffer = Slice.new(cstr, length) @@ -377,7 +386,7 @@ struct BigFloat < Float point = decimal_exponent exp = point - exp_mode = point > 15 || point < -3 + exp_mode = !point_range.includes?(point) point = 1 if exp_mode # add leading zero @@ -394,23 +403,27 @@ struct BigFloat < Float buffer = buffer[point...] end - io << '.' + # omit `.0000...000` if *int_trailing_zeros* is false (used by the + # Ryu Printf specs only) + if int_trailing_zeros || !buffer.all?(&.=== '0') + io << '.' - # add leading zeros after point - if point < 0 - (-point).times { io << '0' } - end + # add leading zeros after point + if point < 0 + (-point).times { io << '0' } + end - # add fractional part digits - io.write_string buffer + # add fractional part digits + io.write_string buffer - # print trailing 0 if whole number or exp notation of power of ten - if (decimal_exponent >= length && !exp_mode) || (exp != point && length == 1) - io << '0' + # print trailing 0 if whole number or exp notation of power of ten + if (decimal_exponent >= length && !exp_mode) || ((exp != point || exp_mode) && length == 1) + io << '0' + end end # exp notation - if exp != point + if exp_mode io << 'e' io << '+' if exp > 0 (exp - 1).to_s(io) @@ -466,6 +479,27 @@ struct BigFloat < Float {% end %} end + # :inherit: + def format(io : IO, separator = '.', delimiter = ',', decimal_places : Int? = nil, *, group : Int = 3, only_significant : Bool = false) : Nil + number = self + if decimal_places + number = number.round(decimal_places) + end + + if decimal_places && decimal_places >= 0 + string = number.abs.to_s_impl(point_range: .., int_trailing_zeros: true) + integer, _, decimals = string.partition('.') + else + string = number.to_s_impl(point_range: .., int_trailing_zeros: true) + _, _, decimals = string.partition(".") + integer = number.trunc.to_big_i.abs.to_s + end + + is_negative = number < 0 + + format_impl(io, is_negative, integer, decimals, separator, delimiter, decimal_places, group, only_significant) + end + def clone self end diff --git a/src/humanize.cr b/src/humanize.cr index e0a5f3f17844..37e129f3bd9d 100644 --- a/src/humanize.cr +++ b/src/humanize.cr @@ -56,10 +56,23 @@ struct Number integer, _, decimals = string.partition('.') end + is_negative = number.is_a?(Float::Primitive) ? Math.copysign(1, number) < 0 : number < 0 + + format_impl(io, is_negative, integer, decimals, separator, delimiter, decimal_places, group, only_significant) + end + + # :ditto: + def format(separator = '.', delimiter = ',', decimal_places : Int? = nil, *, group : Int = 3, only_significant : Bool = false) : String + String.build do |io| + format(io, separator, delimiter, decimal_places, group: group, only_significant: only_significant) + end + end + + private def format_impl(io, is_negative, integer, decimals, separator, delimiter, decimal_places, group, only_significant) : Nil int_size = integer.size dec_size = decimals.size - io << '-' if number.is_a?(Float::Primitive) ? Math.copysign(1, number) < 0 : number < 0 + io << '-' if is_negative start = int_size % group start += group if start == 0 @@ -91,13 +104,6 @@ struct Number end end - # :ditto: - def format(separator = '.', delimiter = ',', decimal_places : Int? = nil, *, group : Int = 3, only_significant : Bool = false) : String - String.build do |io| - format(io, separator, delimiter, decimal_places, group: group, only_significant: only_significant) - end - end - # Default SI prefixes ordered by magnitude. SI_PREFIXES = { {'q', 'r', 'y', 'z', 'a', 'f', 'p', 'n', 'ยต', 'm'}, {nil, 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q'} }