Skip to content

Commit

Permalink
Tidy up Float::Printer
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil committed Nov 28, 2023
1 parent a59b5ab commit 9574c59
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions src/float.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
151 changes: 78 additions & 73 deletions src/float/printer.cr
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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
2 changes: 1 addition & 1 deletion src/humanize.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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(".")
Expand Down

0 comments on commit 9574c59

Please sign in to comment.