Skip to content

Commit 5f0c0b6

Browse files
committed
Add round_with_overflow, ceil_with_overflow, floor_with_overflow.
1 parent 3ba8bde commit 5f0c0b6

File tree

2 files changed

+274
-0
lines changed

2 files changed

+274
-0
lines changed

src/FixedPointDecimals.jl

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,67 @@ function rdiv_with_overflow(x::FD{T, f}, y::Integer) where {T<:Integer, f}
526526
return (reinterpret(FD{T, f}, v), false)
527527
end
528528

529+
# Does not exist in Base.Checked, so just exists in this package.
530+
@doc """
531+
FixedPointDecimals.ceil_with_overflow(x::FD)::Tuple{FD,Bool}
532+
533+
Calculates the nearest integral value of the same type as x that is greater than or equal
534+
to x, returning it and a boolean indicating whether overflow has occurred.
535+
536+
The overflow protection may impose a perceptible performance penalty.
537+
"""
538+
function ceil_with_overflow(x::FD{T,f}) where {T<:Integer,f}
539+
powt = coefficient(FD{T, f})
540+
quotient, remainder = fldmodinline(x.i, powt)
541+
return if remainder > 0
542+
# Could overflow when powt is 1 (f is 0) and x/x.i is typemax.
543+
v, add_overflowed = Base.Checked.add_with_overflow(quotient, one(quotient))
544+
# Could overflow when x is close to typemax (max quotient) independent of f.
545+
backing, mul_overflowed = Base.Checked.mul_with_overflow(v, powt)
546+
(reinterpret(FD{T, f}, backing), add_overflowed || mul_overflowed)
547+
else
548+
(FD{T, f}(quotient), false)
549+
end
550+
end
551+
552+
# Does not exist in Base.Checked, so just exists in this package.
553+
@doc """
554+
FixedPointDecimals.floor_with_overflow(x::FD)::Tuple{FD,Bool}
555+
556+
Calculates the nearest integral value of the same type as x that is less than or equal
557+
to x, returning it and a boolean indicating whether overflow has occurred.
558+
559+
The overflow protection may impose a perceptible performance penalty.
560+
"""
561+
function floor_with_overflow(x::FD{T, f}) where {T, f}
562+
powt = coefficient(FD{T, f})
563+
# Won't underflow, powt is an integer.
564+
quotient = fld(x.i, powt)
565+
# When we convert it back to the backing format it might though. Occurs when
566+
# the integer part of x is at its maximum.
567+
backing, overflowed = Base.Checked.mul_with_overflow(quotient, powt)
568+
return (reinterpret(FD{T, f}, backing), overflowed)
569+
end
570+
571+
round_with_overflow(fd::FD, ::RoundingMode{:Up}) = ceil_with_overflow(fd)
572+
round_with_overflow(fd::FD, ::RoundingMode{:Down}) = floor_with_overflow(fd)
573+
# trunc cannot overflow.
574+
round_with_overflow(fd::FD, ::RoundingMode{:ToZero}) = (trunc(fd), false)
575+
function round_with_overflow(
576+
x::FD{T, f},
577+
m::Union{
578+
RoundingMode{:Nearest},
579+
RoundingMode{:NearestTiesUp},
580+
RoundingMode{:NearestTiesAway}
581+
}=RoundNearest,
582+
) where {T, f}
583+
powt = coefficient(FD{T, f})
584+
quotient, remainder = fldmodinline(x.i, powt)
585+
v = _round_to_nearest(quotient, remainder, powt, m)
586+
backing, overflowed = Base.Checked.mul_with_overflow(v, powt)
587+
(reinterpret(FD{T, f}, backing), overflowed)
588+
end
589+
529590
Base.checked_add(x::FD, y::FD) = Base.checked_add(promote(x, y)...)
530591
Base.checked_sub(x::FD, y::FD) = Base.checked_sub(promote(x, y)...)
531592
Base.checked_mul(x::FD, y::FD) = Base.checked_mul(promote(x, y)...)

test/FixedDecimal.jl

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,6 +1268,120 @@ end
12681268
end
12691269
end
12701270

1271+
@testset "round_with_overflow" begin
1272+
using FixedPointDecimals: round_with_overflow
1273+
1274+
# Is alias for `ceil`.
1275+
@testset "up" begin
1276+
@test round_with_overflow(FD2(-0.51), RoundUp) === (FD2(0), false)
1277+
@test round_with_overflow(FD2(-0.50), RoundUp) === (FD2(0), false)
1278+
@test round_with_overflow(FD2(-0.49), RoundUp) === (FD2(0), false)
1279+
@test round_with_overflow(FD2(0.50), RoundUp) === (FD2(1), false)
1280+
@test round_with_overflow(FD2(0.51), RoundUp) === (FD2(1), false)
1281+
@test round_with_overflow(FD2(1.50), RoundUp) === (FD2(2), false)
1282+
@test round_with_overflow(typemin(FD2), RoundUp) ===
1283+
(parse(FD2, "-92233720368547758"), false)
1284+
1285+
@testset "overflowing" begin
1286+
@test round_with_overflow(typemax(FD2), RoundUp) ===
1287+
(parse(FD2, "-92233720368547757.16"), true)
1288+
@test round_with_overflow(parse(FD2, "92233720368547758.01"), RoundUp) ===
1289+
(parse(FD2, "-92233720368547757.16"), true)
1290+
end
1291+
end
1292+
1293+
# Is alias for `floor`.
1294+
@testset "down" begin
1295+
@test round_with_overflow(FD2(-0.51), RoundDown) === (FD2(-1), false)
1296+
@test round_with_overflow(FD2(-0.50), RoundDown) === (FD2(-1), false)
1297+
@test round_with_overflow(FD2(-0.49), RoundDown) === (FD2(-1), false)
1298+
@test round_with_overflow(FD2(0.50), RoundDown) === (FD2(0), false)
1299+
@test round_with_overflow(FD2(0.51), RoundDown) === (FD2(0), false)
1300+
@test round_with_overflow(FD2(1.50), RoundDown) === (FD2(1), false)
1301+
@test round_with_overflow(typemax(FD2), RoundDown) ===
1302+
(parse(FD2, "92233720368547758"), false)
1303+
1304+
@testset "overflowing" begin
1305+
@test round_with_overflow(typemin(FD2), RoundDown) ===
1306+
(parse(FD2, "92233720368547757.16"), true)
1307+
@test round_with_overflow(parse(FD2, "-92233720368547758.01"), RoundDown) ===
1308+
(parse(FD2, "92233720368547757.16"), true)
1309+
end
1310+
end
1311+
1312+
# Is alias for `trunc`.
1313+
@testset "to zero" begin
1314+
@test round_with_overflow(FD2(-0.51), RoundToZero) === (FD2(0), false)
1315+
@test round_with_overflow(FD2(-0.50), RoundToZero) === (FD2(0), false)
1316+
@test round_with_overflow(FD2(-0.49), RoundToZero) === (FD2(0), false)
1317+
@test round_with_overflow(FD2(0.50), RoundToZero) === (FD2(0), false)
1318+
@test round_with_overflow(FD2(0.51), RoundToZero) === (FD2(0), false)
1319+
@test round_with_overflow(FD2(1.50), RoundToZero) === (FD2(1), false)
1320+
1321+
@test round_with_overflow(typemin(FD2), RoundToZero) ===
1322+
(parse(FD2, "-92233720368547758"), false)
1323+
@test round_with_overflow(typemax(FD2), RoundToZero) ===
1324+
(parse(FD2, "92233720368547758"), false)
1325+
1326+
# Cannot overflow.
1327+
end
1328+
1329+
@testset "tie away" begin
1330+
@test round_with_overflow(FD2(-0.51), RoundNearestTiesAway) === (FD2(-1), false)
1331+
@test round_with_overflow(FD2(-0.50), RoundNearestTiesAway) === (FD2(-1), false)
1332+
@test round_with_overflow(FD2(-0.49), RoundNearestTiesAway) === (FD2(0), false)
1333+
@test round_with_overflow(FD2(0.50), RoundNearestTiesAway) === (FD2(1), false)
1334+
@test round_with_overflow(FD2(0.51), RoundNearestTiesAway) === (FD2(1), false)
1335+
@test round_with_overflow(FD2(1.50), RoundNearestTiesAway) === (FD2(2), false)
1336+
1337+
@test round_with_overflow(typemin(FD2), RoundNearestTiesAway) ===
1338+
(parse(FD2, "-92233720368547758"), false)
1339+
@test round_with_overflow(typemax(FD2), RoundNearestTiesAway) ===
1340+
(parse(FD2, "92233720368547758"), false)
1341+
1342+
@testset "overflowing" begin
1343+
# For max, FD2 has fractional .07 so use FD3 which has .807.
1344+
@test round_with_overflow(typemin(FD3), RoundNearestTiesAway) ===
1345+
(parse(FD3, "9223372036854775.616"), true)
1346+
@test round_with_overflow(typemax(FD3), RoundNearestTiesAway) ===
1347+
(parse(FD3, "-9223372036854775.616"), true)
1348+
1349+
@test round_with_overflow(parse(FD3, "9223372036854775.5"), RoundNearestTiesAway) ===
1350+
(parse(FD3, "-9223372036854775.616"), true)
1351+
@test round_with_overflow(parse(FD3, "-9223372036854775.5"), RoundNearestTiesAway) ===
1352+
(parse(FD3, "9223372036854775.616"), true)
1353+
end
1354+
end
1355+
1356+
@testset "tie up" begin
1357+
@test round_with_overflow(FD2(-0.51), RoundNearestTiesUp) === (FD2(-1), false)
1358+
@test round_with_overflow(FD2(-0.50), RoundNearestTiesUp) === (FD2(0), false)
1359+
@test round_with_overflow(FD2(-0.49), RoundNearestTiesUp) === (FD2(0), false)
1360+
@test round_with_overflow(FD2(0.50), RoundNearestTiesUp) === (FD2(1), false)
1361+
@test round_with_overflow(FD2(0.51), RoundNearestTiesUp) === (FD2(1), false)
1362+
@test round_with_overflow(FD2(1.50), RoundNearestTiesUp) === (FD2(2), false)
1363+
1364+
@test round_with_overflow(typemin(FD2), RoundNearestTiesUp) ===
1365+
(parse(FD2, "-92233720368547758"), false)
1366+
@test round_with_overflow(typemax(FD2), RoundNearestTiesUp) ===
1367+
(parse(FD2, "92233720368547758"), false)
1368+
1369+
# For max, FD2 has fractional .07 so use FD3 which has .807.
1370+
@test round_with_overflow(parse(FD3, "-9223372036854775.5"), RoundNearestTiesUp) ===
1371+
(FD3(-9223372036854775), false)
1372+
1373+
@testset "overflowing" begin
1374+
@test round_with_overflow(typemin(FD3), RoundNearestTiesUp) ===
1375+
(parse(FD3, "9223372036854775.616"), true)
1376+
@test round_with_overflow(typemax(FD3), RoundNearestTiesUp) ===
1377+
(parse(FD3, "-9223372036854775.616"), true)
1378+
1379+
@test round_with_overflow(parse(FD3, "9223372036854775.5"), RoundNearestTiesUp) ===
1380+
(parse(FD3, "-9223372036854775.616"), true)
1381+
end
1382+
end
1383+
end
1384+
12711385
@testset "trunc" begin
12721386
@test trunc(Int, FD2(0.99)) === 0
12731387
@test trunc(Int, FD2(-0.99)) === 0
@@ -1416,6 +1530,105 @@ epsi(::Type{T}) where T = eps(T)
14161530
end
14171531
end
14181532

1533+
@testset "floor_with_overflow" begin
1534+
using FixedPointDecimals: floor_with_overflow
1535+
1536+
@testset "non-overflowing" begin
1537+
@test floor_with_overflow(FD{Int8,2}(1.02)) == (FD{Int8,2}(1), false)
1538+
@test floor_with_overflow(FD{Int8,2}(-0.02)) == (FD{Int8,2}(-1), false)
1539+
@test floor_with_overflow(FD{Int8,2}(-1)) == (FD{Int8,2}(-1), false)
1540+
1541+
@test floor_with_overflow(FD{Int16,1}(5.2)) == (FD{Int16,1}(5), false)
1542+
@test floor_with_overflow(FD{Int16,1}(-5.2)) == (FD{Int16,1}(-6), false)
1543+
1544+
@test floor_with_overflow(typemax(FD{Int32,0})) == (typemax(FD{Int32,0}), false)
1545+
@test floor_with_overflow(typemin(FD{Int32,0})) == (typemin(FD{Int32,0}), false)
1546+
1547+
@test floor_with_overflow(FD{Int64,8}(40.054672)) == (FD{Int64,8}(40), false)
1548+
@test floor_with_overflow(FD{Int64,8}(-40.054672)) == (FD{Int64,8}(-41), false)
1549+
@test floor_with_overflow(FD{Int64,8}(-92233720368)) ==
1550+
(FD{Int64,8}(-92233720368), false)
1551+
1552+
@test floor_with_overflow(typemax(FD{Int128,18})) ==
1553+
(FD{Int128,18}(170141183460469231731), false)
1554+
@test floor_with_overflow(FD{Int128,18}(-400.0546798232)) ==
1555+
(FD{Int128,18}(-401), false)
1556+
end
1557+
1558+
@testset "overflowing" begin
1559+
@test floor_with_overflow(typemin(FD{Int8,2})) == (FD{Int8,2}(0.56), true)
1560+
@test floor_with_overflow(FD{Int8,2}(-1.02)) == (FD{Int8,2}(0.56), true)
1561+
1562+
@test floor_with_overflow(typemin(FD{Int16,3})) == (FD{Int16,3}(32.536), true)
1563+
@test floor_with_overflow(FD{Int16,3}(-32.111)) == (FD{Int16,3}(32.536), true)
1564+
1565+
@test floor_with_overflow(typemin(FD{Int32,1})) == (FD{Int32,1}(214748364.6), true)
1566+
@test floor_with_overflow(FD{Int32,1}(-214748364.7)) ==
1567+
(FD{Int32,1}(214748364.6), true)
1568+
1569+
@test floor_with_overflow(typemin(FD{Int64,8})) ==
1570+
(parse(FD{Int64,8}, "92233720368.09551616"), true)
1571+
@test floor_with_overflow(FD{Int64,8}(-92233720368.5)) ==
1572+
(parse(FD{Int64,8}, "92233720368.09551616"), true)
1573+
1574+
@test floor_with_overflow(typemin(FD{Int128,2})) ==
1575+
(parse(FD{Int128,2}, "1701411834604692317316873037158841056.56"), true)
1576+
@test floor_with_overflow(parse(FD{Int128,2}, "-1701411834604692317316873037158841057.27")) ==
1577+
(parse(FD{Int128,2}, "1701411834604692317316873037158841056.56"), true)
1578+
end
1579+
end
1580+
1581+
@testset "ceil_with_overflow" begin
1582+
using FixedPointDecimals: ceil_with_overflow
1583+
1584+
@testset "non-overflowing" begin
1585+
@test ceil_with_overflow(FD{Int8,2}(-1.02)) == (FD{Int8,2}(-1), false)
1586+
@test ceil_with_overflow(FD{Int8,2}(-0.02)) == (FD{Int8,2}(0), false)
1587+
@test ceil_with_overflow(FD{Int8,2}(0.49)) == (FD{Int8,2}(1), false)
1588+
@test ceil_with_overflow(FD{Int8,2}(1)) == (FD{Int8,2}(1), false)
1589+
1590+
@test ceil_with_overflow(FD{Int16,1}(5.2)) == (FD{Int16,1}(6), false)
1591+
@test ceil_with_overflow(FD{Int16,1}(-5.2)) == (FD{Int16,1}(-5), false)
1592+
1593+
@test ceil_with_overflow(typemax(FD{Int32,0})) == (typemax(FD{Int32,0}), false)
1594+
@test ceil_with_overflow(typemin(FD{Int32,0})) == (typemin(FD{Int32,0}), false)
1595+
1596+
@test ceil_with_overflow(FD{Int64,8}(40.054672)) == (FD{Int64,8}(41), false)
1597+
@test ceil_with_overflow(FD{Int64,8}(-40.054672)) == (FD{Int64,8}(-40), false)
1598+
@test ceil_with_overflow(FD{Int64,8}(-92233720368)) ==
1599+
(FD{Int64,8}(-92233720368), false)
1600+
@test ceil_with_overflow(FD{Int64,8}(92233720368)) ==
1601+
(FD{Int64,8}(92233720368), false)
1602+
1603+
@test ceil_with_overflow(typemin(FD{Int128,18})) ==
1604+
(FD{Int128,18}(-170141183460469231731), false)
1605+
@test ceil_with_overflow(FD{Int128,18}(-400.0546798232)) ==
1606+
(FD{Int128,18}(-400), false)
1607+
end
1608+
1609+
@testset "overflowing" begin
1610+
@test ceil_with_overflow(typemax(FD{Int8,2})) == (FD{Int8,2}(-0.56), true)
1611+
@test ceil_with_overflow(FD{Int8,2}(1.02)) == (FD{Int8,2}(-0.56), true)
1612+
1613+
@test ceil_with_overflow(typemax(FD{Int16,3})) == (FD{Int16,3}(-32.536), true)
1614+
@test ceil_with_overflow(FD{Int16,3}(32.111)) == (FD{Int16,3}(-32.536), true)
1615+
1616+
@test ceil_with_overflow(typemax(FD{Int32,1})) == (FD{Int32,1}(-214748364.6), true)
1617+
@test ceil_with_overflow(FD{Int32,1}(214748364.7)) ==
1618+
(FD{Int32,1}(-214748364.6), true)
1619+
1620+
@test ceil_with_overflow(typemax(FD{Int64,8})) ==
1621+
(parse(FD{Int64,8}, "-92233720368.09551616"), true)
1622+
@test ceil_with_overflow(FD{Int64,8}(92233720368.5)) ==
1623+
(parse(FD{Int64,8}, "-92233720368.09551616"), true)
1624+
1625+
@test ceil_with_overflow(typemax(FD{Int128,2})) ==
1626+
(parse(FD{Int128,2}, "-1701411834604692317316873037158841056.56"), true)
1627+
@test ceil_with_overflow(parse(FD{Int128,2}, "1701411834604692317316873037158841057.27")) ==
1628+
(parse(FD{Int128,2}, "-1701411834604692317316873037158841056.56"), true)
1629+
end
1630+
end
1631+
14191632
@testset "type stability" begin
14201633
# Test that basic operations are type stable for all the basic integer types.
14211634
fs = [0, 1, 2, 7, 16, 38] # To save time, don't test all possible combinations.

0 commit comments

Comments
 (0)