Skip to content

Commit

Permalink
Merge pull request #2650 from crystal-lang/feature/float_to_s
Browse files Browse the repository at this point in the history
Float: more accurate to_s
  • Loading branch information
Ary Borenszweig committed May 26, 2016
2 parents 463d301 + 649c990 commit 0cb2388
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 8 deletions.
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

0 comments on commit 0cb2388

Please sign in to comment.