diff --git a/Project.toml b/Project.toml index 11f7b50..8a3875b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FixedPointDecimals" uuid = "fb4d412d-6eee-574d-9565-ede6634db7b0" authors = ["Fengyang Wang ", "Curtis Vogt "] -version = "0.6.1" +version = "0.6.2" [deps] BitIntegers = "c3b6d118-76ef-56ca-8cc7-ebb389d030a1" diff --git a/src/FixedPointDecimals.jl b/src/FixedPointDecimals.jl index 458dee5..9e5e25d 100644 --- a/src/FixedPointDecimals.jl +++ b/src/FixedPointDecimals.jl @@ -445,7 +445,7 @@ overflow/underflow did in fact happen. Throws a DivideError on divide-by-zero. function div_with_overflow(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f} C = coefficient(FD{T, f}) # This case will break the div call below. - if T <: Signed && x.i == typemin(T) && y.i == -1 + if y.i == -1 && T <: Signed && hasmethod(typemin, (Type{T},)) && x.i == typemin(T) # To perform the div and overflow means reaching the max and adding 1, so typemin. return (x, true) end @@ -454,6 +454,77 @@ function div_with_overflow(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f} return (reinterpret(FD{T,f}, v), b) end +# Does not exist in Base.Checked, so just exists in this package. +@doc """ + FixedPointDecimals.fld_with_overflow(x::FD, y::FD)::Tuple{FD,Bool} + +Calculates the largest integer less than or equal to `x / y`, checking for overflow errors +where applicable, returning the result and a boolean indicating whether overflow occured. +Throws a DivideError on divide-by-zero. + +The overflow protection may impose a perceptible performance penalty. + +See also: +- `Base.checked_fld`. +""" +function fld_with_overflow(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f} + C = coefficient(FD{T, f}) + # This case will break the fld call below. + if y.i == -1 && T <: Signed && hasmethod(typemin, (Type{T},)) && x.i == typemin(T) + # To fld and overflow means reaching the max and adding 1, so typemin (x). + return (x, true) + end + # Note: The fld() will already throw for divide-by-zero, that's not an overflow. + v, b = Base.Checked.mul_with_overflow(C, fld(x.i, y.i)) + return (reinterpret(FD{T, f}, v), b) +end + +""" + FixedPointDecimals.rdiv_with_overflow(x::FD, y::FD)::Tuple{FD,Bool} + +Calculates `x / y`, checking for overflow errors where applicable, returning the result +and a boolean indicating whether overflow occured. Throws a DivideError on divide-by-zero. + +The overflow protection may impose a perceptible performance penalty. + +See also: +- `Base.checked_rdiv`. +""" +function rdiv_with_overflow(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f} + powt = coefficient(FD{T, f}) + # No multiplication can reach the typemax/typemin of a wider type, thus no typemin / -1. + quotient, remainder = fldmod(_widemul(x.i, powt), y.i) + # quotient is necessarily not typemax/typemin. x.i * powt cannot reach typemax/typemin + # of the widened type and y.i is an integer. Thus the following call cannot overflow. + v = _round_to_nearest(quotient, remainder, y.i) + return (reinterpret(FD{T,f}, rem(v, T)), v < typemin(T) || v > typemax(T)) +end + +# These functions allow us to perform division with integers outside of the range of the +# FixedDecimal. +function rdiv_with_overflow(x::Integer, y::FD{T, f}) where {T<:Integer, f} + powt = coefficient(FD{T, f}) + powtsq = _widemul(powt, powt) + # No multiplication can reach the typemax/typemin of a wider type, thus no typemin / -1. + quotient, remainder = fldmod(_widemul(x, powtsq), y.i) + # Same deal as previous overload as to why this will not overload. Note that all + # multiplication operations were widemuls. + v = _round_to_nearest(quotient, remainder, y.i) + return (reinterpret(FD{T,f}, rem(v, T)), v < typemin(T) || v > typemax(T)) +end +function rdiv_with_overflow(x::FD{T, f}, y::Integer) where {T<:Integer, f} + if y == -1 && T <: Signed && hasmethod(typemin, (Type{T},)) && x.i == typemin(T) + # typemin / -1 for signed integers wraps, giving typemin (x) again. + return (x, true) + end + + quotient, remainder = fldmod(x.i, y) + # It is impossible for both the quotient to be typemax/typemin AND remainder to be + # non-zero because y is an integer. Thus the following call cannot overflow. + v = _round_to_nearest(quotient, remainder, y) + return (reinterpret(FD{T, f}, v), false) +end + Base.checked_add(x::FD, y::FD) = Base.checked_add(promote(x, y)...) Base.checked_sub(x::FD, y::FD) = Base.checked_sub(promote(x, y)...) Base.checked_mul(x::FD, y::FD) = Base.checked_mul(promote(x, y)...) @@ -547,28 +618,22 @@ See also: checked_rdiv(x::FD, y::FD) = checked_rdiv(promote(x, y)...) function checked_rdiv(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f} - powt = coefficient(FD{T, f}) - quotient, remainder = fldmod(_widemul(x.i, powt), y.i) - v = _round_to_nearest(quotient, remainder, y.i) - typemin(T) <= v <= typemax(T) || Base.Checked.throw_overflowerr_binaryop(:/, x, y) - return reinterpret(FD{T, f}, v) + (z, b) = rdiv_with_overflow(x, y) + b && Base.Checked.throw_overflowerr_binaryop(:/, x, y) + return z end # These functions allow us to perform division with integers outside of the range of the # FixedDecimal. function checked_rdiv(x::Integer, y::FD{T, f}) where {T<:Integer, f} - powt = coefficient(FD{T, f}) - powtsq = _widemul(powt, powt) - quotient, remainder = fldmod(_widemul(x, powtsq), y.i) - v = _round_to_nearest(quotient, remainder, y.i) - typemin(T) <= v <= typemax(T) || Base.Checked.throw_overflowerr_binaryop(:/, x, y) - reinterpret(FD{T, f}, v) + (z, b) = rdiv_with_overflow(x, y) + b && Base.Checked.throw_overflowerr_binaryop(:/, x, y) + return z end function checked_rdiv(x::FD{T, f}, y::Integer) where {T<:Integer, f} - quotient, remainder = fldmod(x.i, y) - v = _round_to_nearest(quotient, remainder, y) - typemin(T) <= v <= typemax(T) || Base.Checked.throw_overflowerr_binaryop(:/, x, y) - reinterpret(FD{T, f}, v) + (z, b) = rdiv_with_overflow(x, y) + b && Base.Checked.throw_overflowerr_binaryop(:/, x, y) + return z end diff --git a/test/FixedDecimal.jl b/test/FixedDecimal.jl index 3fd4138..b1d8528 100644 --- a/test/FixedDecimal.jl +++ b/test/FixedDecimal.jl @@ -777,6 +777,8 @@ end end @testset "limits: with_overflow math" begin + using FixedPointDecimals: rdiv_with_overflow, fld_with_overflow + # Easy to reason about cases of overflow: @test Base.Checked.add_with_overflow(FD{Int8,2}(1), FD{Int8,2}(1)) == (FD{Int8,2}(-0.56), true) @test Base.Checked.add_with_overflow(FD{Int8,2}(1), FD{Int8,2}(1)) == (FD{Int8,2}(-0.56), true) @@ -789,6 +791,19 @@ end @test div_with_overflow(typemin(FD{Int32,0}), FD{Int32,0}(-1)) == (typemin(FD{Int32,0}), true) @test div_with_overflow(FD{Int16,1}(1639), FD{Int16,1}(0.5)) == (FD{Int16,1}(-3275.6), true) + @test rdiv_with_overflow(Int8(1), FD{Int8,2}(0.7)) == (FD{Int8,2}(-1.13), true) + @test rdiv_with_overflow(FD{Int16,2}(165), FD{Int16,2}(0.5)) == (FD{Int16,2}(-325.36), true) + @test rdiv_with_overflow(FD{Int16,2}(-165), FD{Int16,2}(0.5)) == (FD{Int16,2}(325.36), true) + @test rdiv_with_overflow(typemin(FD{Int64,8}), Int32(-1)) == (typemin(FD{Int64,8}), true) + @test rdiv_with_overflow(typemin(FD{Int64,0}), FD{Int64,0}(-1)) == (typemin(FD{Int64,0}), true) + @test rdiv_with_overflow(typemin(FD{Int8,2}), FD{Int8,2}(-1)) == (typemin(FD{Int8,2}), true) + @test rdiv_with_overflow(typemin(FD{Int8,2}), FD{Int8,2}(-0.01)) == (FD{Int8,2}(0), true) + + @test fld_with_overflow(FD{Int8,2}(-1), FD{Int8,2}(0.9)) == (FD{Int8,2}(0.56), true) + @test fld_with_overflow(typemin(FD{Int64,0}), FD{Int64,0}(-1)) == (typemin(FD{Int64,0}), true) + @test fld_with_overflow(FD{Int8,1}(7), FD{Int8,1}(0.5)) == (FD{Int8,1}(-11.6), true) + @test FixedPointDecimals.fld_with_overflow(typemin(FD{Int8,2}), FD{Int8,2}(-0.01)) == (typemin(FD{Int8,2}), true) + @testset "with_overflow math corner cases" begin @testset for I in (Int128, UInt128, Int8, UInt8), f in (0,2) T = FD{I, f} @@ -823,7 +838,21 @@ end issigned(I) && @test_throws DivideError div_with_overflow(typemax(T), T(0)) issigned(I) && @test_throws DivideError div_with_overflow(typemin(T), T(0)) issigned(I) && @test div_with_overflow(typemin(T), -eps(T))[2] + + @test fld_with_overflow(typemax(T), eps(T))[2] + issigned(I) && @test fld_with_overflow(typemin(T), eps(T))[2] + issigned(I) && @test fld_with_overflow(typemax(T), -eps(T))[2] end + + @test_throws DivideError rdiv_with_overflow(typemax(T), T(0)) + @test_throws DivideError rdiv_with_overflow(typemin(T), T(0)) + @test_throws DivideError rdiv_with_overflow(eps(T), T(0)) + @test_throws DivideError rdiv_with_overflow(-eps(T), T(0)) + + @test_throws DivideError fld_with_overflow(typemax(T), T(0)) + @test_throws DivideError fld_with_overflow(typemin(T), T(0)) + @test_throws DivideError fld_with_overflow(eps(T), T(0)) + @test_throws DivideError fld_with_overflow(-eps(T), T(0)) end end @@ -848,6 +877,17 @@ end @test div_with_overflow(FD{Int128,14}(10), FD{Int128,14}(20.1)) == (FD{Int128,14}(0), false) @test div_with_overflow(FD{Int128,30}(10.1), FD{Int128,30}(1)) == (FD{Int128,30}(10), false) @test div_with_overflow(typemin(FD{Int32,8}(1)), FD{Int32,8}(-1)) == (21, false) + + @test rdiv_with_overflow(Int8(1), FD{Int8,2}(0.8)) == (FD{Int8,2}(1.25), false) + @test rdiv_with_overflow(FD{Int64,8}(5), FD{Int64,8}(2)) == (FD{Int64,8}(2.5), false) + @test rdiv_with_overflow(FD{Int64,8}(5), FD{Int64,8}(0.5)) == (FD{Int64,8}(10), false) + @test rdiv_with_overflow(FD{Int128,0}(20000), Int32(5000)) == (FD{Int128,0}(4), false) + + @test fld_with_overflow(typemax(FD{Int128,38}), FD{Int128,38}(1)) == (FD{Int128,38}(1), false) + @test fld_with_overflow(FD{Int64,8}(20.5), FD{Int64,8}(2.1)) == (FD{Int64,8}(9), false) + @test fld_with_overflow(FD{Int8,0}(-5), FD{Int8,0}(-1)) == (FD{Int8,0}(5), false) + @test fld_with_overflow(FD{Int8,2}(0.99), FD{Int8,2}(0.5)) == (FD{Int8,2}(1), false) + @test fld_with_overflow(typemin(FD{Int8,2}), FD{Int8,2}(-1)) == (FD{Int8,2}(1), false) end end