Skip to content

Commit f1ad20e

Browse files
committed
Restricting precision based upon storage type
1 parent e61df92 commit f1ad20e

File tree

2 files changed

+103
-70
lines changed

2 files changed

+103
-70
lines changed

src/FixedPointDecimals.jl

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,25 @@ for fn in [:trunc, :floor, :ceil]
7272
end
7373

7474
"""
75-
FixedDecimal{I <: Integer, f::Int}
75+
FixedDecimal{T <: Integer, f::Int}
7676
77-
A fixed-point decimal type backed by integral type `I`, with `f` digits after
77+
A fixed-point decimal type backed by integral type `T`, with `f` digits after
7878
the decimal point stored.
7979
"""
8080
immutable FixedDecimal{T <: Integer, f} <: Real
8181
i::T
8282

83-
# internal constructor
84-
Base.reinterpret{T, f}(::Type{FixedDecimal{T, f}}, i::Integer) =
85-
new{T, f}(i % T)
83+
# inner constructor
84+
function Base.reinterpret{T, f}(::Type{FixedDecimal{T, f}}, i::Integer)
85+
if T != BigInt && 0 <= f <= max_exp10(T) || T == BigInt && f >= 0
86+
new{T, f}(i % T)
87+
else
88+
throw(ArgumentError(
89+
"Requested number of decimal places $f exceeds the max allowed for the " *
90+
"storage type $T: [0, $(max_exp10(T))]"
91+
))
92+
end
93+
end
8694
end
8795

8896
const FD = FixedDecimal
@@ -424,7 +432,13 @@ T or wider.
424432
end
425433
end
426434

427-
coefficient{T, f}(::Type{FD{T, f}}) = exp10(FD{T, f})
435+
"""
436+
coefficient(::Type{FD{T, f}}) -> T
437+
438+
Compute `10^f` as an Integer without overflow. Note that overflow will not occur for any
439+
constructable `FD{T, f}`.
440+
"""
441+
coefficient{T, f}(::Type{FD{T, f}}) = T(10)^f
428442
coefficient{T, f}(fd::FD{T, f}) = coefficient(FD{T, f})
429443
value(fd::FD) = fd.i
430444

test/runtests.jl

Lines changed: 83 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,12 @@ end
9292
# ensure that the coefficient multiplied by the highest and lowest representable values of
9393
# the container type do not result in overflow.
9494
@testset "coefficient" begin
95-
for (i, T) in enumerate(CONTAINER_TYPES)
96-
for j in i:length(CONTAINER_TYPES)
97-
f = FixedPointDecimals.max_exp10(CONTAINER_TYPES[j])
98-
powt = FixedPointDecimals.coefficient(FD{T, f})
99-
@test powt % 10 == 0
100-
@test checked_mul(widen(powt), typemax(T)) == widemul(powt, typemax(T))
101-
@test checked_mul(widen(powt), typemin(T)) == widemul(powt, typemin(T))
102-
end
95+
@testset "overflow $T" for T in CONTAINER_TYPES
96+
f = FixedPointDecimals.max_exp10(T)
97+
powt = FixedPointDecimals.coefficient(FD{T, f})
98+
@test powt % 10 == 0
99+
@test checked_mul(widen(powt), typemax(T)) == widemul(powt, typemax(T))
100+
@test checked_mul(widen(powt), typemin(T)) == widemul(powt, typemin(T))
103101
end
104102
end
105103

@@ -143,8 +141,13 @@ end
143141
end
144142

145143
@testset "limits of $T" for T in CONTAINER_TYPES
146-
f = FixedPointDecimals.max_exp10(T) + 1
147-
powt = FixedPointDecimals.coefficient(FD{T,f})
144+
max_exp = FixedPointDecimals.max_exp10(T)
145+
f = max_exp
146+
powt = widen(FixedPointDecimals.coefficient(FD{T,f}))
147+
148+
# Smallest positive integer which is out-of-bounds for the FD
149+
x = max_exp - f + 1
150+
oob = T(10)^(x > 0 ? x : 0)
148151

149152
# ideally we would just use `typemax(T)` but due to precision issues with
150153
# floating-point its possible the closest float will exceed `typemax(T)`.
@@ -161,7 +164,7 @@ end
161164
@test convert(FD{T,f}, typemax(T) // powt) == reinterpret(FD{T,f}, typemax(T))
162165
@test convert(FD{T,f}, typemin(T) // powt) == reinterpret(FD{T,f}, typemin(T))
163166

164-
@test_throws InexactError convert(FD{T,f}, T(1))
167+
@test_throws InexactError convert(FD{T,f}, oob)
165168

166169
# Converting to a floating-point
167170
fd = reinterpret(FD{T,f}, typemax(T))
@@ -181,24 +184,21 @@ end
181184
fd = reinterpret(FD{T,f}, typemin(T))
182185
@test convert(Rational, fd) == typemin(T) // powt
183186

184-
# Adjust number of decimal places allowed so we can have `-10 < x < 10` where x is
185-
# a FD{T,f}. Only needed to test `convert(::FD, ::Integer)`
186-
f = FixedPointDecimals.max_exp10(T)
187-
powt = FixedPointDecimals.coefficient(FD{T,f})
188-
187+
# The following tests require that the number of decimal places allow for
188+
# `-10 < x < 10` where x is a FD{T,f}. Needed to test `convert(::FD, ::Integer)`.
189189
max_int = typemax(T) ÷ powt * powt
190190
min_int = typemin(T) ÷ powt * powt
191191

192192
@test convert(FD{T,f}, max_int ÷ powt) == reinterpret(FD{T,f}, max_int)
193193
@test convert(FD{T,f}, min_int ÷ powt) == reinterpret(FD{T,f}, min_int)
194194

195-
@test_throws InexactError convert(FD{T,f}, max_int ÷ powt + T(1))
196-
@test_throws InexactError convert(FD{T,f}, min_int ÷ powt - T(1)) # Overflows with Unsigned
195+
@test_throws InexactError convert(FD{T,f}, max_int ÷ powt + oob)
196+
@test_throws InexactError convert(FD{T,f}, min_int ÷ powt - oob) # Overflows with Unsigned
197197
end
198198

199199
@testset "limits from $U to $T" for T in CONTAINER_TYPES, U in CONTAINER_TYPES
200-
f = FixedPointDecimals.max_exp10(T) + 1
201-
g = FixedPointDecimals.max_exp10(U) + 1
200+
f = FixedPointDecimals.max_exp10(T)
201+
g = FixedPointDecimals.max_exp10(U)
202202
powt = div(
203203
FixedPointDecimals.coefficient(FD{T, f}),
204204
FixedPointDecimals.coefficient(FD{U, g}),
@@ -329,10 +329,8 @@ end
329329
end
330330

331331
@testset "limits of $T" for T in CONTAINER_TYPES
332-
x = FixedPointDecimals.max_exp10(T)
333-
f = x + 1
334-
335-
scalar = reinterpret(FD{T,f}, T(10)^x) # 0.1
332+
f = FixedPointDecimals.max_exp10(T)
333+
scalar = convert(FD{T,f}, 1 // 10) # 0.1
336334

337335
# Since multiply will round the result we'll make sure our value does not
338336
# always rounds down.
@@ -445,10 +443,13 @@ end
445443
end
446444

447445
@testset "limits of $T" for T in CONTAINER_TYPES
448-
x = FixedPointDecimals.max_exp10(T)
449-
f = x + 1
446+
max_exp = FixedPointDecimals.max_exp10(T)
447+
f = max_exp
448+
scalar = convert(FD{T,f}, 1 // 10) # 0.1
450449

451-
scalar = reinterpret(FD{T,f}, T(10)^x) # 0.1
450+
# Should be outside of the bounds of a FD{T,f}
451+
x = T(10)
452+
@test_throws InexactError FD{T,f}(x)
452453

453454
# Since multiply will round the result we'll make sure our value always
454455
# rounds down.
@@ -459,13 +460,8 @@ end
459460

460461
@test (max_fd * scalar) / scalar == max_fd
461462
@test (min_fd * scalar) / scalar == min_fd
462-
@test max_fd / T(2) == reinterpret(FD{T,f}, div(max_int, 2))
463-
@test min_fd / T(2) == reinterpret(FD{T,f}, div(min_int, 2))
464-
465-
# Since the precision of `f` doesn't allow us to make a FixedDecimal >= 1
466-
# there is no way testing this function without raising an exception.
467-
max_fd != 0 && @test_throws InexactError T(2) / max_fd
468-
min_fd != 0 && @test_throws InexactError T(2) / min_fd
463+
@test max_fd / x == reinterpret(FD{T,f}, div(max_int, x))
464+
@test min_fd / x == reinterpret(FD{T,f}, div(min_int, x))
469465
end
470466
end
471467

@@ -486,10 +482,11 @@ end
486482
end
487483

488484
@testset "isinteger" begin
489-
@testset "overflow" begin
490-
# Note: After overflow `Int8(10)^6 == 64`
491-
@test !isinteger(reinterpret(FD{Int8,6}, 64)) # 0.000064
492-
end
485+
# Note: Test cannot be used unless we can construct `FD{Int8,6}`
486+
# @testset "overflow" begin
487+
# # Note: After overflow `Int8(10)^6 == 64`
488+
# @test !isinteger(reinterpret(FD{Int8,6}, 64)) # 0.000064
489+
# end
493490

494491
@testset "limits of $T" for T in CONTAINER_TYPES
495492
f = FixedPointDecimals.max_exp10(T)
@@ -531,7 +528,7 @@ end
531528
end
532529

533530
@testset "limits of $T" for T in CONTAINER_TYPES
534-
f = FixedPointDecimals.max_exp10(T) + 1
531+
f = FixedPointDecimals.max_exp10(T)
535532
powt = FixedPointDecimals.coefficient(FD{T,f})
536533

537534
# Ideally we would just use `typemax(T)` but due to precision issues with
@@ -547,18 +544,23 @@ end
547544
@test round(FD{T,f}, typemax(T) // powt) == reinterpret(FD{T,f}, typemax(T))
548545
@test round(FD{T,f}, typemin(T) // powt) == reinterpret(FD{T,f}, typemin(T))
549546

550-
# Note: due to the size of `f` all values `x::FD{T,f}` are `-1 < x < 1` which means
551-
# that rounding away from zero will result in an exception.
552-
if round(T, typemax(T) / powt) != 0
553-
@test_throws InexactError round(reinterpret(FD{T,f}, typemax(T)))
547+
# Note: rounding away from zero will result in an exception.
548+
max_int = typemax(T)
549+
min_int = typemin(T)
550+
551+
max_dec = max_int / powt
552+
min_dec = min_int / powt
553+
554+
if round(T, max_dec) == trunc(T, max_dec)
555+
@test round(reinterpret(FD{T,f}, max_int)) == FD{T,f}(round(T, max_dec))
554556
else
555-
@test round(reinterpret(FD{T,f}, typemax(T))) == zero(FD{T,f})
557+
@test_throws InexactError round(reinterpret(FD{T,f}, max_int))
556558
end
557559

558-
if round(T, typemin(T) / powt) != 0
559-
@test_throws InexactError round(reinterpret(FD{T,f}, typemin(T)))
560+
if round(T, min_dec) == trunc(T, min_dec)
561+
@test round(reinterpret(FD{T,f}, min_int)) == FD{T,f}(round(T, min_dec))
560562
else
561-
@test round(reinterpret(FD{T,f}, typemin(T))) == zero(FD{T,f})
563+
@test_throws InexactError round(reinterpret(FD{T,f}, min_int))
562564
end
563565
end
564566
end
@@ -608,7 +610,7 @@ end
608610
end
609611

610612
@testset "limits of $T" for T in CONTAINER_TYPES
611-
f = FixedPointDecimals.max_exp10(T) + 1
613+
f = FixedPointDecimals.max_exp10(T)
612614
powt = FixedPointDecimals.coefficient(FD{T,f})
613615

614616
# Ideally we would just use `typemax(T)` but due to precision issues with
@@ -624,9 +626,8 @@ end
624626
@test value(trunc(FD{T,f}, max_int / powt)) in [max_int, max_int - 1]
625627
@test value(trunc(FD{T,f}, min_int / powt)) in [min_int, min_int + 1]
626628

627-
# Note: all values `x` in FD{T,f} are -1 < x < 1
628-
@test trunc(reinterpret(FD{T,f}, typemax(T))) == zero(FD{T,f})
629-
@test trunc(reinterpret(FD{T,f}, typemin(T))) == zero(FD{T,f})
629+
@test trunc(reinterpret(FD{T,f}, typemax(T))) == FD{T,f}(div(typemax(T), powt))
630+
@test trunc(reinterpret(FD{T,f}, typemin(T))) == FD{T,f}(div(typemin(T), powt))
630631
end
631632
end
632633

@@ -674,7 +675,7 @@ epsi{T}(::Type{T}) = eps(T)
674675
end
675676

676677
@testset "limits of $T" for T in CONTAINER_TYPES
677-
f = FixedPointDecimals.max_exp10(T) + 1
678+
f = FixedPointDecimals.max_exp10(T)
678679
powt = FixedPointDecimals.coefficient(FD{T,f})
679680

680681
# Ideally we would just use `typemax(T)` but due to precision issues with
@@ -695,16 +696,26 @@ epsi{T}(::Type{T}) = eps(T)
695696
@test value(ceil(FD{T,f}, max_dec)) in [max_int, signed(widen(max_int)) + 1]
696697
@test value(ceil(FD{T,f}, min_dec)) in [min_int, min_int + 1]
697698

698-
# Note: all values `x` in FD{T,f} are -1 < x < 1
699-
@test floor(reinterpret(FD{T,f}, typemax(T))) == zero(FD{T,f})
700-
if T <: Unsigned
701-
@test floor(reinterpret(FD{T,f}, typemin(T))) == zero(FD{T,f})
699+
# Note: rounding away from zero will result in an exception.
700+
max_int = typemax(T)
701+
min_int = typemin(T)
702+
703+
max_dec = max_int / powt
704+
min_dec = min_int / powt
705+
706+
@test floor(reinterpret(FD{T,f}, max_int)) == FD{T,f}(floor(T, max_dec))
707+
if floor(T, min_dec) == trunc(T, min_dec)
708+
@test floor(reinterpret(FD{T,f}, min_int)) == FD{T,f}(floor(T, min_dec))
702709
else
703-
@test_throws InexactError floor(reinterpret(FD{T,f}, typemin(T)))
710+
@test_throws InexactError floor(reinterpret(FD{T,f}, min_int))
704711
end
705712

706-
@test_throws InexactError ceil(reinterpret(FD{T,f}, typemax(T)))
707-
@test ceil(reinterpret(FD{T,f}, typemin(T))) == zero(FD{T,f})
713+
if ceil(T, max_dec) == trunc(T, max_dec)
714+
@test ceil(reinterpret(FD{T,f}, max_int)) == FD{T,f}(ceil(T, max_dec))
715+
else
716+
@test_throws InexactError ceil(reinterpret(FD{T,f}, max_int))
717+
end
718+
@test ceil(reinterpret(FD{T,f}, min_int)) == FD{T,f}(ceil(T, min_dec))
708719
end
709720
end
710721

@@ -720,12 +731,20 @@ end
720731
# Displaying a decimal could be incorrect when using a decimal place precision which is
721732
# close to or at the limit for our storage type.
722733
@testset "limits of $T" for T in CONTAINER_TYPES
723-
f = FixedPointDecimals.max_exp10(T) + 1
724-
max_str = "0." * rpad(typemax(T), f, '0')
725-
min_str = (typemin(T) < 0 ? "-" : "") * "0." * rpad(abs(widen(typemin(T))), f, '0')
734+
f = FixedPointDecimals.max_exp10(T)
735+
736+
function fmt(val, f)
737+
str = string(val)
738+
neg = ""
739+
if str[1] == '-'
740+
neg = "-"
741+
str = str[2:end]
742+
end
743+
return string(neg, str[1], ".", rpad(str[2:end], f, '0'))
744+
end
726745

727-
@test string(reinterpret(FD{T,f}, typemax(T))) == max_str
728-
@test string(reinterpret(FD{T,f}, typemin(T))) == min_str
746+
@test string(reinterpret(FD{T,f}, typemax(T))) == fmt(typemax(T), f)
747+
@test string(reinterpret(FD{T,f}, typemin(T))) == fmt(typemin(T), f)
729748
end
730749
end
731750

0 commit comments

Comments
 (0)