Skip to content

Commit e769be4

Browse files
authored
Merge pull request #85 from JuliaMath/nhd-checked-math
Add checked math to FixedDecimals; default to overflow behavior
2 parents dfc11f0 + 73410da commit e769be4

File tree

5 files changed

+382
-32
lines changed

5 files changed

+382
-32
lines changed

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
version:
1616
- '1.6'
1717
- '1'
18-
# - 'nightly'
18+
- 'nightly'
1919
os:
2020
- ubuntu-latest
2121
- macOS-latest

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.4.4"
4+
version = "0.5.0"
55

66
[deps]
77
Parsers = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,64 @@ julia> 0.1 + 0.2
4444
julia> FixedDecimal{Int,1}(0.1) + FixedDecimal{Int,1}(0.2)
4545
FixedDecimal{Int64,1}(0.3)
4646
```
47+
48+
### Arithmetic details: Overflow and checked math
49+
50+
_NOTE: This section applies to FixedPointDecimals v0.5+._
51+
52+
By default, all arithmetic operations on FixedDecimals, except division, **will silently overflow** following the standard behavior for bit integer types in Julia. For example:
53+
```julia
54+
julia> FixedDecimal{Int8,2}(1.0) + FixedDecimal{Int8,2}(1.0)
55+
FixedDecimal{Int8,2}(-0.56)
56+
57+
julia> -FixedDecimal{Int8,2}(-1.28) # negative typemin wraps to typemin again
58+
FixedDecimal{Int8,2}(-1.28)
59+
60+
julia> abs(FixedDecimal{Int8,2}(-1.28)) # negative typemin wraps to typemin again
61+
FixedDecimal{Int8,2}(-1.28)
62+
```
63+
64+
*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.*
65+
66+
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:
67+
```julia
68+
julia> Base.checked_mul(FixedDecimal{Int8,2}(1.2), FixedDecimal{Int8,2}(1.2))
69+
ERROR: OverflowError: 1.20 * 1.20 overflowed for type FixedDecimal{Int8, 2}
70+
71+
julia> Base.checked_add(FixedDecimal{Int8,2}(1.2), 1)
72+
ERROR: OverflowError: 1.20 + 1.00 overflowed for type FixedDecimal{Int8, 2}
73+
74+
julia> Base.checked_div(Int8(1), FixedDecimal{Int8,2}(0.5))
75+
ERROR: OverflowError: 1.00 ÷ 0.50 overflowed for type FixedDecimal{Int8, 2}
76+
```
77+
78+
**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.
79+
80+
Here are all the checked arithmetic operations supported by `FixedDecimal`s:
81+
- `Base.checked_add(x,y)`
82+
- `Base.checked_sub(x,y)`
83+
- `Base.checked_mul(x,y)`
84+
- `Base.checked_div(x,y)`
85+
- `FixedPointDecimals.checked_rdiv(x,y)`
86+
- `Base.checked_cld(x,y)`
87+
- `Base.checked_fld(x,y)`
88+
- `Base.checked_rem(x,y)`
89+
- `Base.checked_mod(x,y)`
90+
- `Base.checked_neg(x)`
91+
- `Base.checked_abs(x)`
92+
93+
### Conversions, Promotions, and Inexact Errors.
94+
95+
Note that arithmetic operations will _promote_ all arguments to the same FixedDecimal type
96+
before performing the operation. If you are promoting a non-FixedDecimal _number_ to a FixedDecimal, there is always a chance that the Number will not fit in the FD type. In that case, the conversion will throw an exception. Here are some examples:
97+
```julia
98+
julia> FixedDecimal{Int8,2}(2) # 200 doesn't fit in Int8
99+
ERROR: InexactError: convert(FixedDecimal{Int8, 2}, 2)
100+
101+
julia> FixedDecimal{Int8,2}(1) + 2 # Same here: 2 is promoted to FD{Int8,2}(2)
102+
ERROR: InexactError: convert(FixedDecimal{Int8, 2}, 2)
103+
104+
julia> FixedDecimal{Int8,2}(1) + FixedDecimal{Int8,1}(2) # Promote to the higher-precision type again throws.
105+
ERROR: InexactError: convert(FixedDecimal{Int8, 2}, 2.0)
106+
```
107+

src/FixedPointDecimals.jl

Lines changed: 154 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ module FixedPointDecimals
2727

2828
export FixedDecimal, RoundThrows
2929

30+
# (Re)export checked_* arithmetic functions
31+
# - Defined in this package:
32+
export checked_rdiv
33+
# - Reexported from Base:
34+
export checked_abs, checked_add, checked_cld, checked_div, checked_fld,
35+
checked_mod, checked_mul, checked_neg, checked_rem, checked_sub
36+
3037
using Base: decompose, BitInteger
3138
import Parsers
3239

@@ -187,28 +194,12 @@ end
187194

188195
# these functions are needed to avoid InexactError when converting from the
189196
# integer type
190-
Base.:*(x::Integer, y::FD{T, f}) where {T, f} = reinterpret(FD{T, f}, T(x * y.i))
191-
Base.:*(x::FD{T, f}, y::Integer) where {T, f} = reinterpret(FD{T, f}, T(x.i * y))
197+
Base.:*(x::Integer, y::FD{T, f}) where {T, f} = reinterpret(FD{T, f}, x * y.i)
198+
Base.:*(x::FD{T, f}, y::Integer) where {T, f} = reinterpret(FD{T, f}, x.i * y)
192199

193-
function Base.:/(x::FD{T, f}, y::FD{T, f}) where {T, f}
194-
powt = coefficient(FD{T, f})
195-
quotient, remainder = fldmod(widemul(x.i, powt), y.i)
196-
reinterpret(FD{T, f}, T(_round_to_nearest(quotient, remainder, y.i)))
197-
end
198-
199-
# These functions allow us to perform division with integers outside of the range of the
200-
# FixedDecimal.
201-
function Base.:/(x::Integer, y::FD{T, f}) where {T, f}
202-
powt = coefficient(FD{T, f})
203-
powtsq = widemul(powt, powt)
204-
quotient, remainder = fldmod(widemul(x, powtsq), y.i)
205-
reinterpret(FD{T, f}, T(_round_to_nearest(quotient, remainder, y.i)))
206-
end
207-
208-
function Base.:/(x::FD{T, f}, y::Integer) where {T, f}
209-
quotient, remainder = fldmod(x.i, y)
210-
reinterpret(FD{T, f}, T(_round_to_nearest(quotient, remainder, y)))
211-
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)
212203

213204
# integerification
214205
Base.trunc(x::FD{T, f}) where {T, f} = FD{T, f}(div(x.i, coefficient(FD{T, f})))
@@ -359,17 +350,154 @@ for remfn in [:rem, :mod, :mod1, :min, :max]
359350
end
360351
# TODO: When we upgrade to a min julia version >=1.4 (i.e Julia 2.0), this block can be
361352
# dropped in favor of three-argument `div`, below.
362-
for divfn in [:div, :fld, :fld1, :cld]
363-
# div(x.i, y.i) eliminates the scaling coefficient, so we call the FD constructor.
364-
# We don't need any widening logic, since we won't be multiplying by the coefficient.
365-
@eval Base.$divfn(x::T, y::T) where {T <: FD} = T($divfn(x.i, y.i))
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 no 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)
366366
end
367367
if VERSION >= v"1.4.0-"
368368
# div(x.i, y.i) eliminates the scaling coefficient, so we call the FD constructor.
369369
# We don't need any widening logic, since we won't be multiplying by the coefficient.
370-
Base.div(x::T, y::T, r::RoundingMode) where {T <: FD} = T(div(x.i, y.i, r))
370+
@eval function Base.div(x::FD{T, f}, y::FD{T, f}, r::RoundingMode) where {T<:Integer, f}
371+
C = coefficient(FD{T, f})
372+
# Note: The div() will already throw for divide-by-zero and typemin(T) ÷ -1.
373+
v, b = Base.Checked.mul_with_overflow(C, div(x.i, y.i, r))
374+
b && _throw_overflowerr_op(:div, x, y)
375+
return reinterpret(FD{T, f}, v)
376+
end
377+
end
378+
379+
# --- Checked arithmetic ---
380+
381+
Base.checked_add(x::FD, y::FD) = Base.checked_add(promote(x, y)...)
382+
Base.checked_sub(x::FD, y::FD) = Base.checked_sub(promote(x, y)...)
383+
Base.checked_mul(x::FD, y::FD) = Base.checked_mul(promote(x, y)...)
384+
Base.checked_div(x::FD, y::FD) = Base.checked_div(promote(x, y)...)
385+
Base.checked_cld(x::FD, y::FD) = Base.checked_cld(promote(x, y)...)
386+
Base.checked_fld(x::FD, y::FD) = Base.checked_fld(promote(x, y)...)
387+
Base.checked_rem(x::FD, y::FD) = Base.checked_rem(promote(x, y)...)
388+
Base.checked_mod(x::FD, y::FD) = Base.checked_mod(promote(x, y)...)
389+
390+
Base.checked_add(x::FD, y) = Base.checked_add(promote(x, y)...)
391+
Base.checked_add(x, y::FD) = Base.checked_add(promote(x, y)...)
392+
Base.checked_sub(x::FD, y) = Base.checked_sub(promote(x, y)...)
393+
Base.checked_sub(x, y::FD) = Base.checked_sub(promote(x, y)...)
394+
Base.checked_mul(x::FD, y) = Base.checked_mul(promote(x, y)...)
395+
Base.checked_mul(x, y::FD) = Base.checked_mul(promote(x, y)...)
396+
Base.checked_div(x::FD, y) = Base.checked_div(promote(x, y)...)
397+
Base.checked_div(x, y::FD) = Base.checked_div(promote(x, y)...)
398+
Base.checked_cld(x::FD, y) = Base.checked_cld(promote(x, y)...)
399+
Base.checked_cld(x, y::FD) = Base.checked_cld(promote(x, y)...)
400+
Base.checked_fld(x::FD, y) = Base.checked_fld(promote(x, y)...)
401+
Base.checked_fld(x, y::FD) = Base.checked_fld(promote(x, y)...)
402+
Base.checked_rem(x::FD, y) = Base.checked_rem(promote(x, y)...)
403+
Base.checked_rem(x, y::FD) = Base.checked_rem(promote(x, y)...)
404+
Base.checked_mod(x::FD, y) = Base.checked_mod(promote(x, y)...)
405+
Base.checked_mod(x, y::FD) = Base.checked_mod(promote(x, y)...)
406+
407+
function Base.checked_add(x::T, y::T) where {T<:FD}
408+
z, b = Base.add_with_overflow(x.i, y.i)
409+
b && Base.Checked.throw_overflowerr_binaryop(:+, x, y)
410+
return reinterpret(T, z)
411+
end
412+
function Base.checked_sub(x::T, y::T) where {T<:FD}
413+
z, b = Base.sub_with_overflow(x.i, y.i)
414+
b && Base.Checked.throw_overflowerr_binaryop(:-, x, y)
415+
return reinterpret(T, z)
416+
end
417+
function Base.checked_mul(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f}
418+
powt = coefficient(FD{T, f})
419+
quotient, remainder = fldmodinline(widemul(x.i, y.i), powt)
420+
v = _round_to_nearest(quotient, remainder, powt)
421+
typemin(T) <= v <= typemax(T) || Base.Checked.throw_overflowerr_binaryop(:*, x, y)
422+
return reinterpret(FD{T, f}, T(v))
423+
end
424+
# Checked division functions
425+
for divfn in [:div, :fld, :cld]
426+
@eval function Base.$(Symbol("checked_$divfn"))(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f}
427+
C = coefficient(FD{T, f})
428+
# Note: The div() will already throw for divide-by-zero and typemin(T) ÷ -1.
429+
v, b = Base.Checked.mul_with_overflow(C, $divfn(x.i, y.i))
430+
b && _throw_overflowerr_op($(QuoteNode(divfn)), x, y)
431+
return reinterpret(FD{T, f}, v)
432+
end
433+
end
434+
for remfn in [:rem, :mod]
435+
# rem and mod already check for divide-by-zero and typemin(T) ÷ -1, so nothing to do.
436+
@eval Base.$(Symbol("checked_$remfn"))(x::T, y::T) where {T <: FD} = $remfn(x, y)
437+
end
438+
439+
@noinline _throw_overflowerr_op(op, x::T, y::T) where T = throw(OverflowError("$op($x, $y) overflowed for type $T"))
440+
441+
function Base.checked_neg(x::T) where {T<:FD}
442+
r = -x
443+
(x<0) & (r<0) && Base.Checked.throw_overflowerr_negation(x)
444+
return r
445+
end
446+
function Base.checked_abs(x::FD)
447+
r = ifelse(x<0, -x, x)
448+
r<0 || return r
449+
_throw_overflow_abs(x)
450+
end
451+
if VERSION >= v"1.8.0-"
452+
@noinline _throw_overflow_abs(x) =
453+
throw(OverflowError(LazyString("checked arithmetic: cannot compute |x| for x = ", x, "::", typeof(x))))
454+
else
455+
@noinline _throw_overflow_abs(x) =
456+
throw(OverflowError("checked arithmetic: cannot compute |x| for x = $x"))
457+
end
458+
459+
# We introduce a new function for this since Base.Checked only supports integers, and ints
460+
# don't have a decimal division operation.
461+
"""
462+
FixedPointDecimals.checked_rdiv(x::FD, y::FD) -> FD
463+
464+
Calculates `x / y`, checking for overflow errors where applicable.
465+
466+
The overflow protection may impose a perceptible performance penalty.
467+
468+
See also:
469+
- `Base.checked_div` for truncating division.
470+
"""
471+
checked_rdiv(x::FD, y::FD) = checked_rdiv(promote(x, y)...)
472+
473+
function checked_rdiv(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f}
474+
powt = coefficient(FD{T, f})
475+
quotient, remainder = fldmod(widemul(x.i, powt), y.i)
476+
v = _round_to_nearest(quotient, remainder, y.i)
477+
typemin(T) <= v <= typemax(T) || Base.Checked.throw_overflowerr_binaryop(:/, x, y)
478+
return reinterpret(FD{T, f}, v)
371479
end
372480

481+
# These functions allow us to perform division with integers outside of the range of the
482+
# FixedDecimal.
483+
function checked_rdiv(x::Integer, y::FD{T, f}) where {T<:Integer, f}
484+
powt = coefficient(FD{T, f})
485+
powtsq = widemul(powt, powt)
486+
quotient, remainder = fldmod(widemul(x, powtsq), y.i)
487+
v = _round_to_nearest(quotient, remainder, y.i)
488+
typemin(T) <= v <= typemax(T) || Base.Checked.throw_overflowerr_binaryop(:/, x, y)
489+
reinterpret(FD{T, f}, v)
490+
end
491+
function checked_rdiv(x::FD{T, f}, y::Integer) where {T<:Integer, f}
492+
quotient, remainder = fldmod(x.i, y)
493+
v = _round_to_nearest(quotient, remainder, y)
494+
typemin(T) <= v <= typemax(T) || Base.Checked.throw_overflowerr_binaryop(:/, x, y)
495+
reinterpret(FD{T, f}, v)
496+
end
497+
498+
499+
# --------------------------
500+
373501
Base.convert(::Type{AbstractFloat}, x::FD) = convert(floattype(typeof(x)), x)
374502
function Base.convert(::Type{TF}, x::FD{T, f}) where {TF <: AbstractFloat, T, f}
375503
convert(TF, x.i / coefficient(FD{T, f}))::TF

0 commit comments

Comments
 (0)