diff --git a/Project.toml b/Project.toml index 8a3875b..29cc1ce 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.2" +version = "0.6.3" [deps] BitIntegers = "c3b6d118-76ef-56ca-8cc7-ebb389d030a1" diff --git a/src/FixedPointDecimals.jl b/src/FixedPointDecimals.jl index dcef0ed..912093b 100644 --- a/src/FixedPointDecimals.jl +++ b/src/FixedPointDecimals.jl @@ -522,6 +522,67 @@ function rdiv_with_overflow(x::FD{T, f}, y::Integer) where {T<:Integer, f} return (reinterpret(FD{T, f}, v), false) end +# Does not exist in Base.Checked, so just exists in this package. +@doc """ + FixedPointDecimals.ceil_with_overflow(x::FD)::Tuple{FD,Bool} + +Calculates the nearest integral value of the same type as x that is greater than or equal +to x, returning it and a boolean indicating whether overflow has occurred. + +The overflow protection may impose a perceptible performance penalty. +""" +function ceil_with_overflow(x::FD{T,f}) where {T<:Integer,f} + powt = coefficient(FD{T, f}) + quotient, remainder = fldmodinline(x.i, powt) + return if remainder > 0 + # Could overflow when powt is 1 (f is 0) and x/x.i is typemax. + v, add_overflowed = Base.Checked.add_with_overflow(quotient, one(quotient)) + # Could overflow when x is close to typemax (max quotient) independent of f. + backing, mul_overflowed = Base.Checked.mul_with_overflow(v, powt) + (reinterpret(FD{T, f}, backing), add_overflowed || mul_overflowed) + else + (FD{T, f}(quotient), false) + end +end + +# Does not exist in Base.Checked, so just exists in this package. +@doc """ + FixedPointDecimals.floor_with_overflow(x::FD)::Tuple{FD,Bool} + +Calculates the nearest integral value of the same type as x that is less than or equal +to x, returning it and a boolean indicating whether overflow has occurred. + +The overflow protection may impose a perceptible performance penalty. +""" +function floor_with_overflow(x::FD{T, f}) where {T, f} + powt = coefficient(FD{T, f}) + # Won't underflow, powt is an integer. + quotient = fld(x.i, powt) + # When we convert it back to the backing format it might though. Occurs when + # the integer part of x is at its maximum. + backing, overflowed = Base.Checked.mul_with_overflow(quotient, powt) + return (reinterpret(FD{T, f}, backing), overflowed) +end + +round_with_overflow(fd::FD, ::RoundingMode{:Up}) = ceil_with_overflow(fd) +round_with_overflow(fd::FD, ::RoundingMode{:Down}) = floor_with_overflow(fd) +# trunc cannot overflow. +round_with_overflow(fd::FD, ::RoundingMode{:ToZero}) = (trunc(fd), false) +function round_with_overflow( + x::FD{T, f}, + m::Union{ + RoundingMode{:Nearest}, + RoundingMode{:NearestTiesUp}, + RoundingMode{:NearestTiesAway} + }=RoundNearest, +) where {T, f} + powt = coefficient(FD{T, f}) + quotient, remainder = fldmodinline(x.i, powt) + v = _round_to_nearest(quotient, remainder, powt, m) + backing, overflowed = Base.Checked.mul_with_overflow(v, powt) + (reinterpret(FD{T, f}, backing), overflowed) +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)...) diff --git a/test/FixedDecimal.jl b/test/FixedDecimal.jl index 1a6bf25..6b092ff 100644 --- a/test/FixedDecimal.jl +++ b/test/FixedDecimal.jl @@ -1272,6 +1272,123 @@ end end end +@testset "round_with_overflow" begin + using FixedPointDecimals: round_with_overflow + + FD642 = FixedDecimal{Int64,2} + FD643 = FixedDecimal{Int64,3} + + # Is alias for `ceil`. + @testset "up" begin + @test round_with_overflow(FD642(-0.51), RoundUp) === (FD642(0), false) + @test round_with_overflow(FD642(-0.50), RoundUp) === (FD642(0), false) + @test round_with_overflow(FD642(-0.49), RoundUp) === (FD642(0), false) + @test round_with_overflow(FD642(0.50), RoundUp) === (FD642(1), false) + @test round_with_overflow(FD642(0.51), RoundUp) === (FD642(1), false) + @test round_with_overflow(FD642(1.50), RoundUp) === (FD642(2), false) + @test round_with_overflow(typemin(FD642), RoundUp) === + (parse(FD642, "-92233720368547758"), false) + + @testset "overflowing" begin + @test round_with_overflow(typemax(FD642), RoundUp) === + (parse(FD642, "-92233720368547757.16"), true) + @test round_with_overflow(parse(FD642, "92233720368547758.01"), RoundUp) === + (parse(FD642, "-92233720368547757.16"), true) + end + end + + # Is alias for `floor`. + @testset "down" begin + @test round_with_overflow(FD642(-0.51), RoundDown) === (FD642(-1), false) + @test round_with_overflow(FD642(-0.50), RoundDown) === (FD642(-1), false) + @test round_with_overflow(FD642(-0.49), RoundDown) === (FD642(-1), false) + @test round_with_overflow(FD642(0.50), RoundDown) === (FD642(0), false) + @test round_with_overflow(FD642(0.51), RoundDown) === (FD642(0), false) + @test round_with_overflow(FD642(1.50), RoundDown) === (FD642(1), false) + @test round_with_overflow(typemax(FD642), RoundDown) === + (parse(FD642, "92233720368547758"), false) + + @testset "overflowing" begin + @test round_with_overflow(typemin(FD642), RoundDown) === + (parse(FD642, "92233720368547757.16"), true) + @test round_with_overflow(parse(FD642, "-92233720368547758.01"), RoundDown) === + (parse(FD642, "92233720368547757.16"), true) + end + end + + # Is alias for `trunc`. + @testset "to zero" begin + @test round_with_overflow(FD642(-0.51), RoundToZero) === (FD642(0), false) + @test round_with_overflow(FD642(-0.50), RoundToZero) === (FD642(0), false) + @test round_with_overflow(FD642(-0.49), RoundToZero) === (FD642(0), false) + @test round_with_overflow(FD642(0.50), RoundToZero) === (FD642(0), false) + @test round_with_overflow(FD642(0.51), RoundToZero) === (FD642(0), false) + @test round_with_overflow(FD642(1.50), RoundToZero) === (FD642(1), false) + + @test round_with_overflow(typemin(FD642), RoundToZero) === + (parse(FD642, "-92233720368547758"), false) + @test round_with_overflow(typemax(FD642), RoundToZero) === + (parse(FD642, "92233720368547758"), false) + + # Cannot overflow. + end + + @testset "tie away" begin + @test round_with_overflow(FD642(-0.51), RoundNearestTiesAway) === (FD642(-1), false) + @test round_with_overflow(FD642(-0.50), RoundNearestTiesAway) === (FD642(-1), false) + @test round_with_overflow(FD642(-0.49), RoundNearestTiesAway) === (FD642(0), false) + @test round_with_overflow(FD642(0.50), RoundNearestTiesAway) === (FD642(1), false) + @test round_with_overflow(FD642(0.51), RoundNearestTiesAway) === (FD642(1), false) + @test round_with_overflow(FD642(1.50), RoundNearestTiesAway) === (FD642(2), false) + + @test round_with_overflow(typemin(FD642), RoundNearestTiesAway) === + (parse(FD642, "-92233720368547758"), false) + @test round_with_overflow(typemax(FD642), RoundNearestTiesAway) === + (parse(FD642, "92233720368547758"), false) + + @testset "overflowing" begin + # For max, FD642 has fractional .07 so use FD643 which has .807. + @test round_with_overflow(typemin(FD643), RoundNearestTiesAway) === + (parse(FD643, "9223372036854775.616"), true) + @test round_with_overflow(typemax(FD643), RoundNearestTiesAway) === + (parse(FD643, "-9223372036854775.616"), true) + + @test round_with_overflow(parse(FD643, "9223372036854775.5"), RoundNearestTiesAway) === + (parse(FD643, "-9223372036854775.616"), true) + @test round_with_overflow(parse(FD643, "-9223372036854775.5"), RoundNearestTiesAway) === + (parse(FD643, "9223372036854775.616"), true) + end + end + + @testset "tie up" begin + @test round_with_overflow(FD642(-0.51), RoundNearestTiesUp) === (FD642(-1), false) + @test round_with_overflow(FD642(-0.50), RoundNearestTiesUp) === (FD642(0), false) + @test round_with_overflow(FD642(-0.49), RoundNearestTiesUp) === (FD642(0), false) + @test round_with_overflow(FD642(0.50), RoundNearestTiesUp) === (FD642(1), false) + @test round_with_overflow(FD642(0.51), RoundNearestTiesUp) === (FD642(1), false) + @test round_with_overflow(FD642(1.50), RoundNearestTiesUp) === (FD642(2), false) + + @test round_with_overflow(typemin(FD642), RoundNearestTiesUp) === + (parse(FD642, "-92233720368547758"), false) + @test round_with_overflow(typemax(FD642), RoundNearestTiesUp) === + (parse(FD642, "92233720368547758"), false) + + # For max, FD642 has fractional .07 so use FD643 which has .807. + @test round_with_overflow(parse(FD643, "-9223372036854775.5"), RoundNearestTiesUp) === + (FD643(-9223372036854775), false) + + @testset "overflowing" begin + @test round_with_overflow(typemin(FD643), RoundNearestTiesUp) === + (parse(FD643, "9223372036854775.616"), true) + @test round_with_overflow(typemax(FD643), RoundNearestTiesUp) === + (parse(FD643, "-9223372036854775.616"), true) + + @test round_with_overflow(parse(FD643, "9223372036854775.5"), RoundNearestTiesUp) === + (parse(FD643, "-9223372036854775.616"), true) + end + end +end + @testset "trunc" begin @test trunc(Int, FD2(0.99)) === 0 @test trunc(Int, FD2(-0.99)) === 0 @@ -1420,6 +1537,105 @@ epsi(::Type{T}) where T = eps(T) end end +@testset "floor_with_overflow" begin + using FixedPointDecimals: floor_with_overflow + + @testset "non-overflowing" begin + @test floor_with_overflow(FD{Int8,2}(1.02)) == (FD{Int8,2}(1), false) + @test floor_with_overflow(FD{Int8,2}(-0.02)) == (FD{Int8,2}(-1), false) + @test floor_with_overflow(FD{Int8,2}(-1)) == (FD{Int8,2}(-1), false) + + @test floor_with_overflow(FD{Int16,1}(5.2)) == (FD{Int16,1}(5), false) + @test floor_with_overflow(FD{Int16,1}(-5.2)) == (FD{Int16,1}(-6), false) + + @test floor_with_overflow(typemax(FD{Int32,0})) == (typemax(FD{Int32,0}), false) + @test floor_with_overflow(typemin(FD{Int32,0})) == (typemin(FD{Int32,0}), false) + + @test floor_with_overflow(FD{Int64,8}(40.054672)) == (FD{Int64,8}(40), false) + @test floor_with_overflow(FD{Int64,8}(-40.054672)) == (FD{Int64,8}(-41), false) + @test floor_with_overflow(FD{Int64,8}(-92233720368)) == + (FD{Int64,8}(-92233720368), false) + + @test floor_with_overflow(typemax(FD{Int128,18})) == + (FD{Int128,18}(170141183460469231731), false) + @test floor_with_overflow(FD{Int128,18}(-400.0546798232)) == + (FD{Int128,18}(-401), false) + end + + @testset "overflowing" begin + @test floor_with_overflow(typemin(FD{Int8,2})) == (FD{Int8,2}(0.56), true) + @test floor_with_overflow(FD{Int8,2}(-1.02)) == (FD{Int8,2}(0.56), true) + + @test floor_with_overflow(typemin(FD{Int16,3})) == (FD{Int16,3}(32.536), true) + @test floor_with_overflow(FD{Int16,3}(-32.111)) == (FD{Int16,3}(32.536), true) + + @test floor_with_overflow(typemin(FD{Int32,1})) == (FD{Int32,1}(214748364.6), true) + @test floor_with_overflow(FD{Int32,1}(-214748364.7)) == + (FD{Int32,1}(214748364.6), true) + + @test floor_with_overflow(typemin(FD{Int64,8})) == + (parse(FD{Int64,8}, "92233720368.09551616"), true) + @test floor_with_overflow(FD{Int64,8}(-92233720368.5)) == + (parse(FD{Int64,8}, "92233720368.09551616"), true) + + @test floor_with_overflow(typemin(FD{Int128,2})) == + (parse(FD{Int128,2}, "1701411834604692317316873037158841056.56"), true) + @test floor_with_overflow(parse(FD{Int128,2}, "-1701411834604692317316873037158841057.27")) == + (parse(FD{Int128,2}, "1701411834604692317316873037158841056.56"), true) + end +end + +@testset "ceil_with_overflow" begin + using FixedPointDecimals: ceil_with_overflow + + @testset "non-overflowing" begin + @test ceil_with_overflow(FD{Int8,2}(-1.02)) == (FD{Int8,2}(-1), false) + @test ceil_with_overflow(FD{Int8,2}(-0.02)) == (FD{Int8,2}(0), false) + @test ceil_with_overflow(FD{Int8,2}(0.49)) == (FD{Int8,2}(1), false) + @test ceil_with_overflow(FD{Int8,2}(1)) == (FD{Int8,2}(1), false) + + @test ceil_with_overflow(FD{Int16,1}(5.2)) == (FD{Int16,1}(6), false) + @test ceil_with_overflow(FD{Int16,1}(-5.2)) == (FD{Int16,1}(-5), false) + + @test ceil_with_overflow(typemax(FD{Int32,0})) == (typemax(FD{Int32,0}), false) + @test ceil_with_overflow(typemin(FD{Int32,0})) == (typemin(FD{Int32,0}), false) + + @test ceil_with_overflow(FD{Int64,8}(40.054672)) == (FD{Int64,8}(41), false) + @test ceil_with_overflow(FD{Int64,8}(-40.054672)) == (FD{Int64,8}(-40), false) + @test ceil_with_overflow(FD{Int64,8}(-92233720368)) == + (FD{Int64,8}(-92233720368), false) + @test ceil_with_overflow(FD{Int64,8}(92233720368)) == + (FD{Int64,8}(92233720368), false) + + @test ceil_with_overflow(typemin(FD{Int128,18})) == + (FD{Int128,18}(-170141183460469231731), false) + @test ceil_with_overflow(FD{Int128,18}(-400.0546798232)) == + (FD{Int128,18}(-400), false) + end + + @testset "overflowing" begin + @test ceil_with_overflow(typemax(FD{Int8,2})) == (FD{Int8,2}(-0.56), true) + @test ceil_with_overflow(FD{Int8,2}(1.02)) == (FD{Int8,2}(-0.56), true) + + @test ceil_with_overflow(typemax(FD{Int16,3})) == (FD{Int16,3}(-32.536), true) + @test ceil_with_overflow(FD{Int16,3}(32.111)) == (FD{Int16,3}(-32.536), true) + + @test ceil_with_overflow(typemax(FD{Int32,1})) == (FD{Int32,1}(-214748364.6), true) + @test ceil_with_overflow(FD{Int32,1}(214748364.7)) == + (FD{Int32,1}(-214748364.6), true) + + @test ceil_with_overflow(typemax(FD{Int64,8})) == + (parse(FD{Int64,8}, "-92233720368.09551616"), true) + @test ceil_with_overflow(FD{Int64,8}(92233720368.5)) == + (parse(FD{Int64,8}, "-92233720368.09551616"), true) + + @test ceil_with_overflow(typemax(FD{Int128,2})) == + (parse(FD{Int128,2}, "-1701411834604692317316873037158841056.56"), true) + @test ceil_with_overflow(parse(FD{Int128,2}, "1701411834604692317316873037158841057.27")) == + (parse(FD{Int128,2}, "-1701411834604692317316873037158841056.56"), true) + end +end + @testset "type stability" begin # Test that basic operations are type stable for all the basic integer types. fs = [0, 1, 2, 7, 16, 38] # To save time, don't test all possible combinations.