Skip to content

Commit 7623549

Browse files
TotalVerbomus
authored andcommitted
Correct floating point trunc, floor, ceil (#83)
1 parent c6e2dc7 commit 7623549

File tree

2 files changed

+81
-21
lines changed

2 files changed

+81
-21
lines changed

src/FixedPointDecimals.jl

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,41 @@ import Base: reinterpret, zero, one, abs, sign, ==, <, <=, +, -, /, *, div,
3232
typemin, typemax, realmin, realmax, print, show, string, convert,
3333
promote_rule, min, max, trunc, round, floor, ceil, eps, float, widemul
3434

35+
const IEEEFloat = Union{Float16, Float32, Float64}
36+
37+
for fn in [:trunc, :floor, :ceil]
38+
fnname = Symbol(fn, "mul")
39+
40+
@eval begin
41+
@doc """
42+
$($fnname)(x, y) :: Integer
43+
44+
Compute `$($fn)(x * y)`. For floating point values, this function can
45+
be more accurate than `$($fn)(x * y)`. The boundary behavior of this
46+
function (e.g. at large values of `x`, `y`) is untested and possibly
47+
incorrect.
48+
""" function $fnname end
49+
50+
$fnname{T <: Number}(x::T, y::T) = $fn(x * y)
51+
52+
$fnname(x::Number, y::Number) = $fnname(promote(x, y)...)
53+
end
54+
55+
if fn === :trunc
56+
# trunc a little different, implement in terms of floor
57+
@eval function $fnname{T <: IEEEFloat}(x::T, y::T)
58+
copysign(floormul(abs(x), abs(y)), x*y)
59+
end
60+
else
61+
# floor and ceil can be implemented the same way
62+
@eval function $fnname{T <: IEEEFloat}(x::T, y::T)
63+
a = x * y
64+
b = fma(x, y, -a)
65+
ifelse(isinteger(a), a + $fn(b), $fn(a))
66+
end
67+
end
68+
end
69+
3570
"""
3671
FixedDecimal{I <: Integer, f::Int}
3772
@@ -129,12 +164,10 @@ for fn in [:trunc, :floor, :ceil]
129164
@eval $fn{TI <: Integer}(::Type{TI}, x::FD)::TI = $fn(x)
130165

131166
# round/trunc/ceil/flooring to FD; generic
132-
# TODO. this is probably incorrect for floating point and we need to check
133-
# overflow in other cases.
167+
# TODO. we may need to check overflow and boundary conditions here.
134168
@eval function $fn{T, f}(::Type{FD{T, f}}, x::Real)
135169
powt = T(10)^f
136-
val = trunc(T, x)
137-
val = val * powt + $fn(T, (x - val) * powt)
170+
val = trunc(T, $(Symbol(fn, "mul"))(x, powt))
138171
reinterpret(FD{T, f}, val)
139172
end
140173
end

test/runtests.jl

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,15 @@ const keyvalues = Dict(
4141

4242
# Floating point values written as integer strings. Useful for testing behaviours of
4343
# trunc, floor, and ceil.
44-
const INT_2_2 = "22000000000000001776356839400250464677" # 2.2
45-
const INT_2_3 = "22999999999999998223643160599749535322" # 2.3
44+
const INTS = Dict(
45+
1.22 => "12199999999999999733546474089962430298328399658203125",
46+
1.23 => "1229999999999999982236431605997495353221893310546875",
47+
1.51 => "15100000000000000088817841970012523233890533447265625",
48+
2.2 => "220000000000000017763568394002504646778106689453125",
49+
2.3 => "229999999999999982236431605997495353221893310546875"
50+
)
51+
const smaller_than_decimal = [1.22, 1.23, 2.3]
52+
const bigger_than_decimal = [1.51, 2.2]
4653

4754

4855
# numbers that may cause overflow
@@ -299,12 +306,22 @@ end
299306
end
300307

301308
@testset "truncate precision" begin
302-
@test trunc(FD2, 2.3) != trunc(FD3, 2.3)
303-
@test trunc(FD2, 2.3) == FD2(2.29)
304-
@test trunc(FD3, 2.3) == FD3(2.299)
309+
for x in smaller_than_decimal
310+
@test trunc(FD2, x) trunc(FD3, x)
311+
@test trunc(FD2, x) == FD2(x - 0.01)
312+
@test trunc(FD3, x) == FD3(x - 0.001)
313+
314+
for f in 0:12
315+
@test trunc(FD{Int64, f}, x) ==
316+
parse_int(FD{Int64, f}, INTS[x])
317+
end
318+
end
305319

306-
for f in 0:12
307-
trunc(FD{Int64, f}, 2.3) == parse_int(FD{Int64, f}, INT_2_3)
320+
for x in bigger_than_decimal
321+
exactval = FD3(x)
322+
for f in 3:12
323+
@test trunc(FD{Int64, f}, x) == exactval
324+
end
308325
end
309326
end
310327
end
@@ -327,20 +344,30 @@ epsi{T}(::Type{T}) = eps(T)
327344
end
328345

329346
@testset "floor, ceil precision" begin
330-
@test floor(FD2, 2.3) != floor(FD3, 2.3)
331-
@test floor(FD2, 2.3) == FD2(2.29)
332-
@test floor(FD3, 2.3) == FD3(2.299)
347+
for x in smaller_than_decimal
348+
@test floor(FD2, x) != floor(FD3, x)
349+
@test floor(FD2, x) == FD2(x - 0.01)
350+
@test floor(FD3, x) == FD3(x - 0.001)
351+
352+
for f in 0:12
353+
@test floor(FD{Int64, f}, x) ==
354+
parse_int(FD{Int64, f}, INTS[x])
355+
end
333356

334-
for f in 0:12
335-
floor(FD{Int64, f}, 2.3) == parse_int(FD{Int64, f}, INT_2_3)
357+
@test ceil(FD3, x) == ceil(FD4, x) == FD4(x)
336358
end
337359

338-
@test ceil(FD2, 2.2) != ceil(FD3, 2.2)
339-
@test ceil(FD2, 2.2) == FD2(2.21)
340-
@test ceil(FD3, 2.2) == FD3(2.201)
360+
for x in bigger_than_decimal
361+
@test ceil(FD2, x) ceil(FD3, x)
362+
@test ceil(FD2, x) == FD2(x + 0.01)
363+
@test ceil(FD3, x) == FD3(x + 0.001)
364+
365+
for f in 0:12
366+
@test ceil(FD{Int64, f}, x) ==
367+
parse_int(FD{Int64, f}, INTS[x], ceil=true)
368+
end
341369

342-
for f in 0:12
343-
ceil(FD{Int64, f}, 2.2) == parse_int(FD{Int64, f}, INT_2_2, ceil=true)
370+
@test floor(FD3, x) == floor(FD4, x) == FD4(x)
344371
end
345372
end
346373
end

0 commit comments

Comments
 (0)