From 9574c59ec2baff5a98686b188a2296f6de76fb59 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 29 Nov 2023 00:14:38 +0800 Subject: [PATCH] Tidy up `Float::Printer` --- .../shortest_spec.cr} | 4 +- src/float.cr | 8 +- src/float/printer.cr | 151 +++++++++--------- src/humanize.cr | 2 +- 4 files changed, 85 insertions(+), 80 deletions(-) rename spec/std/{float_printer_spec.cr => float_printer/shortest_spec.cr} (99%) diff --git a/spec/std/float_printer_spec.cr b/spec/std/float_printer/shortest_spec.cr similarity index 99% rename from spec/std/float_printer_spec.cr rename to spec/std/float_printer/shortest_spec.cr index b5c72d999868..b012e3da19ac 100644 --- a/spec/std/float_printer_spec.cr +++ b/spec/std/float_printer/shortest_spec.cr @@ -34,9 +34,9 @@ # * https://github.com/microsoft/STL/tree/main/tests/std/tests/P0067R5_charconv require "spec" -require "./spec_helper" +require "../spec_helper" require "spec/helpers/string" -require "../support/number" +require "../../support/number" # Tests that `v.to_s` is the same as the *v* literal is written in the source # code, except possibly omitting the `_f32` suffix for `Float32` literals. diff --git a/src/float.cr b/src/float.cr index a4abcf5abdf8..1e620e4e358e 100644 --- a/src/float.cr +++ b/src/float.cr @@ -214,12 +214,12 @@ struct Float32 def to_s : String String.build(22) do |buffer| - Printer.print(self, buffer) + Printer.shortest(self, buffer) end end def to_s(io : IO) : Nil - Printer.print(self, io) + Printer.shortest(self, io) end def clone @@ -338,12 +338,12 @@ struct Float64 def to_s : String # the longest `Float64` strings are of the form `-1.2345678901234567e+123` String.build(24) do |buffer| - Printer.print(self, buffer) + Printer.shortest(self, buffer) end end def to_s(io : IO) : Nil - Printer.print(self, io) + Printer.shortest(self, io) end def clone diff --git a/src/float/printer.cr b/src/float/printer.cr index 2189e365a5fe..f8666e1ea8d0 100644 --- a/src/float/printer.cr +++ b/src/float/printer.cr @@ -1,21 +1,91 @@ require "./printer/*" # :nodoc: -# -# `Float::Printer` is based on the [Dragonbox](https://github.com/jk-jeon/dragonbox) -# algorithm developed by Junekey Jeon around 2020-2021. module Float::Printer extend self + BUFFER_SIZE = 17 # maximum number of decimal digits required - # Converts `Float` *v* to a string representation and prints it onto *io*. + # Writes *v*'s shortest string representation to the given *io*. + # + # Based on the [Dragonbox](https://github.com/jk-jeon/dragonbox) algorithm + # developed by Junekey Jeon around 2020-2021. # - # It is used by `Float64#to_s` and it is probably not necessary to use - # this directly. + # It is used by `Float::Primitive#to_s` and `Number#format`. It is probably + # not necessary to use this directly. # # *point_range* designates the boundaries of scientific notation which is used # for all values whose decimal point position is outside that range. - def print(v : Float64 | Float32, io : IO, *, point_range = -3..15) : Nil + def shortest(v : Float64 | Float32, io : IO, *, point_range = -3..15) : Nil + check_special_float(v, io) do |pos_v| + significand, decimal_exponent = Dragonbox.to_decimal(pos_v) + + # generate `significand.to_s` in a reasonably fast manner + str = uninitialized UInt8[BUFFER_SIZE] + ptr = str.to_unsafe + BUFFER_SIZE + while significand > 0 + ptr -= 1 + ptr.value = 48_u8 &+ significand.unsafe_mod(10).to_u8! + significand = significand.unsafe_div(10) + end + + # remove trailing zeros + buffer = str.to_slice[ptr - str.to_unsafe..] + while buffer.size > 1 && buffer.unsafe_fetch(buffer.size - 1) === '0' + buffer = buffer[..-2] + decimal_exponent += 1 + end + length = buffer.size + + point = decimal_exponent + length + + exp = point + exp_mode = !point_range.includes?(point) + point = 1 if exp_mode + + # add leading zero + io << '0' if point < 1 + + i = 0 + + # add integer part digits + if decimal_exponent > 0 && !exp_mode + # whole number but not big enough to be exp form + io.write_string buffer.to_slice[i, length - i] + i = length + (point - length).times { io << '0' } + elsif i < point + io.write_string buffer.to_slice[i, point - i] + i = point + end + + io << '.' + + # add leading zeros after point + if point < 0 + (-point).times { io << '0' } + end + + # add fractional part digits + io.write_string buffer.to_slice[i, length - i] + + # print trailing 0 if whole number or exp notation of power of ten + if (decimal_exponent >= 0 && !exp_mode) || (exp != point && length == 1) + io << '0' + end + + # exp notation + if exp != point + io << 'e' + io << '+' if exp > 0 + (exp - 1).to_s(io) + end + end + end + + # If *v* is finite and nonzero, yields its absolute value, otherwise writes + # *v* to *io* + private def check_special_float(v : Float::Primitive, io : IO, &) d = IEEE.to_uint(v) if IEEE.nan?(d) @@ -33,72 +103,7 @@ module Float::Printer elsif IEEE.inf?(d) io << "Infinity" else - internal(v, io, point_range) - end - end - - private def internal(v : Float64 | Float32, io : IO, point_range) - significand, decimal_exponent = Dragonbox.to_decimal(v) - - # generate `significand.to_s` in a reasonably fast manner - str = uninitialized UInt8[BUFFER_SIZE] - ptr = str.to_unsafe + BUFFER_SIZE - while significand > 0 - ptr -= 1 - ptr.value = 48_u8 &+ significand.unsafe_mod(10).to_u8! - significand = significand.unsafe_div(10) - end - - # remove trailing zeros - buffer = str.to_slice[ptr - str.to_unsafe..] - while buffer.size > 1 && buffer.unsafe_fetch(buffer.size - 1) === '0' - buffer = buffer[..-2] - decimal_exponent += 1 - end - length = buffer.size - - point = decimal_exponent + length - - exp = point - exp_mode = !point_range.includes?(point) - point = 1 if exp_mode - - # add leading zero - io << '0' if point < 1 - - i = 0 - - # add integer part digits - if decimal_exponent > 0 && !exp_mode - # whole number but not big enough to be exp form - io.write_string buffer.to_slice[i, length - i] - i = length - (point - length).times { io << '0' } - elsif i < point - io.write_string buffer.to_slice[i, point - i] - i = point - end - - io << '.' - - # add leading zeros after point - if point < 0 - (-point).times { io << '0' } - end - - # add fractional part digits - io.write_string buffer.to_slice[i, length - i] - - # print trailing 0 if whole number or exp notation of power of ten - if (decimal_exponent >= 0 && !exp_mode) || (exp != point && length == 1) - io << '0' - end - - # exp notation - if exp != point - io << 'e' - io << '+' if exp > 0 - (exp - 1).to_s(io) + yield v end end end diff --git a/src/humanize.cr b/src/humanize.cr index 76f4fc3b2a12..16cff9d63174 100644 --- a/src/humanize.cr +++ b/src/humanize.cr @@ -41,7 +41,7 @@ struct Number else string = String.build do |io| # Make sure to avoid scientific notation of default Float#to_s - Float::Printer.print(number.abs, io, point_range: ..) + Float::Printer.shortest(number.abs, io, point_range: ..) end _, _, decimals = string.partition(".") integer, _, _ = ("%f" % number.abs).partition(".")