Skip to content

Commit

Permalink
Add round_/ceil_/floor_with_overflow (#114)
Browse files Browse the repository at this point in the history
* Add round_with_overflow, ceil_with_overflow, floor_with_overflow.
  • Loading branch information
mbarbar authored Feb 18, 2025
1 parent a123e68 commit 26c0a19
Show file tree
Hide file tree
Showing 3 changed files with 278 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "FixedPointDecimals"
uuid = "fb4d412d-6eee-574d-9565-ede6634db7b0"
authors = ["Fengyang Wang <[email protected]>", "Curtis Vogt <[email protected]>"]
version = "0.6.2"
version = "0.6.3"

[deps]
BitIntegers = "c3b6d118-76ef-56ca-8cc7-ebb389d030a1"
Expand Down
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

1 comment on commit 26c0a19

@NHDaly
Copy link
Member

@NHDaly NHDaly commented on 26c0a19 Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegsitrator register()

Release Notes:

New Library functions

  • FixedPointDecimals.floor_with_overflow(x::FD)::Tuple{FD,Bool}
    
  • FixedPointDecimals.ceil_with_overflow(x::FD)::Tuple{FD,Bool}
    
  • FixedPointDecimals.round_with_overflow(x::FD, [rounding_mode])::Tuple{FD,Bool}
    

Please sign in to comment.