Skip to content

Commit 6236e66

Browse files
authored
Add support for digits keyword argument of round() (#235)
This only supports the non-negative `digits` option. All other options throw an error. This provides specialized implementations only for `N0f8` and `Fixed{Int8}`. The other types use fallback with floating-point operations and therefore it is slower and less accurate.
1 parent f57e96b commit 6236e66

File tree

6 files changed

+115
-8
lines changed

6 files changed

+115
-8
lines changed

src/FixedPointNumbers.jl

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,35 @@ end
390390

391391
signbit(x::X) where {X <: FixedPoint} = signbit(x.i)
392392

393+
~(x::X) where {X <: FixedPoint} = X(~x.i, 0)
394+
395+
function round(x::FixedPoint, r::RoundingMode=RoundNearest;
396+
digits::Union{Nothing, Integer}=nothing,
397+
sigdigits::Union{Nothing, Integer}=nothing,
398+
base::Union{Nothing, Integer}=nothing)
399+
400+
d = digits === nothing ? 0 : Int(digits)
401+
402+
if base !== nothing && base != 10
403+
throw(ArgumentError("`base` numbers other than 10 are not supported."))
404+
elseif sigdigits !== nothing
405+
throw(ArgumentError("`sigdigits` is not supported."))
406+
elseif d < 0
407+
throw(ArgumentError("negative `digits` is not supported."))
408+
end
409+
d === 0 ? _round_digits0(x, r) : _round_digits(x, r, d)
410+
end
411+
function _round_digits(x::X, r::RoundingMode, d::Int) where {T, f, X <: FixedPoint{T,f}}
412+
log10_2 = 0.3010299956639812
413+
d > floor(Int, ((f + 1) * log10_2)) && return x
414+
r = round(float(x), r, digits=d)
415+
typemin(X) - eps(X)/2 <= r < typemax(X) + eps(X)/2 || throw_converterror(X, r)
416+
clamp(r, X)
417+
end
418+
419+
trunc(x::X) where {X <: FixedPoint{<:Unsigned}} = floor(x)
420+
trunc(::Type{Ti}, x::X) where {X <: FixedPoint{<:Unsigned}, Ti <: Integer} = floor(Ti, x)
421+
393422
for f in (:zero, :oneunit, :one, :eps, :rawone, :rawtype, :floattype)
394423
@eval begin
395424
$f(x::FixedPoint) = $f(typeof(x))
@@ -400,11 +429,6 @@ for f in (:(==), :<, :<=, :fld1)
400429
$f(x::X, y::X) where {X <: FixedPoint} = $f(x.i, y.i)
401430
end
402431
end
403-
for f in (:~, )
404-
@eval begin
405-
$f(x::X) where {X <: FixedPoint} = X($f(x.i), 0)
406-
end
407-
end
408432
for f in (:mod1, :min, :max)
409433
@eval begin
410434
$f(x::X, y::X) where {X <: FixedPoint} = X($f(x.i, y.i), 0)
@@ -415,7 +439,7 @@ for (m, f) in ((:(:Nearest), :round),
415439
(:(:Up), :ceil),
416440
(:(:Down), :floor))
417441
@eval begin
418-
round(x::FixedPoint, ::RoundingMode{$m}) = $f(x)
442+
_round_digits0(x::FixedPoint, ::RoundingMode{$m}) = $f(x)
419443
round(::Type{Ti}, x::FixedPoint, ::RoundingMode{$m}) where {Ti <: Integer} = $f(Ti, x)
420444
end
421445
end

src/fixed.jl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,27 @@ function round(x::Fixed{T,f}) where {T, f}
214214
z = y & intmask(x)
215215
reinterpret(Fixed{T,f}, z - T(y & m == rawone(x)) << f)
216216
end
217+
function _round_digits(x::F, r::RoundingMode, d::Int) where {f, F <: Fixed{Int8, f}}
218+
xd = x.i * Int16(d === 1 ? 10 : 100)
219+
uf = UInt8(f)
220+
if r isa Union{RoundingMode{:Down}, RoundingMode{:ToZero}}
221+
t = r isa RoundingMode{:ToZero} && x.i < 0 ? fracmask(x) : Int16(0)
222+
xr = (xd + t) >> uf
223+
else
224+
xr = r isa RoundingMode{:Up} ? (xd + fracmask(x)) >> uf : div_2f(xd, Val(Int(f)))
225+
if d < 3 && xr == (Int16(d === 1 ? 10 : 100) << (0x7 - uf))
226+
throw_converterror(F, @exp2(7 - f))
227+
end
228+
end
229+
h8, h24 = Int16(0x80), Int32(1 << 23)
230+
if d === 1
231+
f >= 5 && return F(((xr * Int16(6553) >> (0x8 - uf) + h8) >> 0x8) % Int8, 0)
232+
f >= 3 && return F(((xr * Int32(26843545) >> (0x4 - uf) + h24) >> 0x18) % Int8, 0)
233+
elseif d === 2
234+
return F(((xr * Int32(42949672) >> (0x8 - uf) + h24) >> 0x18) % Int8, 0)
235+
end
236+
return x
237+
end
217238

218239
function trunc(::Type{Ti}, x::Fixed{T,f}) where {Ti <: Integer, T, f}
219240
f == 0 && return convert(Ti, x.i)

src/normed.jl

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,6 @@ end
294294

295295

296296
# Functions
297-
trunc(x::N) where {N <: Normed} = floor(x)
298297
floor(x::N) where {N <: Normed} = reinterpret(N, x.i - x.i % rawone(N))
299298
function ceil(x::Normed{T,f}) where {T, f}
300299
f == 1 && return x
@@ -310,8 +309,26 @@ function round(x::Normed{T,f}) where {T, f}
310309
q = rawone(x) - r
311310
reinterpret(Normed{T,f}, r > q ? x.i + q : x.i - r)
312311
end
312+
function _round_digits(x::N0f8, r::RoundingMode, d::Int)
313+
t(::RoundingMode{:Nearest}) = 0x0081
314+
t(::RoundingMode{:ToZero}) = 0x0003
315+
t(::RoundingMode{:Up}) = 0x00ff
316+
t(::RoundingMode{:Down}) = 0x0003
317+
xd = x.i * (d === 1 ? 0x000a : 0x0064)
318+
if d === 1
319+
x10 = xd + (x.i >> 0x5) + t(r)
320+
y = (x10 >> 0x8) % UInt8
321+
return N0f8(y * 0x19 + (y + 0x3) >> 0x2 + y >> 0x2, 0)
322+
elseif d === 2
323+
x100 = xd + ((xd + x.i) >> 0x8) + t(r)
324+
y = (x100 >> 0x8) % UInt8
325+
z = ((y * 0x233 + 0x212) >> 0xa) % UInt8
326+
return N0f8(z + y + y - (y === 0x1e) - (y === 0x46), 0)
327+
else
328+
return x
329+
end
330+
end
313331

314-
trunc(::Type{Ti}, x::Normed) where {Ti <: Integer} = floor(Ti, x)
315332
function floor(::Type{Ti}, x::Normed) where {Ti <: Integer}
316333
convert(Ti, reinterpret(x) ÷ rawone(x))
317334
end

test/common.jl

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,45 @@ function test_fld1_mod1(TX::Type)
317317
end
318318
end
319319

320+
function test_round_digits(TX::Type)
321+
function check(x, r, d)
322+
X = typeof(x)
323+
dec = round(Int, float(rationalize(x) * 10^d), r) // (10^d)
324+
if dec >= typemax(X) + eps(X)/2
325+
try
326+
round(x, r, digits=d)
327+
catch
328+
return true
329+
end
330+
return false
331+
end
332+
actual, expected = round(x, r, digits=d), float(dec) % X
333+
actual === expected && return true
334+
(actual - dec) == (dec - expected) && return true
335+
error(actual, " != ", expected)
336+
end
337+
for X in target(TX, :i8)
338+
xs = typemin(X):eps(X):typemax(X)
339+
modes = (RoundNearest, RoundToZero, RoundUp, RoundDown)
340+
@testset "round(::$X, $r, digits=x)" for r in modes
341+
@test all(x -> check(x, r, 1), xs)
342+
@test all(x -> check(x, r, 2), xs)
343+
@test all(x -> check(x, r, 3), xs)
344+
end
345+
end
346+
@testset "round(::$X, r, digits=x)" for X in target(TX; ex = :thin)
347+
@test round(X(0.462), digits=6) X(0.462) atol=1e-6
348+
@test trunc(X(0.462), digits=2) X(0.46) atol=1e-6
349+
@test ceil(X(0.462), digits=1) X(0.5) atol=1e-6
350+
@test floor(X(0.462), digits=0) X(0) atol=1e-6
351+
end
352+
for X in target(TX, :i8; ex = :thin)
353+
@test_throws ArgumentError round(eps(X), digits=-1)
354+
@test_throws ArgumentError round(eps(X), sigdigits=2)
355+
@test_throws ArgumentError round(eps(X), base=2)
356+
end
357+
end
358+
320359
function test_isapprox(TX::Type)
321360
@testset "approx $X" for X in target(TX, :i8, :i16; ex = :light)
322361
xs = typemin(X):eps(X):typemax(X)-eps(X)

test/fixed.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,9 @@ end
543543
@test round(Int, -1.5Q1f6, RoundUp) === -1
544544
@test round(Int, -1.5Q1f6, RoundDown) === -2
545545
end
546+
@testset "rounding with digits" begin
547+
test_round_digits(Fixed)
548+
end
546549
@test_throws InexactError trunc(UInt, typemin(Q0f7))
547550
@test_throws InexactError floor(UInt, -eps(Q0f7))
548551
end

test/normed.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,9 @@ end
515515
@test round(Int, 1.504N1f7, RoundUp) === 2
516516
@test round(Int, 1.504N1f7, RoundDown) === 1
517517
end
518+
@testset "rounding with digits" begin
519+
test_round_digits(Normed)
520+
end
518521
end
519522

520523
@testset "approx" begin

0 commit comments

Comments
 (0)