diff --git a/src/FixedPointDecimals.jl b/src/FixedPointDecimals.jl index 28b8611..6286d03 100644 --- a/src/FixedPointDecimals.jl +++ b/src/FixedPointDecimals.jl @@ -397,6 +397,32 @@ end # --- Checked arithmetic --- +function Base.add_with_overflow(x::T, y::T) where {T<:FD} + z, b = Base.add_with_overflow(x.i, y.i) + return (reinterpret(T, z), b) +end +Base.Checked.add_with_overflow(x::FD, y::FD) = Base.Checked.add_with_overflow(promote(x, y)...) +Base.Checked.add_with_overflow(x::FD, y) = Base.Checked.add_with_overflow(promote(x, y)...) +Base.Checked.add_with_overflow(x, y::FD) = Base.Checked.add_with_overflow(promote(x, y)...) + +function Base.sub_with_overflow(x::T, y::T) where {T<:FD} + z, b = Base.sub_with_overflow(x.i, y.i) + return (reinterpret(T, z), b) +end +Base.Checked.sub_with_overflow(x::FD, y::FD) = Base.Checked.sub_with_overflow(promote(x, y)...) +Base.Checked.sub_with_overflow(x::FD, y) = Base.Checked.sub_with_overflow(promote(x, y)...) +Base.Checked.sub_with_overflow(x, y::FD) = Base.Checked.sub_with_overflow(promote(x, y)...) + +function Base.Checked.mul_with_overflow(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f} + powt = coefficient(FD{T, f}) + quotient, remainder = fldmodinline(_widemul(x.i, y.i), powt) + v = _round_to_nearest(quotient, remainder, powt) + return (reinterpret(FD{T,f}, Base.trunc_int(T, v)), v < typemin(T) || v > typemax(T)) +end +Base.Checked.mul_with_overflow(x::FD, y::FD) = Base.Checked.mul_with_overflow(promote(x, y)...) +Base.Checked.mul_with_overflow(x::FD, y) = Base.Checked.mul_with_overflow(promote(x, y)...) +Base.Checked.mul_with_overflow(x, y::FD) = Base.Checked.mul_with_overflow(promote(x, y)...) + 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)...) @@ -424,21 +450,19 @@ Base.checked_mod(x::FD, y) = Base.checked_mod(promote(x, y)...) Base.checked_mod(x, y::FD) = Base.checked_mod(promote(x, y)...) function Base.checked_add(x::T, y::T) where {T<:FD} - z, b = Base.add_with_overflow(x.i, y.i) + z, b = Base.Checked.add_with_overflow(x, y) b && Base.Checked.throw_overflowerr_binaryop(:+, x, y) - return reinterpret(T, z) + return z end function Base.checked_sub(x::T, y::T) where {T<:FD} - z, b = Base.sub_with_overflow(x.i, y.i) + z, b = Base.Checked.sub_with_overflow(x, y) b && Base.Checked.throw_overflowerr_binaryop(:-, x, y) - return reinterpret(T, z) + return z end function Base.checked_mul(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f} - powt = coefficient(FD{T, f}) - quotient, remainder = fldmodinline(_widemul(x.i, y.i), powt) - v = _round_to_nearest(quotient, remainder, powt) - typemin(T) <= v <= typemax(T) || Base.Checked.throw_overflowerr_binaryop(:*, x, y) - return reinterpret(FD{T, f}, T(v)) + z, b = Base.Checked.mul_with_overflow(x, y) + b && Base.Checked.throw_overflowerr_binaryop(:*, x, y) + return z end # Checked division functions for divfn in [:div, :fld, :cld] diff --git a/test/FixedDecimal.jl b/test/FixedDecimal.jl index 18d87d7..a4520d6 100644 --- a/test/FixedDecimal.jl +++ b/test/FixedDecimal.jl @@ -776,6 +776,82 @@ end @test FD{Int8,1}(2) / Int8(20) == FD{Int8,1}(0.1) end + @testset "limits: with_overflow math" begin + # 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), 1) == (FD{Int8,2}(-0.56), true) + @test Base.Checked.add_with_overflow(FD{Int8,2}(1), FD{Int8,2}(0.4)) == (FD{Int8,2}(-1.16), true) + + @test Base.Checked.sub_with_overflow(FD{Int8,2}(1), FD{Int8,2}(-1)) == (FD{Int8,2}(-0.56), true) + @test Base.Checked.sub_with_overflow(1, FD{Int8,2}(-1)) == (FD{Int8,2}(-0.56), true) + @test Base.Checked.sub_with_overflow(FD{Int8,2}(-1), FD{Int8,2}(0.4)) == (FD{Int8,2}(1.16), true) + + @test Base.Checked.mul_with_overflow(FD{Int8,2}(1.2), FD{Int8,2}(1.2)) == (FD{Int8,2}(-1.12), true) + @test Base.Checked.mul_with_overflow(FD{Int8,1}(12), 2) == (FD{Int8,1}(-1.6), true) + @test Base.Checked.mul_with_overflow(FD{Int8,0}(120), 2) == (FD{Int8,0}(-16), true) + @test Base.Checked.mul_with_overflow(120, FD{Int8,0}(2)) == (FD{Int8,0}(-16), true) + + @testset "with_overflow math corner cases" begin + @testset for I in (Int128, UInt128, Int8, UInt8), f in (0,2) + T = FD{I, f} + issigned(I) = signed(I) === I + + @test Base.Checked.add_with_overflow(typemax(T), eps(T)) == (typemax(T) + eps(T), true) + issigned(I) && @test Base.Checked.add_with_overflow(typemin(T), -eps(T)) == (typemin(T) + -eps(T), true) + @test Base.Checked.add_with_overflow(typemax(T), 1) == (typemax(T) + 1, true) + @test Base.Checked.add_with_overflow(1, typemax(T)) == (typemax(T) + 1, true) + + @test Base.Checked.sub_with_overflow(typemin(T), eps(T)) == (typemin(T) - eps(T), true) + issigned(I) && @test Base.Checked.sub_with_overflow(typemax(T), -eps(T)) == (typemax(T) - -eps(T), true) + @test Base.Checked.sub_with_overflow(typemin(T), 1) == (typemin(T) - 1, true) + if issigned(I) && 2.0 <= typemax(T) + @test Base.Checked.sub_with_overflow(-2, typemax(T)) == (-2 -typemax(T), true) + end + + @test Base.Checked.mul_with_overflow(typemax(T), typemax(T)) == (typemax(T) * typemax(T), true) + issigned(I) && @test Base.Checked.mul_with_overflow(typemin(T), typemax(T)) == (typemin(T) * typemax(T), true) + if 2.0 <= typemax(T) + @test Base.Checked.mul_with_overflow(typemax(T), 2) == (typemax(T) * 2, true) + @test Base.Checked.mul_with_overflow(2, typemax(T)) == (2 * typemax(T), true) + issigned(I) && @test Base.Checked.mul_with_overflow(typemin(T), 2) == (typemin(T) * 2, true) + issigned(I) && @test Base.Checked.mul_with_overflow(2, typemin(T)) == (2 * typemin(T), true) + end + end + end + + @testset "with_overflow math promotions" begin + x = FD{Int8,1}(1) + y = FD{Int64,1}(2) + @testset for op in ( + Base.Checked.add_with_overflow, Base.Checked.sub_with_overflow, + Base.Checked.mul_with_overflow, + ) + @test op(x, y) === op(FD{Int64,1}(1), y) + @test op(y, x) === op(y, FD{Int64,1}(1)) + + @test op(x, 2) === op(x, FD{Int8,1}(2)) + @test op(2, x) === op(FD{Int8,1}(2), x) + end + end + + @testset "non-overflowing with_overflow math" begin + @test Base.Checked.add_with_overflow(1, FD{Int8,1}(1.1)) == (FD{Int8,1}(2.1), false) + @test Base.Checked.add_with_overflow(FD{Int8,1}(1.1), 1) == (FD{Int8,1}(2.1), false) + @test Base.Checked.add_with_overflow(FD{Int64,8}(30.123), FD{Int64,8}(30)) == (FD{Int64,8}(60.123), false) + @test Base.Checked.add_with_overflow(FD{Int64,8}(30.123), FD{Int64,5}(30)) == (FD{Int64,8}(60.123), false) + + @test Base.Checked.sub_with_overflow(3, FD{Int16,2}(2.5)) == (FD{Int16,1}(0.5), false) + @test Base.Checked.sub_with_overflow(FD{Int16,2}(2.5), 3) == (FD{Int16,1}(-0.5), false) + @test Base.Checked.sub_with_overflow(FD{Int32,4}(10.11), FD{Int32,4}(2)) == (FD{Int32,4}(8.11), false) + @test Base.Checked.sub_with_overflow(FD{Int32,4}(10.11), FD{Int32,3}(2)) == (FD{Int32,4}(8.11), false) + + @test Base.Checked.mul_with_overflow(4, FD{Int64,6}(2.22)) == (FD{Int64,6}(8.88), false) + @test Base.Checked.mul_with_overflow(FD{Int64,6}(2.22), 4) == (FD{Int64,6}(8.88), false) + @test Base.Checked.mul_with_overflow(FD{Int128,14}(10), FD{Int128,14}(20.1)) == (FD{Int128,14}(201), false) + @test Base.Checked.mul_with_overflow(FD{Int128,30}(10.11), FD{Int128,0}(1)) == (FD{Int128,30}(10.11), false) + end + end + @testset "limits: overflow checked math" begin # Easy to reason about cases of overflow: @test_throws OverflowError Base.checked_add(FD{Int8,2}(1), FD{Int8,2}(1))