Skip to content

Commit 4cb2d5d

Browse files
committed
Make division operations always throw on overflow!
Also update the README
1 parent 3270ad5 commit 4cb2d5d

File tree

3 files changed

+54
-50
lines changed

3 files changed

+54
-50
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ FixedDecimal{Int64,1}(0.3)
4747

4848
### Arithmetic details: Overflow and checked math
4949

50-
By default, all arithmetic operations on FixedDecimals **will silently overflow**, following the standard behavior for bit integer types in Julia. For example:
50+
By default, all arithmetic operations on FixedDecimals, except division, **will silently overflow** following the standard behavior for bit integer types in Julia. For example:
5151
```julia
5252
julia> FixedDecimal{Int8,2}(1.0) + FixedDecimal{Int8,2}(1.0)
5353
FixedDecimal{Int8,2}(-0.56)
@@ -59,6 +59,8 @@ julia> abs(FixedDecimal{Int8,2}(-1.28)) # negative typemin wraps to typemin aga
5959
FixedDecimal{Int8,2}(-1.28)
6060
```
6161

62+
*Note that **division** on FixedDecimals will throw OverflowErrors on overflow, and will not wrap. This decision may be reevaluated in a future breaking version change release of FixedDecimals. Please keep this in mind.*
63+
6264
In most applications dealing with `FixedDecimals`, you will likely want to use the **checked arithmetic** operations instead. These operations will _throw an OverflowError_ on overflow or underflow, rather than silently wrapping. For example:
6365
```julia
6466
julia> Base.checked_mul(FixedDecimal{Int8,2}(1.2), FixedDecimal{Int8,2}(1.2))
@@ -71,14 +73,14 @@ julia> Base.checked_div(Int8(1), FixedDecimal{Int8,2}(0.5))
7173
ERROR: OverflowError: 1.00 ÷ 0.50 overflowed for type FixedDecimal{Int8, 2}
7274
```
7375

74-
**Checked division:** Note that `checked_div` performs truncating, integer division. Julia Base does not provide a function to perform checked decimal division, so we provide one in this package, `FixedPointDecimals.checked_decimal_division`.
76+
**Checked division:** Note that `checked_div` performs truncating, integer division. Julia Base does not provide a function to perform checked *decimal* division (`/`), so we provide one in this package, `FixedPointDecimals.checked_rdiv`. However, as noted above, the default division arithmetic operators will throw on overflow anyway.
7577

7678
Here are all the checked arithmetic operations supported by `FixedDecimal`s:
7779
- `Base.checked_add(x,y)`
7880
- `Base.checked_sub(x,y)`
7981
- `Base.checked_mul(x,y)`
8082
- `Base.checked_div(x,y)`
81-
- `FixedPointDecimals.checked_decimal_division(x,y)`
83+
- `FixedPointDecimals.checked_rdiv(x,y)`
8284
- `Base.checked_cld(x,y)`
8385
- `Base.checked_fld(x,y)`
8486
- `Base.checked_rem(x,y)`

src/FixedPointDecimals.jl

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -197,25 +197,9 @@ end
197197
Base.:*(x::Integer, y::FD{T, f}) where {T, f} = reinterpret(FD{T, f}, *(x, y.i))
198198
Base.:*(x::FD{T, f}, y::Integer) where {T, f} = reinterpret(FD{T, f}, *(x.i, y))
199199

200-
function Base.:/(x::FD{T, f}, y::FD{T, f}) where {T, f}
201-
powt = coefficient(FD{T, f})
202-
quotient, remainder = fldmod(widemul(x.i, powt), y.i)
203-
reinterpret(FD{T, f}, _round_to_nearest(quotient, remainder, y.i))
204-
end
205-
206-
# These functions allow us to perform division with integers outside of the range of the
207-
# FixedDecimal.
208-
function Base.:/(x::Integer, y::FD{T, f}) where {T, f}
209-
powt = coefficient(FD{T, f})
210-
powtsq = widemul(powt, powt)
211-
quotient, remainder = fldmod(widemul(x, powtsq), y.i)
212-
reinterpret(FD{T, f}, _round_to_nearest(quotient, remainder, y.i))
213-
end
214-
215-
function Base.:/(x::FD{T, f}, y::Integer) where {T, f}
216-
quotient, remainder = fldmod(x.i, y)
217-
reinterpret(FD{T, f}, _round_to_nearest(quotient, remainder, y))
218-
end
200+
Base.:/(x::FD, y::FD) = checked_rdiv(x, y)
201+
Base.:/(x::Integer, y::FD) = checked_rdiv(x, y)
202+
Base.:/(x::FD, y::Integer) = checked_rdiv(x, y)
219203

220204
# integerification
221205
Base.trunc(x::FD{T, f}) where {T, f} = FD{T, f}(div(x.i, coefficient(FD{T, f})))
@@ -366,17 +350,19 @@ for remfn in [:rem, :mod, :mod1, :min, :max]
366350
end
367351
# TODO: When we upgrade to a min julia version >=1.4 (i.e Julia 2.0), this block can be
368352
# dropped in favor of three-argument `div`, below.
369-
for divfn in [:div, :fld, :fld1, :cld]
370-
# div(x.i, y.i) eliminates the scaling coefficient, so we call the FD constructor.
371-
# We don't need any widening logic, since we won't be multiplying by the coefficient.
372-
#@eval Base.$divfn(x::T, y::T) where {T <: FD} = T($divfn(x.i, y.i))
373-
# @eval Base.$divfn(x::T, y::T) where {T <: FD} = $divfn(promote(x.i, y.i)...)
374-
# TODO(PR): I'm not sure about this one...
375-
# What should it *mean* for `typemax(FD) ÷ FD(0.5)` to overflow?
376-
@eval function Base.$divfn(x::T, y::T) where {T <: FD}
377-
C = coefficient(T)
378-
return reinterpret(T, C * $divfn(promote(x.i, y.i)...))
379-
end
353+
# The division functions all default to *throwing OverflowError* rather than
354+
# wrapping on integer overflow.
355+
# This decision may be changed in a future release of FixedPointDecimals.
356+
Base.div(x::FD, y::FD) = Base.checked_div(x, y)
357+
Base.fld(x::FD, y::FD) = Base.checked_fld(x, y)
358+
Base.cld(x::FD, y::FD) = Base.checked_cld(x, y)
359+
# There is not checked_fld1, so this is implemented here:
360+
function Base.fld1(x::FD{T,f}, y::FD{T,f}) where {T, f}
361+
C = coefficient(FD{T, f})
362+
# Note: fld1() will already throw for divide-by-zero and typemin(T) ÷ -1.
363+
v, b = Base.Checked.mul_with_overflow(C, fld1(x.i, y.i))
364+
b && _throw_overflowerr_op(:fld1, x, y)
365+
return reinterpret(FD{T, f}, v)
380366
end
381367
if VERSION >= v"1.4.0-"
382368
# div(x.i, y.i) eliminates the scaling coefficient, so we call the FD constructor.
@@ -480,8 +466,6 @@ See also:
480466
- `Base.checked_div` for truncating division.
481467
"""
482468
checked_rdiv(x::FD, y::FD) = checked_rdiv(promote(x, y)...)
483-
checked_rdiv(x, y::FD) = checked_rdiv(promote(x, y)...)
484-
checked_rdiv(x::FD, y) = checked_rdiv(promote(x, y)...)
485469

486470
function checked_rdiv(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f}
487471
powt = coefficient(FD{T, f})
@@ -491,6 +475,24 @@ function checked_rdiv(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f}
491475
return reinterpret(FD{T, f}, v)
492476
end
493477

478+
# These functions allow us to perform division with integers outside of the range of the
479+
# FixedDecimal.
480+
function checked_rdiv(x::Integer, y::FD{T, f}) where {T<:Integer, f}
481+
powt = coefficient(FD{T, f})
482+
powtsq = widemul(powt, powt)
483+
quotient, remainder = fldmod(widemul(x, powtsq), y.i)
484+
v = _round_to_nearest(quotient, remainder, y.i)
485+
typemin(T) <= v <= typemax(T) || Base.Checked.throw_overflowerr_binaryop(:/, x, y)
486+
reinterpret(FD{T, f}, v)
487+
end
488+
function checked_rdiv(x::FD{T, f}, y::Integer) where {T<:Integer, f}
489+
quotient, remainder = fldmod(x.i, y)
490+
v = _round_to_nearest(quotient, remainder, y)
491+
typemin(T) <= v <= typemax(T) || Base.Checked.throw_overflowerr_binaryop(:/, x, y)
492+
reinterpret(FD{T, f}, v)
493+
end
494+
495+
494496
# --------------------------
495497

496498
Base.convert(::Type{AbstractFloat}, x::FD) = convert(floattype(typeof(x)), x)

test/FixedDecimal.jl

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ end
539539

540540
# signed integers using two's complement have one additional negative value
541541
if x < 0 && x == typemin(x)
542-
@test x / -one(x) == x # -typemin(x) == typemin(x)
542+
@test_throws OverflowError x / -one(x)
543543
else
544544
@test x / -one(x) == -x
545545
end
@@ -835,30 +835,30 @@ end
835835

836836
@testset "division" begin
837837
# TODO(PR): Is this the expected value?
838-
@test typemax(T) / T(0.5) == FD2(-0.2)
839-
@test typemin(T) / T(0.5) == FD2(0)
838+
@test_throws OverflowError typemax(T) / T(0.5)
839+
@test_throws OverflowError typemin(T) / T(0.5)
840840
end
841841

842842
@testset "truncating division" begin
843843
# TODO(PR): Is this the expected value?
844-
@test typemax(T) ÷ T(0.5) == T(-0.6)
845-
@test typemin(T) ÷ T(0.5) == T(0.6)
846-
@test typemax(T) ÷ eps(T) == T(-1)
847-
@test typemin(T) ÷ eps(T) == T(0)
844+
@test_throws OverflowError typemax(T) ÷ T(0.5)
845+
@test_throws OverflowError typemin(T) ÷ T(0.5)
846+
@test_throws OverflowError typemax(T) ÷ eps(T)
847+
@test_throws OverflowError typemin(T) ÷ eps(T)
848848
end
849849

850850
@testset "fld / cld" begin
851851
# TODO(PR): Is this the expected value?
852-
@test fld(typemax(T), T(0.5)) == T(-0.6)
853-
@test fld(typemin(T), T(0.5)) == T(-0.4)
854-
@test fld(typemax(T), eps(T)) == T(-1)
855-
@test fld(typemin(T), eps(T)) == T(0)
852+
@test_throws OverflowError fld(typemax(T), T(0.5))
853+
@test_throws OverflowError fld(typemin(T), T(0.5))
854+
@test_throws OverflowError fld(typemax(T), eps(T))
855+
@test_throws OverflowError fld(typemin(T), eps(T))
856856

857857
# TODO(PR): Is this the expected value?
858-
@test cld(typemax(T), T(0.5)) == T(0.4)
859-
@test cld(typemin(T), T(0.5)) == T(0.6)
860-
@test cld(typemax(T), eps(T)) == T(-1)
861-
@test cld(typemin(T), eps(T)) == T(0)
858+
@test_throws OverflowError cld(typemax(T), T(0.5))
859+
@test_throws OverflowError cld(typemin(T), T(0.5))
860+
@test_throws OverflowError cld(typemax(T), eps(T))
861+
@test_throws OverflowError cld(typemin(T), eps(T))
862862
end
863863

864864
@testset "abs / neg" begin

0 commit comments

Comments
 (0)