Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ To construct such a number, use `1.3N4f12`, `N4f12(1.3)`, `convert(N4f12, 1.3)`,
`Normed{UInt16,12}(1.3)`, or `reinterpret(N4f12, 0x14cc)`.
The last syntax means to construct a `N4f12` from the `UInt16` value `0x14cc`.

To read a number from its textual representation, use `parse(N4f12, "1.3")`, or
`tryparse(N4f12, "1.3")` which returns `nothing` instead of throwing when the
string is malformed or out of range.
The string must hold a decimal number, optionally with an exponent such as `"13e-1"`,
and is rounded exactly to the nearest representable value, with ties to even.

More generally, an arbitrary number of bits from any of the standard unsigned
integer widths can be used for the fractional part. For example:
`Normed{UInt32,16}`, `Normed{UInt64,3}`, `Normed{UInt128,7}`.
Expand Down
118 changes: 118 additions & 0 deletions src/FixedPointNumbers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,124 @@ function rationalize(::Type{Ti}, x::FixedPoint; tol::Real=eps(x)) where Ti <: In
tol <= eps(x) ? Rational{Ti}(x) : rationalize(Ti, float(x), tol)
end

# parsing

_isdigit(c) = UInt8('0') <= c <= UInt8('9')
_isspace(c) = (c == UInt8(' ')) | (UInt8('\t') <= c <= UInt8('\r'))

# unsigned type that holds ten times the scale of `X` without overflow
_parsetype(::Type{X}) where {T, X <: FixedPoint{T}} =
bitwidth(T) <= 32 ? UInt64 : bitwidth(T) <= 64 ? UInt128 : BigInt

# the parser compares code units with ASCII bytes, which needs UTF-8
Base.tryparse(::Type{X}, s::AbstractString) where {X <: FixedPoint} = tryparse(X, String(s))

function Base.tryparse(::Type{X}, s::Union{String, SubString{String}}) where {X <: FixedPoint}
cu = codeunits(s)
lo, hi = 1, length(cu)
# skip leading whitespace
while lo <= hi && _isspace(cu[lo])
lo += 1
end
lo <= hi || return nothing
# skip trailing whitespace, stopping at the latest at `lo`, which holds no space
while _isspace(cu[hi])
hi -= 1
end
neg = cu[lo] == UInt8('-')
if neg || cu[lo] == UInt8('+')
lo += 1
end
i = lo
# digits before the dot
point = 0
hasdot = false
# scan the digits and at most one dot
while i <= hi
c = cu[i]
if _isdigit(c)
point += !hasdot
elseif c == UInt8('.') && !hasdot
hasdot = true
else
break
end
i += 1
end
# the loop consumed at most one dot, so it consumed a digit if it consumed more
i - lo > hasdot || return nothing
mantissa_end = i - 1
exponent = zero(widen(Int))
if i <= hi
(cu[i] == UInt8('e')) | (cu[i] == UInt8('E')) || return nothing
i += 1
i <= hi || return nothing
negexp = cu[i] == UInt8('-')
if negexp || cu[i] == UInt8('+')
i += 1
i <= hi || return nothing
end
# accumulate the exponent digits
while i <= hi
_isdigit(cu[i]) || return nothing
# with at most `typemax(Int)` digits, nonzero values beyond the cap overflow or round to 0
exponent = min(10exponent + (cu[i] - UInt8('0')), 2 * widen(typemax(Int)))
i += 1
end
negexp && (exponent = -exponent)
end
return _tryparse_decimal(X, cu, lo, mantissa_end, point + exponent, neg)
end

# Round the decimal digits in `cu[lo:hi]` (ignoring the dot), of which the first `point`
# form the integer part, exactly and with ties to even.
function _tryparse_decimal(::Type{X}, cu::Base.CodeUnits{UInt8}, lo::Int, hi::Int, point::Integer, neg::Bool) where {T, f, X <: FixedPoint{T,f}}
U = _parsetype(X)
# the raw value is the parsed value times the scale `S`, rounded
S = X <: Fixed ? one(U) << f : (one(U) << f) - one(U)
qmax = X <: Fixed && neg ? U(typemax(T)) + one(U) : U(typemax(T))
imax = qmax ÷ S + one(U)
ip, k = zero(U), 0
i = lo
while i <= hi && k < point
c = cu[i]
if c != UInt8('.')
ip = 10ip + (c - UInt8('0'))
ip > imax && return nothing
k += 1
end
i += 1
end
for _ in k+1:point
iszero(ip) && break
ip *= 10
ip > imax && return nothing
end
# Horner's scheme from the last digit yields `S * fraction == fp + (r + rest) / 10`
# with an integer `fp`, a digit `r` and `0 <= rest < 1`, where `sticky == (rest > 0)`.
fp, r, sticky = zero(U), zero(U), false
for j in hi:-1:i
cu[j] == UInt8('.') && continue
sticky |= !iszero(r)
fp, r = divrem((cu[j] - UInt8('0')) * S + fp, U(10))
end
for _ in 1:-point
sticky |= !iszero(r)
fp, r = divrem(fp, U(10))
iszero(fp) & iszero(r) && break
end
q = ip * S + fp
inexact = !iszero(r) | sticky
if X <: Normed
neg && (!iszero(q) | inexact) && return nothing
(q < qmax) | ((q == qmax) & !inexact) || return nothing
end
q += (r > 5) | ((r == 5) & (sticky | isodd(q)))
q <= qmax || return nothing
raw = q % T
return reinterpret(X, neg ? -raw : raw)
end

"""
isapprox(x::FixedPoint, y::FixedPoint; rtol=0, atol=max(eps(x), eps(y)))

Expand Down
34 changes: 34 additions & 0 deletions test/common.jl
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,40 @@ function test_rem_type(TX::Type)
end
end

# `n / 10^d` written out in decimal notation
function decimal_string(n::BigInt, d::Int)
s = lpad(string(abs(n)), d + 1, '0')
string(n < 0 ? "-" : "", s[1:end-d], d == 0 ? "" : ".", s[end-d+1:end])
end

# exact conversion of `v`, rounding ties to even
function parse_reference(::Type{X}, v::Rational{BigInt}) where {T, f, X <: FixedPoint{T,f}}
if X <: Fixed
y = round(BigInt, v * big(2)^f)
typemin(T) <= y <= typemax(T) || return nothing
else
y = v * (big(2)^f - 1)
0 <= y <= typemax(T) || return nothing
y = round(BigInt, y)
end
return reinterpret(X, y % T)
end

function test_parse(TX::Type)
@testset "parse $X" for X in target(TX; ex = :light)
T, f = rawtype(X), nbitsfrac(X)
S = X <: Fixed ? big(2)^f : big(2)^f - 1
rng = StableRNG(1234)
raws = (typemin(T), typemax(T), zero(T), one(T), rand(rng, T, 10)...)
# decimals next to a representable value (h == 0) or a midpoint, which
# is exact for `Fixed` with `d == f + 1`
@test all(Iterators.product(raws, -1:1, (0, 3, 20, f + 1), -1:1)) do (raw, h, d, δ)
n = round(BigInt, (2big(raw) + h) * big(10)^d // 2S) + δ
tryparse(X, decimal_string(n, d)) === parse_reference(X, n // big(10)^d)
end
end
end

function test_rem_nan(TX::Type)
@testset "nan % $X" for X in target(TX, :i8, :i16, :i32, :i64; ex = :thin)
@test NaN % X === NaN32 % X === NaN16 % X === zero(X)
Expand Down
75 changes: 74 additions & 1 deletion test/fixed.jl
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ end

@testset "domain of f" begin
# TODO: change the upper limit
@test_logs (:warn, r"`f=8` with raw type `T=Int8` will be removed") zero(Fixed{Int8,8})
if Base.JLOptions().depwarn == 1
@test_logs (:warn, r"`f=8` with raw type `T=Int8` will be removed") zero(Fixed{Int8,8})
else
@test zero(Fixed{Int8,8}) isa Fixed{Int8,8}
end
@test_throws DomainError zero(Fixed{Int8,-1})
# @test_throws DomainError zero(Fixed{Int8,8})
@test_throws DomainError zero(Fixed{Int8,9})
Expand Down Expand Up @@ -204,6 +208,75 @@ end
@test convert(Q0f63, tp) === reinterpret(Q0f63, typemax(Int64))
end

@testset "parse" begin
@test parse(Q3f4, "7.92") === 7.9375Q3f4
@test parse(Q0f7, "-1.0") === -1.0Q0f7
@test tryparse(Q3f4, "7.92") === 7.9375Q3f4
@test tryparse(Q3f4, "-3.5") === -3.5Q3f4
@test tryparse(Q3f4, "999.0") === nothing
@test tryparse(Q3f4, "abc") === nothing
@test_throws ArgumentError parse(Q3f4, "abc")
@test_throws ArgumentError parse(Q3f4, "999.0")

# leading/trailing whitespace and exponent notation
@test tryparse(Q3f4, " 1.5 ") === 1.5Q3f4
@test tryparse(Q3f4, "1e0") === 1.0Q3f4
@test tryparse(Q3f4, "1.0e0") === 1.0Q3f4
# signs, bare dot positions and malformed input
@test tryparse(Q3f4, "+1.5") === 1.5Q3f4
@test tryparse(Q3f4, ".5") === 0.5Q3f4
@test tryparse(Q3f4, "1.") === 1.0Q3f4
@test tryparse(Q3f4, "-.5") === -0.5Q3f4
@test tryparse(Q3f4, "") === nothing
@test tryparse(Q3f4, ".") === nothing
@test tryparse(Q3f4, "1.2.3") === nothing
@test tryparse(Q3f4, "\t1.5\r\n") === 1.5Q3f4
@test tryparse(Q3f4, SubString("x1.5", 2)) === 1.5Q3f4
@test tryparse(Q3f4, Test.GenericString("-1.5e0")) === -1.5Q3f4
@test tryparse(Q3f4, "1 .5") === nothing
@test tryparse(Q3f4, "+-1") === nothing
@test tryparse(Q3f4, "1_0") === nothing
@test tryparse(Q3f4, "1e") === nothing
@test tryparse(Q3f4, "1e+") === nothing
@test tryparse(Q3f4, "e1") === nothing
@test tryparse(Q3f4, "0x1p-1") === nothing
@test tryparse(Q3f4, "Inf") === nothing
@test tryparse(Q3f4, "NaN") === nothing
# exponents, of which only the sign matters beyond the `Int` range
@test tryparse(Q3f4, "25E-1") === 2.5Q3f4
@test tryparse(Q3f4, ".0625e+2") === 6.25Q3f4
@test tryparse(Q3f4, "1e1") === nothing
@test tryparse(Q3f4, "0e$(typemax(Int))") === 0Q3f4
@test tryparse(Q3f4, "0e99999999999999999999") === 0Q3f4
@test tryparse(Q3f4, "1e99999999999999999999") === nothing
@test tryparse(Q3f4, "-1e-$(typemax(Int))") === 0Q3f4
@test tryparse(Q3f4, "-1e-99999999999999999999") === 0Q3f4
# zeros before the first nonzero digit, before and after the dot
@test tryparse(Q3f4, "00000000000000000000000000001.5") === 1.5Q3f4
@test tryparse(Q3f4, "0." * "0"^27 * "15e28") === 1.5Q3f4
@test tryparse(Q3f4, "0") === 0Q3f4
@test tryparse(Q3f4, "00.") === 0Q3f4
@test tryparse(Q3f4, ".00") === 0Q3f4
@test tryparse(Q3f4, "-0.0e1") === 0Q3f4
@test tryparse(Q3f4, "0..5") === nothing
@test tryparse(Q3f4, ".e1") === nothing
# ties go to even, the bounds are typemin - eps/2 inclusive and typemax + eps/2 exclusive
@test tryparse(Q3f4, "0.03125") === 0Q3f4
@test tryparse(Q3f4, "0.09375") === 0.125Q3f4
@test tryparse(Q3f4, "0.03125000000000000000000000001") === eps(Q3f4)
@test tryparse(Q3f4, "7.96874999999999999999999999999") === typemax(Q3f4)
@test tryparse(Q3f4, "7.96875") === nothing
@test tryparse(Q3f4, "-8.03125") === typemin(Q3f4)
@test tryparse(Q3f4, "-8.03125000000000000000000000001") === nothing
# rounding through `Float64` would round to the midpoint first, and then to even
@test tryparse(Q0f7, "0.00390625000000000000001") === eps(Q0f7)
@test tryparse(Fixed{Int128,64}, "0.5") === reinterpret(Fixed{Int128,64}, Int128(1) << 63)
@test tryparse(Q3f4, "0.50000000000000000000") === 0.5Q3f4
@test tryparse(Q3f4, "0.50000000000000000001") === 0.5Q3f4

test_parse(Fixed)
end

@testset "bool conversions" begin
@test convert(Bool, 0.0Q1f6) === false
@test convert(Bool, 1.0Q1f6) === true
Expand Down
45 changes: 45 additions & 0 deletions test/normed.jl
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,51 @@ end
@test_throws InexactError convert(Int8, 256N8f8)
end

@testset "parse" begin
@test parse(N0f8, "0.5") === N0f8(0.5)
@test parse(N0f8, "1.0") === 1N0f8
@test parse(N0f8, "0.0") === 0N0f8
@test tryparse(N0f8, "0.5") === N0f8(0.5)
@test tryparse(N0f8, "-0.1") === nothing
@test tryparse(N0f8, "1.5") === nothing
@test tryparse(N0f8, "abc") === nothing
@test_throws ArgumentError parse(N0f8, "-0.1")
@test_throws ArgumentError parse(N0f8, "abc")

# leading/trailing whitespace and exponent notation
@test tryparse(N0f8, " 0.5 ") === N0f8(0.5)
@test tryparse(N0f8, "1e0") === 1N0f8
@test tryparse(N0f8, "0.5e0") === N0f8(0.5)
# signs, bare dot positions and malformed input
@test tryparse(N0f8, "+0.5") === N0f8(0.5)
@test tryparse(N0f8, ".5") === N0f8(0.5)
@test tryparse(N0f8, "-1") === nothing
@test tryparse(N0f8, "") === nothing
@test tryparse(N0f8, ".") === nothing
@test tryparse(N0f8, "0.5.0") === nothing
@test tryparse(N0f8, "0x1p-1") === nothing
# the value must lie in range exactly, but negative zero is fine
@test tryparse(N0f8, "-0.0") === 0N0f8
@test tryparse(N0f8, "-0e-$(typemax(Int))") === 0N0f8
@test tryparse(N0f8, "-0e99999999999999999999") === 0N0f8
@test tryparse(N0f8, "-0.001") === nothing
@test tryparse(N0f8, "-1e-$(typemax(Int))") === nothing
@test tryparse(N0f8, "-1e-99999999999999999999") === nothing
@test tryparse(N0f8, "1e-99999999999999999999") === 0N0f8
@test tryparse(N0f8, "1.000000000000000000000000000001") === nothing
@test tryparse(N0f8, "0.999999999999999999999999999999") === 1N0f8
@test tryparse(N0f8, "1e99999999999999999999") === nothing
# ties go to even
@test tryparse(N0f8, "0.5") === reinterpret(N0f8, 0x80)
@test tryparse(N7f1, "0.25") === 0N7f1
@test tryparse(N7f1, "0.75") === 1N7f1
@test tryparse(N7f1, "255") === typemax(N7f1)
@test tryparse(N7f1, "255.25") === nothing
@test tryparse(Normed{UInt128,128}, "0.5") === reinterpret(Normed{UInt128,128}, typemax(UInt128) ÷ 2 + 1)

test_parse(Normed)
end

@testset "rational conversions" begin
@testset "$N to/from rational" for N in target(Normed)
@test convert(Rational, oneunit(N)) == 1//1
Expand Down
Loading