Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Float: more accurate to_s #2650

Merged
merged 1 commit into from
May 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions spec/std/float_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,42 @@ describe "Float" do
end

describe "to_s" do
it "does to_s for f32 and f64" do
it "does to_s for f64" do
12.34.to_s.should eq("12.34")
12.34_f64.to_s.should eq("12.34")
1.2.to_s.should eq("1.2")
1.23.to_s.should eq("1.23")
1.234.to_s.should eq("1.234")
0.65000000000000002.to_s.should eq("0.65")
1.234001.to_s.should eq("1.234001")
1.23499.to_s.should eq("1.23499")
1.23499999999999.to_s.should eq("1.235")
1.2345.to_s.should eq("1.2345")
1.23456.to_s.should eq("1.23456")
1.234567.to_s.should eq("1.234567")
1.2345678.to_s.should eq("1.2345678")
1.23456789.to_s.should eq("1.23456789")
1.234567891.to_s.should eq("1.234567891")
1.2345678911.to_s.should eq("1.2345678911")
1.2345678912.to_s.should eq("1.2345678912")
1.23456789123.to_s.should eq("1.23456789123")
9525365.25.to_s.should eq("9525365.25")
12.9999.to_s.should eq("12.9999")
12.999999999999.to_s.should eq("13.0")
1.0.to_s.should eq("1.0")
end

it "does to_s for f32" do
12.34_f32.to_s.should eq("12.34")
1.2_f32.to_s.should eq("1.2")
1.23_f32.to_s.should eq("1.23")
1.234_f32.to_s.should eq("1.234")
0.65000000000000002_f32.to_s.should eq("0.65")
# 1.234001_f32.to_s.should eq("1.234001")
1.23499_f32.to_s.should eq("1.23499")
1.23499999999999_f32.to_s.should eq("1.235")
1.2345_f32.to_s.should eq("1.2345")
1.23456_f32.to_s.should eq("1.23456")
# 9525365.25_f32.to_s.should eq("9525365.25")
end
end

Expand Down
122 changes: 116 additions & 6 deletions src/float.cr
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,17 @@ struct Float32
end

def to_s
to_f64.to_s
String.new(22) do |buffer|
LibC.snprintf(buffer, 22, "%g", to_f64)
len = LibC.strlen(buffer)
{len, len}
end
end

def to_s(io : IO)
to_f64.to_s(io)
chars = StaticArray(UInt8, 22).new(0_u8)
LibC.snprintf(chars, 22, "%g", to_f64)
io.write_utf8 chars.to_slice[0, LibC.strlen(chars)]
end

def hash
Expand Down Expand Up @@ -192,16 +198,120 @@ struct Float64

def to_s
String.new(22) do |buffer|
LibC.snprintf(buffer, 22, "%g", self)
len = LibC.strlen(buffer)
len = to_s_internal(buffer)
{len, len}
end
end

def to_s(io : IO)
chars = StaticArray(UInt8, 22).new(0_u8)
LibC.snprintf(chars, 22, "%g", self)
io.write_utf8 chars.to_slice[0, LibC.strlen(chars)]
len = to_s_internal(chars.to_unsafe)
io.write_utf8 chars.to_slice[0, len]
end

private def to_s_internal(buffer)
LibC.snprintf(buffer, 22, "%.17g", self)
len = LibC.strlen(buffer)

# Check if we have a run of zeros or nines after
# the decimal digit. If so, we remove them
# (rounding, if needed). This is a very simple
# (and probably inefficient) algorithm, but a good
# one is much longer and harder to do: we can probably
# do that later.
slice = Slice.new(buffer, len)
index = slice.index('.'.ord.to_u8)

# If there's no dot add ".0" to it, if there's enough size
unless index
if len < 21
buffer[len] = '.'.ord.to_u8
buffer[len + 1] = '0'.ord.to_u8
len += 2
end
return len
end

# Also return if the dot is the last char (shouldn't happen)
return len if index + 1 == len

# And also return if the length is less than 7
# (digit, dot plus at least 5 digits)
return len if len < 7

this_run = 0 # number of chars in this run
max_run = 0 # maximum consecutive chars of a run
run_byte = 0_u8 # the run character
last_run_start = -1 # where did the last run start
max_run_byte = 0_u8 # the byte of the last run
max_run_start = -1 # the index where the maximum run starts
max_run_end = -1 # the index where the maximum run ends

while index < len
byte = slice.to_unsafe[index]

if byte == run_byte
this_run += 1
if this_run > max_run
max_run = this_run
max_run_byte = byte
max_run_start = last_run_start
max_run_end = index
end
elsif byte === '0' || byte === '9'
run_byte = byte
last_run_byte = byte
last_run_start = index
this_run = 1
else
run_byte = 0_u8
this_run = 0
end

index += 1
end

# If the maximum run ends one or two chars before
# the end of the string, we replace the run
# (only if the run is long, 5 or more chars)
if (len - 3 <= max_run_end < len) && max_run >= 5
case max_run_byte
when '0'
# Just trim
len = max_run_start
when '9'
# Need to add one and carry to the left
len = max_run_start
index = len - 1
while index > 0
byte = slice.to_unsafe[index]
case byte
when '.'
# Nothing, continue
when '9'
# If this is the last char, remove it,
# otherwise turn into a zero
if index == len
len -= 1
else
slice.to_unsafe[index] = '0'.ord.to_u8
end
else
slice.to_unsafe[index] = byte + 1
break
end
index -= 1
end
end
end

# Add a zero if the last char is a dot
if slice.to_unsafe[len - 1] === '.'
slice.to_unsafe[len] = '0'.ord.to_u8
len += 1
end

len
end

def hash
Expand Down