Skip to content

Commit 26c0a19

Browse files
authored
Add round_/ceil_/floor_with_overflow (#114)
* Add round_with_overflow, ceil_with_overflow, floor_with_overflow.
1 parent a123e68 commit 26c0a19

File tree

3 files changed

+278
-1
lines changed

3 files changed

+278
-1
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "FixedPointDecimals"
22
uuid = "fb4d412d-6eee-574d-9565-ede6634db7b0"
33
authors = ["Fengyang Wang <fengyang.wang.0@gmail.com>", "Curtis Vogt <curtis.vogt@gmail.com>"]
4-
version = "0.6.2"
4+
version = "0.6.3"
55

66
[deps]
77
BitIntegers = "c3b6d118-76ef-56ca-8cc7-ebb389d030a1"

src/FixedPointDecimals.jl

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

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

test/FixedDecimal.jl

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,6 +1272,123 @@ end
12721272
end
12731273
end
12741274

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

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

0 commit comments

Comments
 (0)