Skip to content

Add round_/ceil_/floor_with_overflow #114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2e71f3e
Fix ambiguities.
mbarbar Jan 16, 2025
2297c6f
Add tests.
mbarbar Jan 16, 2025
48cc31a
Merge remote-tracking branch 'upstream/master'
mbarbar Jan 16, 2025
c7e0826
Comment
mbarbar Jan 16, 2025
112d5e0
Simplify logic.
mbarbar Jan 16, 2025
68e2935
Merge remote-tracking branch 'upstream/master'
mbarbar Jan 25, 2025
b43473b
Remove promoting with_overflow functions.
mbarbar Jan 25, 2025
48f1d8d
Update tests.
mbarbar Jan 25, 2025
d5ef6e6
Bump to 0.6
mbarbar Jan 28, 2025
5a0dfe5
Update Project.toml
NHDaly Jan 28, 2025
404161e
Merge branch 'master' into master
NHDaly Jan 28, 2025
1399e4f
Add rdiv_with_overflow and fld_with_overflow.
mbarbar Feb 6, 2025
564d841
Add more DIvideError tests.
mbarbar Feb 6, 2025
05381f9
Merge remote-tracking branch 'upstream/master'
mbarbar Feb 6, 2025
3ba8bde
Only on Signed.
mbarbar Feb 6, 2025
5f0c0b6
Add round_with_overflow, ceil_with_overflow, floor_with_overflow.
mbarbar Feb 12, 2025
a59ebdf
Add signed check
mbarbar Feb 14, 2025
d0cdb98
Remove comment
mbarbar Feb 14, 2025
e45f98e
Merge branch 'master' into master
mbarbar Feb 14, 2025
6976457
Address
mbarbar Feb 15, 2025
ecece4e
Merge remote-tracking branch 'origin/master'
mbarbar Feb 15, 2025
b2f0a82
Apply suggestions from code review
NHDaly Feb 17, 2025
933367b
bump to v0.6.2
NHDaly Feb 17, 2025
8f9c709
Merge remote-tracking branch 'origin/master' into mbar-round
mbarbar Feb 18, 2025
459f8a5
Merge remote-tracking branch 'upstream/master' into mbar-round
mbarbar Feb 18, 2025
d4a4243
Explicitly 64 bit
mbarbar Feb 18, 2025
cf0b5f6
Update Project.toml
NHDaly Feb 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/FixedPointDecimals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)...)
Expand Down
216 changes: 216 additions & 0 deletions test/FixedDecimal.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading