diff --git a/docs/make.jl b/docs/make.jl index a95f09a..e60989c 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,5 +1,5 @@ using Documenter, JSON -makedocs(modules = [JSON], sitename = "JSON.jl") +makedocs(modules = [JSON], sitename = "JSON.jl", checkdocs_ignored_modules = [JSON.Ryu]) deploydocs(repo = "github.com/JuliaIO/JSON.jl.git", push_preview = true) diff --git a/src/JSON.jl b/src/JSON.jl index 2b424d0..2d1570f 100644 --- a/src/JSON.jl +++ b/src/JSON.jl @@ -42,6 +42,7 @@ export JSONText, StructUtils, @noarg, @defaults, @tags, @choosetype, @nonstruct, throw(ArgumentError(msg)) end +include("ryu/Ryu.jl") include("utils.jl") include("object.jl") diff --git a/src/ryu/LICENSE.md b/src/ryu/LICENSE.md new file mode 100644 index 0000000..148635c --- /dev/null +++ b/src/ryu/LICENSE.md @@ -0,0 +1,27 @@ +The code in this directory (src/ryu) is a derivative based on the work in (base/ryu) in the https://github.com/JuliaLang/julia repository. + +The code in (base/ryu) is a derivative based on the work in the https://github.com/ulfjack/ryu repository, which allows the use of the Boost software license, included below. + +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/src/ryu/Ryu.jl b/src/ryu/Ryu.jl new file mode 100644 index 0000000..1c59d49 --- /dev/null +++ b/src/ryu/Ryu.jl @@ -0,0 +1,214 @@ +""" +This is mostly a copy of (base/ryu) in the https://github.com/JuliaLang/julia repository. +Unfortunately that code is not public. +""" +module Ryu + +# The following imported from Base are also internal so there definitions are copied below +# import .Base: significand_bits, significand_mask, exponent_bits, exponent_mask, exponent_bias, exponent_max, uinttype +const IEEEFloat = Union{Float16, Float32, Float64} + +exponent_mask(::Type{Float64}) = 0x7ff0_0000_0000_0000 +exponent_one(::Type{Float64}) = 0x3ff0_0000_0000_0000 +significand_mask(::Type{Float64}) = 0x000f_ffff_ffff_ffff + +exponent_mask(::Type{Float32}) = 0x7f80_0000 +exponent_one(::Type{Float32}) = 0x3f80_0000 +significand_mask(::Type{Float32}) = 0x007f_ffff + +exponent_mask(::Type{Float16}) = 0x7c00 +exponent_one(::Type{Float16}) = 0x3c00 +significand_mask(::Type{Float16}) = 0x03ff + +mantissa(x::T) where {T} = reinterpret(Unsigned, x) & significand_mask(T) + +for T in (Float16, Float32, Float64) + sb = trailing_ones(significand_mask(T)) + em = exponent_mask(T) + eb = Int(exponent_one(T) >> sb) + @eval significand_bits(::Type{$T}) = $(sb) + @eval exponent_bits(::Type{$T}) = $(sizeof(T)*8 - sb - 1) + @eval exponent_bias(::Type{$T}) = $(eb) + # maximum float exponent + @eval exponent_max(::Type{$T}) = $(Int(em >> sb) - eb - 1) + # maximum float exponent without bias + @eval exponent_raw_max(::Type{$T}) = $(Int(em >> sb)) +end + +# integer size of float +uinttype(::Type{Float64}) = UInt64 +uinttype(::Type{Float32}) = UInt32 +uinttype(::Type{Float16}) = UInt16 + +# import Base: append_c_digits_fast as append_c_digits, append_nine_digits + +# 2-digit decimal characters ("00":"99") +const _dec_d100 = UInt16[ +# generating expression: UInt16[(0x30 + i % 10) << 0x8 + (0x30 + i รท 10) for i = 0:99] +# 0 0, 0 1, 0 2, 0 3, and so on in little-endian + 0x3030, 0x3130, 0x3230, 0x3330, 0x3430, 0x3530, 0x3630, 0x3730, 0x3830, 0x3930, + 0x3031, 0x3131, 0x3231, 0x3331, 0x3431, 0x3531, 0x3631, 0x3731, 0x3831, 0x3931, + 0x3032, 0x3132, 0x3232, 0x3332, 0x3432, 0x3532, 0x3632, 0x3732, 0x3832, 0x3932, + 0x3033, 0x3133, 0x3233, 0x3333, 0x3433, 0x3533, 0x3633, 0x3733, 0x3833, 0x3933, + 0x3034, 0x3134, 0x3234, 0x3334, 0x3434, 0x3534, 0x3634, 0x3734, 0x3834, 0x3934, + 0x3035, 0x3135, 0x3235, 0x3335, 0x3435, 0x3535, 0x3635, 0x3735, 0x3835, 0x3935, + 0x3036, 0x3136, 0x3236, 0x3336, 0x3436, 0x3536, 0x3636, 0x3736, 0x3836, 0x3936, + 0x3037, 0x3137, 0x3237, 0x3337, 0x3437, 0x3537, 0x3637, 0x3737, 0x3837, 0x3937, + 0x3038, 0x3138, 0x3238, 0x3338, 0x3438, 0x3538, 0x3638, 0x3738, 0x3838, 0x3938, + 0x3039, 0x3139, 0x3239, 0x3339, 0x3439, 0x3539, 0x3639, 0x3739, 0x3839, 0x3939 +] + +function base_append_c_digits(olength::Int, digits::Unsigned, buf, pos::Int) + i = olength + while i >= 2 + d, c = divrem(digits, 0x64) + digits = oftype(digits, d) + @inbounds d100 = _dec_d100[(c % Int)::Int + 1] + @inbounds buf[pos + i - 2] = d100 % UInt8 + @inbounds buf[pos + i - 1] = (d100 >> 0x8) % UInt8 + i -= 2 + end + if i == 1 + @inbounds buf[pos] = UInt8('0') + rem(digits, 0xa) % UInt8 + i -= 1 + end + return pos + olength +end + +function append_nine_digits(digits::Unsigned, buf, pos::Int) + if digits == 0 + for _ = 1:9 + @inbounds buf[pos] = UInt8('0') + pos += 1 + end + return pos + end + return @inline base_append_c_digits(9, digits, buf, pos) # force loop-unrolling on the length +end + +function append_c_digits_fast(olength::Int, digits::Unsigned, buf, pos::Int) + i = olength + # n.b. olength may be larger than required to print all of `digits` (and will be padded + # with zeros), but the printed number will be undefined if it is smaller, and may include + # bits of both the high and low bytes. + maxpow10 = 0x3b9aca00 # 10e9 as UInt32 + while i > 9 && digits > typemax(UInt) + # do everything in cheap math chunks, using the processor's native math size + d, c = divrem(digits, maxpow10) + digits = oftype(digits, d) + append_nine_digits(c % UInt32, buf, pos + i - 9) + i -= 9 + end + base_append_c_digits(i, digits % UInt, buf, pos) + return pos + olength +end + +const append_c_digits = append_c_digits_fast + +include("utils.jl") +include("shortest.jl") +include("fixed.jl") +include("exp.jl") + +""" + Ryu.neededdigits(T) + +Number of digits necessary to represent type `T` in fixed-precision decimal. +""" +neededdigits(::Type{Float64}) = 309 + 17 +neededdigits(::Type{Float32}) = 39 + 9 + 2 +neededdigits(::Type{Float16}) = 9 + 5 + 9 + +""" + Ryu.writeshortest(x, plus=false, space=false, hash=true, precision=-1, expchar=UInt8('e'), padexp=false, decchar=UInt8('.'), typed=false, compact=false) + Ryu.writeshortest(buf::AbstractVector{UInt8}, pos::Int, x, args...) + +Convert a float value `x` into its "shortest" decimal string, which can be parsed back to the same value. +This function allows achieving the `%g` printf format. +Note the 2nd method allows passing in a byte buffer and position directly; callers must ensure the buffer has sufficient room to hold the entire decimal string. + +Various options for the output format include: + * `plus`: for positive `x`, prefix decimal string with a `'+'` character + * `space`: for positive `x`, prefix decimal string with a `' '` character; overridden if `plus=true` + * `hash`: whether the decimal point should be written, even if no additional digits are needed for precision + * `precision`: minimum number of digits to be included in the decimal string; extra `'0'` characters will be added for padding if necessary + * `expchar`: character to use exponent component in scientific notation + * `padexp`: whether two digits should always be written, even for single-digit exponents (e.g. `e+1` becomes `e+01`) + * `decchar`: decimal point character to be used + * `typed`: whether additional type information should be printed for `Float16` / `Float32` + * `compact`: output will be limited to 6 significant digits +""" +function writeshortest(x::T, + plus::Bool=false, + space::Bool=false, + hash::Bool=true, + precision::Integer=-1, + expchar::UInt8=UInt8('e'), + padexp::Bool=false, + decchar::UInt8=UInt8('.'), + typed::Bool=false, + compact::Bool=false) where {T <: IEEEFloat} + buf = Vector{UInt8}(undef, neededdigits(T)) + pos = writeshortest(buf, 1, x, plus, space, hash, precision, expchar, padexp, decchar, typed, compact) + return String(resize!(buf, pos - 1)) +end + +""" + Ryu.writefixed(x, precision, plus=false, space=false, hash=false, decchar=UInt8('.'), trimtrailingzeros=false) + Ryu.writefixed(buf::AbstractVector{UInt8}, pos::Int, x, args...) + +Convert a float value `x` into a "fixed" size decimal string of the provided precision. +This function allows achieving the `%f` printf format. +Note the 2nd method allows passing in a byte buffer and position directly; callers must ensure the buffer has sufficient room to hold the entire decimal string. + +Various options for the output format include: + * `plus`: for positive `x`, prefix decimal string with a `'+'` character + * `space`: for positive `x`, prefix decimal string with a `' '` character; overridden if `plus=true` + * `hash`: whether the decimal point should be written, even if no additional digits are needed for precision + * `precision`: minimum number of significant digits to be included in the decimal string; extra `'0'` characters will be added for padding if necessary + * `decchar`: decimal point character to be used + * `trimtrailingzeros`: whether trailing zeros of fractional part should be removed +""" +function writefixed(x::T, + precision::Integer, + plus::Bool=false, + space::Bool=false, + hash::Bool=false, + decchar::UInt8=UInt8('.'), + trimtrailingzeros::Bool=false) where {T <: IEEEFloat} + buf = Vector{UInt8}(undef, precision + neededdigits(T)) + pos = writefixed(buf, 1, x, precision, plus, space, hash, decchar, trimtrailingzeros) + return String(resize!(buf, pos - 1)) +end + +""" + Ryu.writeexp(x, precision, plus=false, space=false, hash=false, expchar=UInt8('e'), decchar=UInt8('.'), trimtrailingzeros=false) + Ryu.writeexp(buf::AbstractVector{UInt8}, pos::Int, x, args...) + +Convert a float value `x` into a scientific notation decimal string. +This function allows achieving the `%e` printf format. +Note the 2nd method allows passing in a byte buffer and position directly; callers must ensure the buffer has sufficient room to hold the entire decimal string. + +Various options for the output format include: + * `plus`: for positive `x`, prefix decimal string with a `'+'` character + * `space`: for positive `x`, prefix decimal string with a `' '` character; overridden if `plus=true` + * `hash`: whether the decimal point should be written, even if no additional digits are needed for precision + * `precision`: minimum number of significant digits to be included in the decimal string; extra `'0'` characters will be added for padding if necessary + * `expchar`: character to use exponent component in scientific notation + * `decchar`: decimal point character to be used + * `trimtrailingzeros`: whether trailing zeros should be removed +""" +function writeexp(x::T, + precision::Integer, + plus::Bool=false, + space::Bool=false, + hash::Bool=false, + expchar::UInt8=UInt8('e'), + decchar::UInt8=UInt8('.'), + trimtrailingzeros::Bool=false) where {T <: IEEEFloat} + buf = Vector{UInt8}(undef, precision + neededdigits(T)) + pos = writeexp(buf, 1, x, precision, plus, space, hash, expchar, decchar, trimtrailingzeros) + return String(resize!(buf, pos - 1)) +end + +end # module diff --git a/src/ryu/exp.jl b/src/ryu/exp.jl new file mode 100644 index 0000000..487c9c3 --- /dev/null +++ b/src/ryu/exp.jl @@ -0,0 +1,240 @@ +function writeexp(buf, pos, v::T, + precision=-1, plus=false, space=false, hash=false, + expchar=UInt8('e'), decchar=UInt8('.'), trimtrailingzeros=false) where {T <: IEEEFloat} + @assert 0 < pos <= length(buf) + startpos = pos + x = Float64(v) + pos = append_sign(x, plus, space, buf, pos) + + # special cases + if iszero(x) + @inbounds buf[pos] = UInt8('0') + pos += 1 + if precision > 0 && !trimtrailingzeros + @inbounds buf[pos] = decchar + pos += 1 + for _ = 1:precision + @inbounds buf[pos] = UInt8('0') + pos += 1 + end + elseif hash + @inbounds buf[pos] = decchar + pos += 1 + end + @inbounds buf[pos] = expchar + @inbounds buf[pos + 1] = UInt8('+') + @inbounds buf[pos + 2] = UInt8('0') + @inbounds buf[pos + 3] = UInt8('0') + return pos + 4 + elseif isnan(x) + @inbounds buf[pos] = UInt8('N') + @inbounds buf[pos + 1] = UInt8('a') + @inbounds buf[pos + 2] = UInt8('N') + return pos + 3 + elseif !isfinite(x) + @inbounds buf[pos] = UInt8('I') + @inbounds buf[pos + 1] = UInt8('n') + @inbounds buf[pos + 2] = UInt8('f') + return pos + 3 + end + + bits = reinterpret(UInt64, x) + mant = bits & MANTISSA_MASK + exp = Int((bits >> 52) & EXP_MASK) + + if iszero(exp) + e2 = 1 - 1023 - 52 + m2 = mant + else + e2 = exp - 1023 - 52 + m2 = (Int64(1) << 52) | mant + end + nonzero = false + precision += 1 + digits = zero(UInt32) + printedDigits = 0 + availableDigits = 0 + e = 0 + if e2 >= -52 + idx = e2 < 0 ? 0 : indexforexp(e2) + p10bits = pow10bitsforindex(idx) + len = lengthforindex(idx) + i = len - 1 + while i >= 0 + j = p10bits - e2 + #=@inbounds=# mula, mulb, mulc = POW10_SPLIT[POW10_OFFSET[idx + 1] + i + 1] + digits = mulshiftmod1e9(m2 << 8, mula, mulb, mulc, j + 8) + if !iszero(printedDigits) + if printedDigits + 9 > precision + availableDigits = 9 + break + end + pos = append_nine_digits(digits, buf, pos) + printedDigits += 9 + elseif !iszero(digits) + availableDigits = decimallength(digits) + e = i * 9 + availableDigits - 1 + if availableDigits > precision + break + end + if precision > 1 + pos = append_d_digits(availableDigits, digits, buf, pos, decchar) + else + @inbounds buf[pos] = UInt8('0') + digits + pos += 1 + if hash + @inbounds buf[pos] = decchar + pos += 1 + end + end + printedDigits = availableDigits + availableDigits = 0 + end + i -= 1 + end + end + if e2 < 0 && iszero(availableDigits) + idx = div(-e2, 16) + i = Int(MIN_BLOCK_2[idx + 1]) + while i < 200 + j = 120 + (-e2 - 16 * idx) + p = POW10_OFFSET_2[idx + 1] + i - MIN_BLOCK_2[idx + 1] + if p >= POW10_OFFSET_2[idx + 2] + digits = zero(UInt32) + else + #=@inbounds=# mula, mulb, mulc = POW10_SPLIT_2[p + 1] + digits = mulshiftmod1e9(m2 << 8, mula, mulb, mulc, j + 8) + end + if !iszero(printedDigits) + if printedDigits + 9 > precision + availableDigits = 9 + break + end + pos = append_nine_digits(digits, buf, pos) + printedDigits += 9 + elseif !iszero(digits) + availableDigits = decimallength(digits) + e = -(i + 1) * 9 + availableDigits - 1 + if availableDigits > precision + break + end + if precision > 1 + pos = append_d_digits(availableDigits, digits, buf, pos, decchar) + else + @inbounds buf[pos] = UInt8('0') + digits + pos += 1 + if hash + @inbounds buf[pos] = decchar + pos += 1 + end + end + printedDigits = availableDigits + availableDigits = 0 + end + i += 1 + end + end + maximum = precision - printedDigits + if iszero(availableDigits) + digits = zero(UInt32) + end + lastDigit = zero(UInt32) + if availableDigits > maximum + for k = 0:(availableDigits - maximum - 1) + lastDigit = digits % UInt32(10) + digits = div(digits, UInt32(10)) + end + end + roundUp = 0 + if lastDigit != 5 + roundUp = lastDigit > 5 ? 1 : 0 + else + rexp = precision - e + requiredTwos = -e2 - rexp + trailingZeros = requiredTwos <= 0 || + (requiredTwos < 60 && pow2(m2, requiredTwos)) + if rexp < 0 + requiredFives = -rexp + trailingZeros = trailingZeros & pow5(m2, requiredFives) + end + roundUp = trailingZeros ? 2 : 1 + end + if !iszero(printedDigits) + if iszero(digits) + for _ = 1:maximum + @inbounds buf[pos] = UInt8('0') + pos += 1 + end + else + pos = append_c_digits(maximum, digits, buf, pos) + end + else + if precision > 1 + pos = append_d_digits(maximum, digits, buf, pos, decchar) + else + @inbounds buf[pos] = UInt8('0') + digits + pos += 1 + if hash + @inbounds buf[pos] = decchar + pos += 1 + end + end + end + if !iszero(roundUp) + roundPos = pos + while true + roundPos -= 1 + if roundPos == (startpos - 1) || (@inbounds buf[roundPos]) == UInt8('-') || (plus && (@inbounds buf[roundPos]) == UInt8('+')) || (space && (@inbounds buf[roundPos]) == UInt8(' ')) + @inbounds buf[roundPos + 1] = UInt8('1') + e += 1 + break + end + c = roundPos > 0 ? (@inbounds buf[roundPos]) : 0x00 + if c == decchar + continue + elseif c == UInt8('9') + @inbounds buf[roundPos] = UInt8('0') + roundUp = 1 + continue + else + if roundUp == 2 && iseven(c) + break + end + @inbounds buf[roundPos] = c + 1 + break + end + end + end + if trimtrailingzeros + while @inbounds buf[pos - 1] == UInt8('0') + pos -= 1 + end + if @inbounds buf[pos - 1] == decchar && !hash + pos -= 1 + end + end + buf[pos] = expchar + pos += 1 + if e < 0 + @inbounds buf[pos] = UInt8('-') + pos += 1 + e = -e + else + @inbounds buf[pos] = UInt8('+') + pos += 1 + end + if e >= 100 + c = (e % 10) % UInt8 + @inbounds d100 = DIGIT_TABLE16[div(e, 10) + 1] + @inbounds buf[pos] = d100 % UInt8 + @inbounds buf[pos + 1] = (d100 >> 0x8) % UInt8 + @inbounds buf[pos + 2] = UInt8('0') + c + pos += 3 + else + @inbounds d100 = DIGIT_TABLE16[e + 1] + @inbounds buf[pos] = d100 % UInt8 + @inbounds buf[pos + 1] = (d100 >> 0x8) % UInt8 + pos += 2 + end + return pos +end diff --git a/src/ryu/fixed.jl b/src/ryu/fixed.jl new file mode 100644 index 0000000..eee31d3 --- /dev/null +++ b/src/ryu/fixed.jl @@ -0,0 +1,182 @@ +function writefixed(buf, pos, v::T, + precision=-1, plus=false, space=false, hash=false, + decchar=UInt8('.'), trimtrailingzeros=false) where {T <: IEEEFloat} + @assert 0 < pos <= length(buf) + startpos = pos + x = Float64(v) + pos = append_sign(x, plus, space, buf, pos) + + # special cases + if x == 0 + buf[pos] = UInt8('0') + pos += 1 + if precision > 0 && !trimtrailingzeros + buf[pos] = decchar + pos += 1 + for _ = 1:precision + buf[pos] = UInt8('0') + pos += 1 + end + elseif hash + buf[pos] = decchar + pos += 1 + end + return pos + elseif isnan(x) + buf[pos] = UInt8('N') + buf[pos + 1] = UInt8('a') + buf[pos + 2] = UInt8('N') + return pos + 3 + elseif !isfinite(x) + buf[pos] = UInt8('I') + buf[pos + 1] = UInt8('n') + buf[pos + 2] = UInt8('f') + return pos + 3 + end + + bits = reinterpret(UInt64, x) + mant = bits & MANTISSA_MASK + exp = Int((bits >> 52) & EXP_MASK) + + if exp == 0 # subnormal + e2 = 1 - 1023 - 52 + m2 = mant + else + e2 = exp - 1023 - 52 + m2 = (Int64(1) << 52) | mant + end + nonzero = false + if e2 >= -52 + idx = e2 < 0 ? 0 : indexforexp(e2) + p10bits = pow10bitsforindex(idx) + len = lengthforindex(idx) + i = len - 1 + while i >= 0 + j = p10bits - e2 + mula, mulb, mulc = POW10_SPLIT[POW10_OFFSET[idx + 1] + i + 1] + digits = mulshiftmod1e9(m2 << 8, mula, mulb, mulc, j + 8) + if nonzero + pos = append_nine_digits(digits, buf, pos) + elseif digits != 0 + olength = decimallength(digits) + pos = append_c_digits(olength, digits, buf, pos) + nonzero = true + end + i -= 1 + end + end + if !nonzero + buf[pos] = UInt8('0') + pos += 1 + end + hasfractional = false + if precision > 0 || hash + buf[pos] = decchar + pos += 1 + hasfractional = true + end + if e2 < 0 + idx = div(-e2, 16) + blocks = div(precision, 9) + 1 + roundUp = 0 + i = 0 + if blocks <= MIN_BLOCK_2[idx + 1] + i = blocks + for _ = 1:precision + buf[pos] = UInt8('0') + pos += 1 + end + elseif i < MIN_BLOCK_2[idx + 1] + i = MIN_BLOCK_2[idx + 1] + for _ = 1:(9 * i) + buf[pos] = UInt8('0') + pos += 1 + end + end + while i < blocks + j = 120 + (-e2 - 16 * idx) + p = POW10_OFFSET_2[idx + 1] + UInt32(i) - MIN_BLOCK_2[idx + 1] + if p >= POW10_OFFSET_2[idx + 2] + for _ = 1:(precision - 9 * i) + buf[pos] = UInt8('0') + pos += 1 + end + break + end + mula, mulb, mulc = POW10_SPLIT_2[p + 1] + digits = mulshiftmod1e9(m2 << 8, mula, mulb, mulc, j + 8) + if i < blocks - 1 + pos = append_nine_digits(digits, buf, pos) + else + maximum = precision - 9 * i + lastDigit = 0 + k = 0 + while k < 9 - maximum + # global digits, lastDigit, k + lastDigit = digits % 10 + digits = div(digits, 10) + k += 1 + end + if lastDigit != 5 + roundUp = lastDigit > 5 ? 1 : 0 + else + requiredTwos = -e2 - precision - 1 + trailingZeros = requiredTwos <= 0 || (requiredTwos < 60 && pow2(m2, requiredTwos)) + roundUp = trailingZeros ? 2 : 1 # 2 means round only if odd + end + if maximum > 0 + pos = append_c_digits(maximum, digits, buf, pos) + end + break + end + i += 1 + end + if roundUp != 0 + roundPos = pos + dotPos = 1 + while true + roundPos -= 1 + if roundPos == (startpos - 1) || (buf[roundPos] == UInt8('-')) || (plus && buf[roundPos] == UInt8('+')) || (space && buf[roundPos] == UInt8(' ')) + buf[pos] = UInt8('0') + buf[roundPos + 1] = UInt8('1') + if dotPos > 1 + buf[dotPos] = UInt8('0') + buf[dotPos + 1] = decchar + hasfractional = true + end + pos += 1 + break + end + c = roundPos > 0 ? buf[roundPos] : 0x00 + if c == decchar + dotPos = roundPos + continue + elseif c == UInt8('9') + buf[roundPos] = UInt8('0') + roundUp = 1 + continue + else + if roundUp == 2 && UInt8(c) % 2 == 0 + break + end + buf[roundPos] = c + 1 + break + end + end + end + else + for _ = 1:precision + buf[pos] = UInt8('0') + pos += 1 + end + end + if trimtrailingzeros && hasfractional + while buf[pos - 1] == UInt8('0') + pos -= 1 + end + if buf[pos - 1] == decchar && !hash + pos -= 1 + end + end + return pos +end diff --git a/src/ryu/shortest.jl b/src/ryu/shortest.jl new file mode 100644 index 0000000..7a1ada0 --- /dev/null +++ b/src/ryu/shortest.jl @@ -0,0 +1,461 @@ +""" + b, e10 = reduce_shortest(f[, maxsignif]) + +Reduce to shortest decimal representation where `abs(f) == b * 10^e10` and `b` is an +integer. If a `maxsignif` argument is provided, then `b < maxsignif`. +""" +@inline function reduce_shortest(f::T, maxsignif=nothing) where {T} + U = uinttype(T) + uf = reinterpret(U, f) + m = uf & significand_mask(T) + e = ((uf & exponent_mask(T)) >> significand_bits(T)) % Int + + ## Step 1 + # mf * 2^ef == f + mf = (one(U) << significand_bits(T)) | m + ef = e - exponent_bias(T) - significand_bits(T) + f_isinteger = mf & ((one(U) << -ef) - one(U)) == 0 + + if ef > 0 || ef < -significand_bits(T) || !f_isinteger + # fixup subnormals + if e == 0 + ef = 1 - exponent_bias(T) - significand_bits(T) + mf = m + end + + ## Step 2 + # u * 2^e2 == (f + prevfloat(f))/2 + # v * 2^e2 == f + # w * 2^e2 == (f + nextfloat(f))/2 + e2 = ef - 2 + mf_iseven = iseven(mf) # trailing bit of significand is zero + + v = U(4) * mf + w = v + U(2) + u_shift_half = m == 0 && e > 1 # if first element of binade, other than first normal one + u = v - U(2) + u_shift_half + + ## Step 3 + # a == floor(u * 2^e2 / 10^e10), exact if a_allzero + # b == floor(v * 2^e2 / 10^e10), exact if b_allzero + # c == floor(w * 2^e2 / 10^e10) + a_allzero = false + b_allzero = false + b_lastdigit = 0x00 + if e2 >= 0 + q = log10pow2(e2) - (T == Float64 ? (e2 > 3) : 0) + e10 = q + k = pow5_inv_bitcount(T) + pow5bits(q) - 1 + i = -e2 + q + k + a, b, c = mulshiftinvsplit(T, u, v, w, q, i) + if T == Float32 || T == Float16 + if q != 0 && div(c - 1, 10) <= div(a, 10) + l = pow5_inv_bitcount(T) + pow5bits(q - 1) - 1 + mul = pow5invsplit_lookup(T, q-1) + b_lastdigit = (mulshift(v, mul, -e2 + q - 1 + l) % 10) % UInt8 + end + end + if q <= qinvbound(T) + if ((v % UInt32) - 5 * div(v, 5)) == 0 + b_allzero = pow5(v, q) + elseif mf_iseven + a_allzero = pow5(u, q) + else + c -= pow5(w, q) + end + end + else + q = log10pow5(-e2) - (T == Float64 ? (-e2 > 1) : 0) + e10 = q + e2 + i = -e2 - q + k = pow5bits(i) - pow5_bitcount(T) + j = q - k + a, b, c = mulshiftsplit(T, u, v, w, i, j) + if T == Float32 || T == Float16 + if q != 0 && div(c - 1, 10) <= div(a, 10) + j = q - 1 - (pow5bits(i + 1) - pow5_bitcount(T)) + mul = pow5split_lookup(T, i+1) + b_lastdigit = (mulshift(v, mul, j) % 10) % UInt8 + end + end + if q <= 1 + b_allzero = true + if mf_iseven + a_allzero = !u_shift_half + else + c -= 1 + end + elseif q < qbound(T) + b_allzero = pow2(v, q - (T != Float64)) + end + end + + ## Step 4: reduction + if a_allzero || b_allzero + # a) slow loop + while true + c_div10 = div(c, 10) + a_div10 = div(a, 10) + if c_div10 <= a_div10 + break + end + a_mod10 = (a % UInt32) - UInt32(10) * (a_div10 % UInt32) + b_div10 = div(b, 10) + b_mod10 = (b % UInt32) - UInt32(10) * (b_div10 % UInt32) + a_allzero &= a_mod10 == 0 + b_allzero &= b_lastdigit == 0 + b_lastdigit = b_mod10 % UInt8 + b = b_div10 + c = c_div10 + a = a_div10 + e10 += 1 + end + if a_allzero + while true + a_div10 = div(a, 10) + a_mod10 = (a % UInt32) - UInt32(10) * (a_div10 % UInt32) + if a_mod10 != 0 && (maxsignif === nothing || b < maxsignif) + break + end + c_div10 = div(c, 10) + b_div10 = div(b, 10) + b_mod10 = (b % UInt32) - UInt32(10) * (b_div10 % UInt32) + b_allzero &= b_lastdigit == 0 + b_lastdigit = b_mod10 % UInt8 + b = b_div10 + c = c_div10 + a = a_div10 + e10 += 1 + end + end + if b_allzero && b_lastdigit == 5 && iseven(b) + b_lastdigit = UInt8(4) + end + roundup = (b == a && (!mf_iseven || !a_allzero)) || b_lastdigit >= 5 + else + # b) specialized for common case (99% Float64, 96% Float32) + roundup = b_lastdigit >= 5 + c_div100 = div(c, 100) + a_div100 = div(a, 100) + if c_div100 > a_div100 + b_div100 = div(b, 100) + b_mod100 = (b % UInt32) - UInt32(100) * (b_div100 % UInt32) + roundup = b_mod100 >= 50 + b = b_div100 + c = c_div100 + a = a_div100 + e10 += 2 + end + while true + c_div10 = div(c, 10) + a_div10 = div(a, 10) + if c_div10 <= a_div10 + break + end + b_div10 = div(b, 10) + b_mod10 = (b % UInt32) - UInt32(10) * (b_div10 % UInt32) + roundup = b_mod10 >= 5 + b = b_div10 + c = c_div10 + a = a_div10 + e10 += 1 + end + roundup = (b == a || roundup) + end + if maxsignif !== nothing && b > maxsignif + # reduce to max significant digits + while true + b_div10 = div(b, 10) + b_mod10 = (b % UInt32) - UInt32(10) * (b_div10 % UInt32) + if b <= maxsignif + break + end + b = b_div10 + roundup = (b_allzero && iseven(b)) ? b_mod10 > 5 : b_mod10 >= 5 + b_allzero &= b_mod10 == 0 + e10 += 1 + end + b = b + roundup + + # remove trailing zeros + while true + b_div10 = div(b, 10) + b_mod10 = (b % UInt32) - UInt32(10) * (b_div10 % UInt32) + if b_mod10 != 0 + break + end + b = b_div10 + e10 += 1 + end + else + b = b + roundup + end + else + # c) specialized f an integer < 2^53 + b = mf >> -ef + e10 = 0 + + if maxsignif !== nothing && b > maxsignif + roundup = false + b_allzero = true + # reduce to max significant digits + while true + b_div10 = div(b, 10) + b_mod10 = (b % UInt32) - UInt32(10) * (b_div10 % UInt32) + if b <= maxsignif + break + end + b = b_div10 + roundup = (b_allzero && iseven(b)) ? b_mod10 > 5 : b_mod10 >= 5 + b_allzero &= b_mod10 == 0 + e10 += 1 + end + b = b + roundup + end + while true + b_div10 = div(b, 10) + b_mod10 = (b % UInt32) - UInt32(10) * (b_div10 % UInt32) + if b_mod10 != 0 + break + end + b = b_div10 + e10 += 1 + end + end + return b, e10 +end + +function writeshortest(buf::AbstractVector{UInt8}, pos, x::T, + plus=false, space=false, hash=true, + precision=-1, expchar=UInt8('e'), padexp=false, decchar=UInt8('.'), + typed=false, compact=false) where {T} + @assert 0 < pos <= length(buf) + # special cases + if x == 0 + if typed && x isa Float16 + @inbounds buf[pos] = UInt8('F') + @inbounds buf[pos + 1] = UInt8('l') + @inbounds buf[pos + 2] = UInt8('o') + @inbounds buf[pos + 3] = UInt8('a') + @inbounds buf[pos + 4] = UInt8('t') + @inbounds buf[pos + 5] = UInt8('1') + @inbounds buf[pos + 6] = UInt8('6') + @inbounds buf[pos + 7] = UInt8('(') + pos += 8 + end + pos = append_sign(x, plus, space, buf, pos) + @inbounds buf[pos] = UInt8('0') + pos += 1 + if hash + @inbounds buf[pos] = decchar + pos += 1 + end + if precision == -1 + if hash + @inbounds buf[pos] = UInt8('0') + pos += 1 + end + if typed && x isa Float32 + @inbounds buf[pos] = UInt8('f') + @inbounds buf[pos + 1] = UInt8('0') + pos += 2 + end + if typed && x isa Float16 + @inbounds buf[pos] = UInt8(')') + pos += 1 + end + return pos + end + while hash && precision > 1 + @inbounds buf[pos] = UInt8('0') + pos += 1 + precision -= 1 + end + if typed && x isa Float32 + @inbounds buf[pos] = UInt8('f') + @inbounds buf[pos + 1] = UInt8('0') + pos += 2 + end + if typed && x isa Float16 + @inbounds buf[pos] = UInt8(')') + pos += 1 + end + return pos + elseif isnan(x) + pos = append_sign(x, plus, space, buf, pos) + @inbounds buf[pos] = UInt8('N') + @inbounds buf[pos + 1] = UInt8('a') + @inbounds buf[pos + 2] = UInt8('N') + if typed + if x isa Float32 + @inbounds buf[pos + 3] = UInt8('3') + @inbounds buf[pos + 4] = UInt8('2') + elseif x isa Float16 + @inbounds buf[pos + 3] = UInt8('1') + @inbounds buf[pos + 4] = UInt8('6') + end + end + return pos + 3 + (typed && x isa Union{Float32, Float16} ? 2 : 0) + elseif !isfinite(x) + pos = append_sign(x, plus, space, buf, pos) + @inbounds buf[pos] = UInt8('I') + @inbounds buf[pos + 1] = UInt8('n') + @inbounds buf[pos + 2] = UInt8('f') + if typed + if x isa Float32 + @inbounds buf[pos + 3] = UInt8('3') + @inbounds buf[pos + 4] = UInt8('2') + elseif x isa Float16 + @inbounds buf[pos + 3] = UInt8('1') + @inbounds buf[pos + 4] = UInt8('6') + end + end + return pos + 3 + (typed && x isa Union{Float32, Float16} ? 2 : 0) + end + + output, nexp = reduce_shortest(x, compact ? 999_999 : nothing) + + if typed && x isa Float16 + @inbounds buf[pos] = UInt8('F') + @inbounds buf[pos + 1] = UInt8('l') + @inbounds buf[pos + 2] = UInt8('o') + @inbounds buf[pos + 3] = UInt8('a') + @inbounds buf[pos + 4] = UInt8('t') + @inbounds buf[pos + 5] = UInt8('1') + @inbounds buf[pos + 6] = UInt8('6') + @inbounds buf[pos + 7] = UInt8('(') + pos += 8 + end + pos = append_sign(x, plus, space, buf, pos) + + olength = decimallength(output) + exp_form = true + pt = nexp + olength + if -4 < pt <= (precision == -1 ? (T == Float16 ? 3 : 6) : precision) && + !(pt >= olength && abs(mod(x + 0.05, 10^(pt - olength)) - 0.05) > 0.05) + exp_form = false + if pt <= 0 + @inbounds buf[pos] = UInt8('0') + pos += 1 + @inbounds buf[pos] = decchar + pos += 1 + for _ = 1:abs(pt) + @inbounds buf[pos] = UInt8('0') + pos += 1 + end + # elseif pt >= olength + # nothing to do at this point + # else + # nothing to do at this point + end + else + # make space for decchar + pos += 1 + end + + append_c_digits(olength, output, buf, pos) + + if !exp_form + if pt <= 0 + pos += olength + precision -= olength + elseif pt >= olength + pos += olength + precision -= olength + for _ = 1:nexp + @inbounds buf[pos] = UInt8('0') + pos += 1 + precision -= 1 + end + if hash + @inbounds buf[pos] = decchar + pos += 1 + if precision < 0 + @inbounds buf[pos] = UInt8('0') + pos += 1 + end + end + else + pointoff = olength - abs(nexp) + # shift bytes after pointoff to make room for decchar + buf_cconv = Base.cconvert(Ptr{UInt8}, buf) + GC.@preserve buf_cconv begin + ptr = Base.unsafe_convert(Ptr{UInt8}, buf_cconv) + Base.unsafe_copyto!(ptr + pos + pointoff, ptr + pos + pointoff - 1, olength - pointoff + 1) + end + @inbounds buf[pos + pointoff] = decchar + pos += olength + 1 + precision -= olength + end + if hash + while precision > 0 + @inbounds buf[pos] = UInt8('0') + pos += 1 + precision -= 1 + end + end + if typed && x isa Float32 + @inbounds buf[pos] = UInt8('f') + @inbounds buf[pos + 1] = UInt8('0') + pos += 2 + end + else + # move leading digit into place + @inbounds buf[pos - 1] = buf[pos] + if olength > 1 || hash + @inbounds buf[pos] = decchar + pos += olength + precision -= olength + end + if hash + if olength == 1 + @inbounds buf[pos] = UInt8('0') + pos += 1 + end + while precision > 0 + @inbounds buf[pos] = UInt8('0') + pos += 1 + precision -= 1 + end + end + + @inbounds buf[pos] = expchar + pos += 1 + exp2 = nexp + olength - 1 + if exp2 < 0 + @inbounds buf[pos] = UInt8('-') + pos += 1 + exp2 = -exp2 + elseif padexp + @inbounds buf[pos] = UInt8('+') + pos += 1 + end + + if exp2 >= 100 + c = exp2 % 10 + @inbounds d100 = DIGIT_TABLE16[(div(exp2, 10) % Int) + 1] + @inbounds buf[pos] = d100 % UInt8 + @inbounds buf[pos + 1] = (d100 >> 0x8) % UInt8 + @inbounds buf[pos + 2] = UInt8('0') + (c % UInt8) + pos += 3 + elseif exp2 >= 10 + @inbounds d100 = DIGIT_TABLE16[(exp2 % Int) + 1] + @inbounds buf[pos] = d100 % UInt8 + @inbounds buf[pos + 1] = (d100 >> 0x8) % UInt8 + pos += 2 + else + if padexp + @inbounds buf[pos] = UInt8('0') + pos += 1 + end + @inbounds buf[pos] = UInt8('0') + (exp2 % UInt8) + pos += 1 + end + end + if typed && x isa Float16 + @inbounds buf[pos] = UInt8(')') + pos += 1 + end + + return pos +end diff --git a/src/ryu/utils.jl b/src/ryu/utils.jl new file mode 100644 index 0000000..5493e9f --- /dev/null +++ b/src/ryu/utils.jl @@ -0,0 +1,313 @@ +const MANTISSA_MASK = significand_mask(Float64) +const EXP_MASK = exponent_mask(Float64) >> significand_bits(Float64) + +# Note: these are smaller than the values given in Figure 4 from the paper +# see https://github.com/ulfjack/ryu/issues/119 +pow5_bitcount(::Type{Float16}) = 30 +pow5_bitcount(::Type{Float32}) = 61 +pow5_bitcount(::Type{Float64}) = 121 + +pow5_inv_bitcount(::Type{Float16}) = 30 +pow5_inv_bitcount(::Type{Float32}) = 59 +pow5_inv_bitcount(::Type{Float64}) = 122 + +qinvbound(::Type{Float16}) = 4 +qinvbound(::Type{Float32}) = 9 +qinvbound(::Type{Float64}) = 21 + +qbound(::Type{Float16}) = 15 +qbound(::Type{Float32}) = 31 +qbound(::Type{Float64}) = 63 + +""" + Ryu.log10pow2(e::Integer) + +Computes `floor(log10(2^e))`. This is valid for all `e < 1651`. +""" +log10pow2(e) = (e * 78913) >> 18 + + +""" + Ryu.log10pow5(e::Integer) + +Computes `floor(log10(5^e))`. This is valid for all `e < 2621`. +""" +log10pow5(e) = (e * 732923) >> 20 + +""" + Ryu.pow5bits(e) + +Computes `e == 0 ? 1 : ceil(log2(5^e))`. This is valid for `e < 3529` (if performend in `Int32` arithmetic). +""" +pow5bits(e) = ((e * 1217359) >> 19) + 1 + +"""" + Ryu.mulshift(m::U, mula, j) where {U<:Unsigned} + +Compute `(m * mul) >> j`, where `j >= 8*sizeof(U)`. The type of the results is the larger of `U` or `UInt32`. +""" +function mulshift(m::U, mul, j) where {U<:Unsigned} + W = widen(U) + nbits = 8*sizeof(U) + return ((((W(m) * (mul % U)) >> nbits) + W(m) * (mul >> nbits)) >> (j - nbits)) % promote_type(U,UInt32) +end + +indexforexp(e) = div(e + 15, 16) +pow10bitsforindex(idx) = 16 * idx + 120 +lengthforindex(idx) = div(((Int64(16 * idx) * 1292913986) >> 32) + 1 + 16 + 8, 9) + +""" + Ryu.pow5(x, p) + +Return `true` if `5^p` is a divisor of `x`. +""" +pow5(x, p) = x % (UInt64(5)^p) == 0 + +""" + Ryu.pow2(x, p) + +Return `true` if `2^p` is a divisor of `x`. In other words, if the trailing `p` bits of `x` are zero. +""" +pow2(x, p) = (x & ((Int64(1) << p) - 1)) == 0 + +""" + Ryu.decimallength(v) + +The number of decimal digits of the integer `v`. +""" +function decimallength(v) + v >= 10000000000000000 && return 17 + v >= 1000000000000000 && return 16 + v >= 100000000000000 && return 15 + v >= 10000000000000 && return 14 + v >= 1000000000000 && return 13 + v >= 100000000000 && return 12 + v >= 10000000000 && return 11 + v >= 1000000000 && return 10 + v >= 100000000 && return 9 + v >= 10000000 && return 8 + v >= 1000000 && return 7 + v >= 100000 && return 6 + v >= 10000 && return 5 + v >= 1000 && return 4 + v >= 100 && return 3 + v >= 10 && return 2 + return 1 +end +function decimallength(v::UInt32) + v >= 100000000 && return 9 + v >= 10000000 && return 8 + v >= 1000000 && return 7 + v >= 100000 && return 6 + v >= 10000 && return 5 + v >= 1000 && return 4 + v >= 100 && return 3 + v >= 10 && return 2 + return 1 +end +function decimallength(v::UInt16) + v >= 10000 && return 5 + v >= 1000 && return 4 + v >= 100 && return 3 + v >= 10 && return 2 + return 1 +end + +function mulshiftinvsplit(::Type{T}, mv, mp, mm, i, j) where {T} + mul = pow5invsplit_lookup(T, i) + vr = mulshift(mv, mul, j) + vp = mulshift(mp, mul, j) + vm = mulshift(mm, mul, j) + return vr, vp, vm +end + +function mulshiftsplit(::Type{T}, mv, mp, mm, i, j) where {T} + mul = pow5split_lookup(T, i) + vr = mulshift(mv, mul, j) + vp = mulshift(mp, mul, j) + vm = mulshift(mm, mul, j) + return vr, vp, vm +end + +""" + Ryu.umul256(a::UInt128, bHi::UInt64, bLo::UInt64)::Tuple{UInt128, UInt128} + +Compute `p = a*b` where `b = bLo + bHi<<64`, returning the result as `pLo, pHi` where `p = pLo + pHi<<128`. +""" +function umul256(a::UInt128, bHi::UInt64, bLo::UInt64) + aLo = a % UInt64 + aHi = (a >> 64) % UInt64 + + b00 = UInt128(aLo) * bLo + b01 = UInt128(aLo) * bHi + b10 = UInt128(aHi) * bLo + b11 = UInt128(aHi) * bHi + + b00Lo = b00 % UInt64 + b00Hi = (b00 >> 64) % UInt64 + + mid1 = b10 + b00Hi + mid1Lo = mid1 % UInt64 + mid1Hi = (mid1 >> 64) % UInt64 + + mid2 = b01 + mid1Lo + mid2Lo = mid2 % UInt64 + mid2Hi = (mid2 >> 64) % UInt64 + + pHi = b11 + mid1Hi + mid2Hi + pLo = (UInt128(mid2Lo) << 64) | b00Lo + return pLo, pHi +end + +""" + Ryu.umul256_hi(a::UInt128, bHi::UInt64, bLo::UInt64)::UInt128 + +Compute `pHi = (a*b)>>128` where `b = bLo + bHi<<64`. +""" +umul256_hi(a::UInt128, bHi::UInt64, bLo::UInt64) = umul256(a, bHi, bLo)[2] + +""" + Ryu.mulshiftmod1e9(m, mula, mulb, mulc, j)::UInt32 + +Compute `(m * mul) >> j % 10^9` where `mul = mula + mulb<<64 + mulc<<128`, and `j >= 128`. +""" +function mulshiftmod1e9(m, mula, mulb, mulc, j) + b0 = UInt128(m) * mula + b1 = UInt128(m) * mulb + b2 = UInt128(m) * mulc + mid = b1 + ((b0 >> 64) % UInt64) + s1 = b2 + ((mid >> 64) % UInt64) + v = s1 >> (j - 128) + multiplied = umul256_hi(v, 0x89705F4136B4A597, 0x31680A88F8953031) + shifted = (multiplied >> 29) % UInt32 + return (v % UInt32) - UInt32(1000000000) * shifted +end + +function append_sign(x, plus::Bool, space::Bool, buf, pos::Int) + if signbit(x) && !isnan(x) # suppress minus sign for signaling NaNs + buf[pos] = UInt8('-') + pos += 1 + elseif plus + buf[pos] = UInt8('+') + pos += 1 + elseif space + buf[pos] = UInt8(' ') + pos += 1 + end + return pos +end + +function append_d_digits(olength::Int, digits::Unsigned, buf, pos::Int, decchar) + newpos = append_c_digits(olength, digits, buf, pos + 1) + @inbounds buf[pos] = buf[pos + 1] + @inbounds buf[pos + 1] = decchar + return newpos # == pos + olength + 1 +end + +const BIG_MASK = (big(1) << 64) - 1 + +const POW10_SPLIT = collect(Iterators.flatten(map(0:63) do idx + pow10bits = pow10bitsforindex(idx) + map(0:lengthforindex(idx)-1) do i + v = (div(big(1) << pow10bits, big(10)^(9 * i)) + 1) % ((big(10)^9) << 136) + return (UInt64(v & BIG_MASK), UInt64((v >> 64) & BIG_MASK), UInt64((v >> 128) & BIG_MASK)) + end +end)) + +function generateinversetables() + POW10_OFFSET_2 = Vector{UInt16}(undef, 68 + 1) + MIN_BLOCK_2 = fill(0xff, 68 + 1) + POW10_SPLIT_2 = Tuple{UInt64, UInt64, UInt64}[] + lowerCutoff = big(1) << (54 + 8) + for idx = 0:67 + POW10_OFFSET_2[idx + 1] = length(POW10_SPLIT_2) + i = 0 + while true + v = ((big(10)^(9 * (i + 1)) >> (-(120 - 16 * idx))) % (big(10)^9) << (120 + 16)) + if MIN_BLOCK_2[idx + 1] == 0xff && ((v * lowerCutoff) >> 128) == 0 + i += 1 + continue + end + if MIN_BLOCK_2[idx + 1] == 0xff + MIN_BLOCK_2[idx + 1] = i + end + v == 0 && break + push!(POW10_SPLIT_2, ((v & BIG_MASK) % UInt64, ((v >> 64) & BIG_MASK) % UInt64, ((v >> 128) & BIG_MASK) % UInt64)) + i += 1 + end + end + POW10_OFFSET_2[end] = length(POW10_SPLIT_2) + MIN_BLOCK_2[end] = 0x00 + + return POW10_OFFSET_2, MIN_BLOCK_2, POW10_SPLIT_2 +end + +const POW10_OFFSET_2, MIN_BLOCK_2, POW10_SPLIT_2 = generateinversetables() + +""" + Ryu.pow5invsplit(T, i) + +Compute `floor(2^k/5^i)+1`, where `k = pow5bits(i) - 1 + pow5_inv_bitcount(T)`. The result +is an unsigned integer twice as wide as `T` (i.e. a `UInt128` if `T == Float64`), with +`pow5_inv_bitcount(T)` significant bits. +""" +function pow5invsplit(::Type{T}, i) where {T<:AbstractFloat} + W = widen(uinttype(T)) + pow = big(5)^i + inv = div(big(1) << (ndigits(pow, base=2) - 1 + pow5_inv_bitcount(T)), pow) + 1 + return W(inv) +end + +""" + Ryu.pow5invsplit_lookup(T, i) + +[`pow5invsplit`](@ref) computed via lookup table. +""" +function pow5invsplit_lookup end +for T in (Float64, Float32, Float16) + e2_max = exponent_max(T) - precision(T) - 1 + i_max = log10pow2(e2_max) + table_sym = Symbol("pow5invsplit_table_", string(T)) + @eval const $table_sym = Tuple(Any[pow5invsplit($T, i) for i = 0:$i_max]) + @eval pow5invsplit_lookup(::Type{$T}, i) = @inbounds($table_sym[i+1]) +end + + +""" + Ryu.pow5split(T, i) + +Compute `floor(5^i/2^k)`, where `k = pow5bits(i) - pow5_bitcount(T)`. The result is an +unsigned integer twice as wide as `T` (i.e. a `UInt128` if `T == Float64`), with +`pow5_bitcount(T)` significant bits. +""" +function pow5split(::Type{T}, i) where {T<:AbstractFloat} + W = widen(uinttype(T)) + pow = big(5)^i + return W(pow >> (ndigits(pow, base=2) - pow5_bitcount(T))) +end + +""" + Ryu.pow5split_lookup(T, i) + +[`pow5split`](@ref) computed via lookup table. +""" +function pow5split_lookup end +for T in (Float64, Float32, Float16) + e2_min = 1 - exponent_bias(T) - significand_bits(T) - 2 + i_max = 1 - e2_min - log10pow5(-e2_min) + table_sym = Symbol("pow5split_table_", string(T)) + @eval const $table_sym = Tuple(Any[pow5split($T, i) for i = 0:$i_max]) + @eval pow5split_lookup(::Type{$T}, i) = @inbounds($table_sym[i+1]) +end + +const DIGIT_TABLE16 = _dec_d100 + +const POW10_OFFSET = UInt16[ + 0, 2, 5, 8, 12, 16, 21, 26, 32, 39, + 46, 54, 62, 71, 80, 90, 100, 111, 122, 134, + 146, 159, 173, 187, 202, 217, 233, 249, 266, 283, + 301, 319, 338, 357, 377, 397, 418, 440, 462, 485, + 508, 532, 556, 581, 606, 632, 658, 685, 712, 740, + 769, 798, 828, 858, 889, 920, 952, 984, 1017, 1050, + 1084, 1118, 1153, 1188 +] diff --git a/src/write.jl b/src/write.jl index ef99f05..7cacba4 100644 --- a/src/write.jl +++ b/src/write.jl @@ -725,14 +725,14 @@ function _number(buf, pos, x::Number, opts::WriteOptions, io, bufsize) return pos + sizeof(inf) end if opts.float_style == :shortest - @checkn Base.Ryu.neededdigits(typeof(x)) - return Base.Ryu.writeshortest(buf, pos, x) + @checkn Ryu.neededdigits(typeof(x)) + return Ryu.writeshortest(buf, pos, x) elseif opts.float_style == :fixed - @checkn (opts.float_precision + Base.Ryu.neededdigits(typeof(x))) - return Base.Ryu.writefixed(buf, pos, x, opts.float_precision, false, false, true) + @checkn (opts.float_precision + Ryu.neededdigits(typeof(x))) + return Ryu.writefixed(buf, pos, x, opts.float_precision, false, false, true) elseif opts.float_style == :exp - @checkn (opts.float_precision + Base.Ryu.neededdigits(typeof(x))) - return Base.Ryu.writeexp(buf, pos, x, opts.float_precision, false, false, true) + @checkn (opts.float_precision + Ryu.neededdigits(typeof(x))) + return Ryu.writeexp(buf, pos, x, opts.float_precision, false, false, true) else # unreachable as we validate float_style inputs @assert false diff --git a/test/runtests.jl b/test/runtests.jl index 298d035..3d2436e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,6 +5,7 @@ include(joinpath(dirname(pathof(JSON)), "../test/lazy.jl")) include(joinpath(dirname(pathof(JSON)), "../test/parse.jl")) include(joinpath(dirname(pathof(JSON)), "../test/json.jl")) include(joinpath(dirname(pathof(JSON)), "../test/arrow.jl")) +include(joinpath(dirname(pathof(JSON)), "../test/ryu.jl")) function tar_files(tarball::String) data = Dict{String, Vector{UInt8}}() diff --git a/test/ryu.jl b/test/ryu.jl new file mode 100644 index 0000000..8fc2569 --- /dev/null +++ b/test/ryu.jl @@ -0,0 +1,905 @@ +# This file was copied from (test/ryu.jl) in the https://github.com/JuliaLang/julia repository. + +# The Julia license is MIT: https://julialang.org/license + +# Note: The Ryu submodule is not public +using JSON.Ryu + +test_array_types = if VERSION < v"1.11" + (Vector{UInt8},) +else + (Vector{UInt8}, Memory{UInt8}) +end + +const maxMantissa = (UInt64(1) << 53) - 1 +todouble(sign, exp, mant) = Core.bitcast(Float64, (UInt64(sign) << 63) | (UInt64(exp) << 52) | (UInt64(mant))) + +@testset "Ryu" begin + +@testset "Float64" begin + +@testset "Basic" begin + @test Ryu.writeshortest(0.0) == "0.0" + @test Ryu.writeshortest(-0.0) == "-0.0" + @test Ryu.writeshortest(1.0) == "1.0" + @test Ryu.writeshortest(-1.0) == "-1.0" + @test Ryu.writeshortest(NaN) == "NaN" + @test Ryu.writeshortest(Inf) == "Inf" + @test Ryu.writeshortest(-Inf) == "-Inf" +end + +@testset "OutputOptions" begin + # plus + @test "+1" == Ryu.writeshortest(1.0, true, false, false) + @test "-1" == Ryu.writeshortest(-1.0, true, false, false) + + # space + @test " 1" == Ryu.writeshortest(1.0, false, true, false) + + # hash + @test "0" == Ryu.writeshortest(0.0, false, false, false) + + # precision + @test "9.9900" == Ryu.writeshortest(9.99, false, false, true, 5) + @test "1." == Ryu.writeshortest(1.0, false, false, true, 1) + + # expchar + @test "1.0d6" == Ryu.writeshortest(1e6, false, false, true, -1, UInt8('d')) + + # padexp + @test "3.0e+08" == Ryu.writeshortest(3e8, false, false, true, -1, UInt8('e'), true) + + # decchar + @test "3,14" == Ryu.writeshortest(3.14, false, false, true, -1, UInt8('e'), false, UInt8(',')) + + # compact + @test "0.333333" == Ryu.writeshortest(1/3, false, false, true, -1, UInt8('e'), false, UInt8('.'), false, true) +end + +@testset "SwitchToSubnormal" begin + @test "2.2250738585072014e-308" == Ryu.writeshortest(2.2250738585072014e-308) +end + +@testset "MinAndMax" begin + @test "1.7976931348623157e308" == Ryu.writeshortest(Core.bitcast(Float64, 0x7fefffffffffffff)) + @test "5.0e-324" == Ryu.writeshortest(Core.bitcast(Float64, Int64(1))) +end + +@testset "LotsOfTrailingZeros" begin + @test "2.9802322387695312e-8" == Ryu.writeshortest(2.98023223876953125e-8) +end + +@testset "Regression" begin + @test "-2.109808898695963e16" == Ryu.writeshortest(-2.109808898695963e16) + @test "4.940656e-318" == Ryu.writeshortest(4.940656e-318) + @test "1.18575755e-316" == Ryu.writeshortest(1.18575755e-316) + @test "2.989102097996e-312" == Ryu.writeshortest(2.989102097996e-312) + @test "9.0608011534336e15" == Ryu.writeshortest(9.0608011534336e15) + @test "4.708356024711512e18" == Ryu.writeshortest(4.708356024711512e18) + @test "9.409340012568248e18" == Ryu.writeshortest(9.409340012568248e18) + @test "1.2345678" == Ryu.writeshortest(1.2345678) +end + +@testset "LooksLikePow5" begin + # These numbers have a mantissa that is a multiple of the largest power of 5 that fits, + # and an exponent that causes the computation for q to result in 22, which is a corner + # case for Ryu. + @test "5.764607523034235e39" == Ryu.writeshortest(Core.bitcast(Float64, 0x4830F0CF064DD592)) + @test "1.152921504606847e40" == Ryu.writeshortest(Core.bitcast(Float64, 0x4840F0CF064DD592)) + @test "2.305843009213694e40" == Ryu.writeshortest(Core.bitcast(Float64, 0x4850F0CF064DD592)) +end + +@testset "pow5 overflow (#47464)" begin + @test "4.6458339e+63" == Ryu.writeexp(4.645833859177319e63, 7) + @test "4.190673780e+40" == Ryu.writeexp(4.190673779576499e40, 9) +end + +@testset "OutputLength" begin + @test "1.0" == Ryu.writeshortest(1.0) # already tested in Basic + @test "1.2" == Ryu.writeshortest(1.2) + @test "1.23" == Ryu.writeshortest(1.23) + @test "1.234" == Ryu.writeshortest(1.234) + @test "1.2345" == Ryu.writeshortest(1.2345) + @test "1.23456" == Ryu.writeshortest(1.23456) + @test "1.234567" == Ryu.writeshortest(1.234567) + @test "1.2345678" == Ryu.writeshortest(1.2345678) # already tested in Regressi + @test "1.23456789" == Ryu.writeshortest(1.23456789) + @test "1.234567895" == Ryu.writeshortest(1.234567895) # 1.234567890 would be trimm + @test "1.2345678901" == Ryu.writeshortest(1.2345678901) + @test "1.23456789012" == Ryu.writeshortest(1.23456789012) + @test "1.234567890123" == Ryu.writeshortest(1.234567890123) + @test "1.2345678901234" == Ryu.writeshortest(1.2345678901234) + @test "1.23456789012345" == Ryu.writeshortest(1.23456789012345) + @test "1.234567890123456" == Ryu.writeshortest(1.234567890123456) + @test "1.2345678901234567" == Ryu.writeshortest(1.2345678901234567) + + # Test 32-bit chunking + @test "4.294967294" == Ryu.writeshortest(4.294967294) # 2^32 - + @test "4.294967295" == Ryu.writeshortest(4.294967295) # 2^32 - + @test "4.294967296" == Ryu.writeshortest(4.294967296) # 2^ + @test "4.294967297" == Ryu.writeshortest(4.294967297) # 2^32 + + @test "4.294967298" == Ryu.writeshortest(4.294967298) # 2^32 + +end + +# Test min, max shift values in shiftright128 +@testset "MinMaxShift" begin + # 32-bit opt-size=0: 49 <= dist <= 50 + # 32-bit opt-size=1: 30 <= dist <= 50 + # 64-bit opt-size=0: 50 <= dist <= 50 + # 64-bit opt-size=1: 30 <= dist <= 50 + @test "1.7800590868057611e-307" == Ryu.writeshortest(todouble(false, 4, 0)) + # 32-bit opt-size=0: 49 <= dist <= 49 + # 32-bit opt-size=1: 28 <= dist <= 49 + # 64-bit opt-size=0: 50 <= dist <= 50 + # 64-bit opt-size=1: 28 <= dist <= 50 + @test "2.8480945388892175e-306" == Ryu.writeshortest(todouble(false, 6, maxMantissa)) + # 32-bit opt-size=0: 52 <= dist <= 53 + # 32-bit opt-size=1: 2 <= dist <= 53 + # 64-bit opt-size=0: 53 <= dist <= 53 + # 64-bit opt-size=1: 2 <= dist <= 53 + @test "2.446494580089078e-296" == Ryu.writeshortest(todouble(false, 41, 0)) + # 32-bit opt-size=0: 52 <= dist <= 52 + # 32-bit opt-size=1: 2 <= dist <= 52 + # 64-bit opt-size=0: 53 <= dist <= 53 + # 64-bit opt-size=1: 2 <= dist <= 53 + @test "4.8929891601781557e-296" == Ryu.writeshortest(todouble(false, 40, maxMantissa)) + + # 32-bit opt-size=0: 57 <= dist <= 58 + # 32-bit opt-size=1: 57 <= dist <= 58 + # 64-bit opt-size=0: 58 <= dist <= 58 + # 64-bit opt-size=1: 58 <= dist <= 58 + @test "1.8014398509481984e16" == Ryu.writeshortest(todouble(false, 1077, 0)) + # 32-bit opt-size=0: 57 <= dist <= 57 + # 32-bit opt-size=1: 57 <= dist <= 57 + # 64-bit opt-size=0: 58 <= dist <= 58 + # 64-bit opt-size=1: 58 <= dist <= 58 + @test "3.6028797018963964e16" == Ryu.writeshortest(todouble(false, 1076, maxMantissa)) + # 32-bit opt-size=0: 51 <= dist <= 52 + # 32-bit opt-size=1: 51 <= dist <= 59 + # 64-bit opt-size=0: 52 <= dist <= 52 + # 64-bit opt-size=1: 52 <= dist <= 59 + @test "2.900835519859558e-216" == Ryu.writeshortest(todouble(false, 307, 0)) + # 32-bit opt-size=0: 51 <= dist <= 51 + # 32-bit opt-size=1: 51 <= dist <= 59 + # 64-bit opt-size=0: 52 <= dist <= 52 + # 64-bit opt-size=1: 52 <= dist <= 59 + @test "5.801671039719115e-216" == Ryu.writeshortest(todouble(false, 306, maxMantissa)) + + # https:#github.com/ulfjack/ryu/commit/19e44d16d80236f5de25800f56d82606d1be00b9#commitcomment-30146483 + # 32-bit opt-size=0: 49 <= dist <= 49 + # 32-bit opt-size=1: 44 <= dist <= 49 + # 64-bit opt-size=0: 50 <= dist <= 50 + # 64-bit opt-size=1: 44 <= dist <= 50 + @test "3.196104012172126e-27" == Ryu.writeshortest(todouble(false, 934, 0x000FA7161A4D6E0C)) +end + +@testset "SmallIntegers" begin + @test "9.007199254740991e15" == Ryu.writeshortest(9007199254740991.0) + @test "9.007199254740992e15" == Ryu.writeshortest(9007199254740992.0) + + @test "1.0" == Ryu.writeshortest(1.0e+0) + @test "12.0" == Ryu.writeshortest(1.2e+1) + @test "123.0" == Ryu.writeshortest(1.23e+2) + @test "1234.0" == Ryu.writeshortest(1.234e+3) + @test "12345.0" == Ryu.writeshortest(1.2345e+4) + @test "123456.0" == Ryu.writeshortest(1.23456e+5) + @test "1.234567e6" == Ryu.writeshortest(1.234567e+6) + @test "1.2345678e7" == Ryu.writeshortest(1.2345678e+7) + @test "1.23456789e8" == Ryu.writeshortest(1.23456789e+8) + @test "1.23456789e9" == Ryu.writeshortest(1.23456789e+9) + @test "1.234567895e9" == Ryu.writeshortest(1.234567895e+9) + @test "1.2345678901e10" == Ryu.writeshortest(1.2345678901e+10) + @test "1.23456789012e11" == Ryu.writeshortest(1.23456789012e+11) + @test "1.234567890123e12" == Ryu.writeshortest(1.234567890123e+12) + @test "1.2345678901234e13" == Ryu.writeshortest(1.2345678901234e+13) + @test "1.23456789012345e14" == Ryu.writeshortest(1.23456789012345e+14) + @test "1.234567890123456e15" == Ryu.writeshortest(1.234567890123456e+15) + + # 10^i + @test "1.0" == Ryu.writeshortest(1.0e+0) + @test "10.0" == Ryu.writeshortest(1.0e+1) + @test "100.0" == Ryu.writeshortest(1.0e+2) + @test "1000.0" == Ryu.writeshortest(1.0e+3) + @test "10000.0" == Ryu.writeshortest(1.0e+4) + @test "100000.0" == Ryu.writeshortest(1.0e+5) + @test "1.0e6" == Ryu.writeshortest(1.0e+6) + @test "1.0e7" == Ryu.writeshortest(1.0e+7) + @test "1.0e8" == Ryu.writeshortest(1.0e+8) + @test "1.0e9" == Ryu.writeshortest(1.0e+9) + @test "1.0e10" == Ryu.writeshortest(1.0e+10) + @test "1.0e11" == Ryu.writeshortest(1.0e+11) + @test "1.0e12" == Ryu.writeshortest(1.0e+12) + @test "1.0e13" == Ryu.writeshortest(1.0e+13) + @test "1.0e14" == Ryu.writeshortest(1.0e+14) + @test "1.0e15" == Ryu.writeshortest(1.0e+15) + + # 10^15 + 10^i + @test "1.000000000000001e15" == Ryu.writeshortest(1.0e+15 + 1.0e+0) + @test "1.00000000000001e15" == Ryu.writeshortest(1.0e+15 + 1.0e+1) + @test "1.0000000000001e15" == Ryu.writeshortest(1.0e+15 + 1.0e+2) + @test "1.000000000001e15" == Ryu.writeshortest(1.0e+15 + 1.0e+3) + @test "1.00000000001e15" == Ryu.writeshortest(1.0e+15 + 1.0e+4) + @test "1.0000000001e15" == Ryu.writeshortest(1.0e+15 + 1.0e+5) + @test "1.000000001e15" == Ryu.writeshortest(1.0e+15 + 1.0e+6) + @test "1.00000001e15" == Ryu.writeshortest(1.0e+15 + 1.0e+7) + @test "1.0000001e15" == Ryu.writeshortest(1.0e+15 + 1.0e+8) + @test "1.000001e15" == Ryu.writeshortest(1.0e+15 + 1.0e+9) + @test "1.00001e15" == Ryu.writeshortest(1.0e+15 + 1.0e+10) + @test "1.0001e15" == Ryu.writeshortest(1.0e+15 + 1.0e+11) + @test "1.001e15" == Ryu.writeshortest(1.0e+15 + 1.0e+12) + @test "1.01e15" == Ryu.writeshortest(1.0e+15 + 1.0e+13) + @test "1.1e15" == Ryu.writeshortest(1.0e+15 + 1.0e+14) + + # Largest power of 2 <= 10^(i+1) + @test "8.0" == Ryu.writeshortest(8.0) + @test "64.0" == Ryu.writeshortest(64.0) + @test "512.0" == Ryu.writeshortest(512.0) + @test "8192.0" == Ryu.writeshortest(8192.0) + @test "65536.0" == Ryu.writeshortest(65536.0) + @test "524288.0" == Ryu.writeshortest(524288.0) + @test "8.388608e6" == Ryu.writeshortest(8388608.0) + @test "6.7108864e7" == Ryu.writeshortest(67108864.0) + @test "5.36870912e8" == Ryu.writeshortest(536870912.0) + @test "8.589934592e9" == Ryu.writeshortest(8589934592.0) + @test "6.8719476736e10" == Ryu.writeshortest(68719476736.0) + @test "5.49755813888e11" == Ryu.writeshortest(549755813888.0) + @test "8.796093022208e12" == Ryu.writeshortest(8796093022208.0) + @test "7.0368744177664e13" == Ryu.writeshortest(70368744177664.0) + @test "5.62949953421312e14" == Ryu.writeshortest(562949953421312.0) + @test "9.007199254740992e15" == Ryu.writeshortest(9007199254740992.0) + + # 1000 * (Largest power of 2 <= 10^(i+1)) + @test "8000.0" == Ryu.writeshortest(8.0e+3) + @test "64000.0" == Ryu.writeshortest(64.0e+3) + @test "512000.0" == Ryu.writeshortest(512.0e+3) + @test "8.192e6" == Ryu.writeshortest(8192.0e+3) + @test "6.5536e7" == Ryu.writeshortest(65536.0e+3) + @test "5.24288e8" == Ryu.writeshortest(524288.0e+3) + @test "8.388608e9" == Ryu.writeshortest(8388608.0e+3) + @test "6.7108864e10" == Ryu.writeshortest(67108864.0e+3) + @test "5.36870912e11" == Ryu.writeshortest(536870912.0e+3) + @test "8.589934592e12" == Ryu.writeshortest(8589934592.0e+3) + @test "6.8719476736e13" == Ryu.writeshortest(68719476736.0e+3) + @test "5.49755813888e14" == Ryu.writeshortest(549755813888.0e+3) + @test "8.796093022208e15" == Ryu.writeshortest(8796093022208.0e+3) +end + +end # Float64 + +@testset "Float32" begin + +@testset "Basic" begin + @test "0.0" == Ryu.writeshortest(Float32(0.0)) + @test "-0.0" == Ryu.writeshortest(Float32(-0.0)) + @test "1.0" == Ryu.writeshortest(Float32(1.0)) + @test "-1.0" == Ryu.writeshortest(Float32(-1.0)) + @test "NaN" == Ryu.writeshortest(Float32(NaN)) + @test "Inf" == Ryu.writeshortest(Float32(Inf)) + @test "-Inf" == Ryu.writeshortest(Float32(-Inf)) +end + +@testset "OutputOptions" begin + # typed + @test "1.0f0" == Ryu.writeshortest(Float32(1.0), false, false, true, -1, UInt8('e'), false, UInt8('.'), true) + @test "Inf32" == Ryu.writeshortest(Float32(Inf), false, false, true, -1, UInt8('e'), false, UInt8('.'), true) + @test "NaN32" == Ryu.writeshortest(Float32(NaN), false, false, true, -1, UInt8('e'), false, UInt8('.'), true) + @test "3.14f0" == Ryu.writeshortest(Float32(3.14), false, false, true, -1, UInt8('e'), false, UInt8('.'), true) + + # typed and no-hash + @test "1f0" == Ryu.writeshortest(1.0f0, false, false, false, -1, UInt8('e'), false, UInt8('.'), true) +end + +@testset "SwitchToSubnormal" begin + @test "1.1754944e-38" == Ryu.writeshortest(1.1754944f-38) +end + +@testset "MinAndMax" begin + @test "3.4028235e38" == Ryu.writeshortest(Core.bitcast(Float32, 0x7f7fffff)) + @test "1.0e-45" == Ryu.writeshortest(Core.bitcast(Float32, Int32(1))) +end + +# Check that we return the exact boundary if it is the shortest +# representation, but only if the original floating point number is even. +@testset "BoundaryRoundeven" begin + @test "3.355445e7" == Ryu.writeshortest(3.355445f7) + @test "9.0e9" == Ryu.writeshortest(8.999999f9) + @test "3.436672e10" == Ryu.writeshortest(3.4366717f10) +end + +# If the exact value is exactly halfway between two shortest representations, +# then we round to even. It seems like this only makes a difference if the +# last two digits are ...2|5 or ...7|5, and we cut off the 5. +@testset "exactValueRoundeven" begin + @test "305404.12" == Ryu.writeshortest(3.0540412f5) + @test "8099.0312" == Ryu.writeshortest(8.0990312f3) +end + +@testset "LotsOfTrailingZeros" begin + # Pattern for the first test: 00111001100000000000000000000000 + @test "0.00024414062" == Ryu.writeshortest(2.4414062f-4) + @test "0.0024414062" == Ryu.writeshortest(2.4414062f-3) + @test "0.0043945312" == Ryu.writeshortest(4.3945312f-3) + @test "0.0063476562" == Ryu.writeshortest(6.3476562f-3) +end + +@testset "Regression" begin + @test "4.7223665e21" == Ryu.writeshortest(4.7223665f21) + @test "8.388608e6" == Ryu.writeshortest(8388608f0) + @test "1.6777216e7" == Ryu.writeshortest(1.6777216f7) + @test "3.3554436e7" == Ryu.writeshortest(3.3554436f7) + @test "6.7131496e7" == Ryu.writeshortest(6.7131496f7) + @test "1.9310392e-38" == Ryu.writeshortest(1.9310392f-38) + @test "-2.47e-43" == Ryu.writeshortest(-2.47f-43) + @test "1.993244e-38" == Ryu.writeshortest(1.993244f-38) + @test "4103.9004" == Ryu.writeshortest(4103.9003f0) + @test "5.3399997e9" == Ryu.writeshortest(5.3399997f9) + @test "6.0898e-39" == Ryu.writeshortest(6.0898f-39) + @test "0.0010310042" == Ryu.writeshortest(0.0010310042f0) + @test "2.882326e17" == Ryu.writeshortest(2.8823261f17) + @test "7.038531e-26" == Ryu.writeshortest(7.0385309f-26) + @test "9.223404e17" == Ryu.writeshortest(9.2234038f17) + @test "6.710887e7" == Ryu.writeshortest(6.7108872f7) + @test "1.0e-44" == Ryu.writeshortest(1.0f-44) + @test "2.816025e14" == Ryu.writeshortest(2.816025f14) + @test "9.223372e18" == Ryu.writeshortest(9.223372f18) + @test "1.5846086e29" == Ryu.writeshortest(1.5846085f29) + @test "1.1811161e19" == Ryu.writeshortest(1.1811161f19) + @test "5.368709e18" == Ryu.writeshortest(5.368709f18) + @test "4.6143166e18" == Ryu.writeshortest(4.6143165f18) + @test "0.007812537" == Ryu.writeshortest(0.007812537f0) + @test "1.0e-45" == Ryu.writeshortest(1.4f-45) + @test "1.18697725e20" == Ryu.writeshortest(1.18697724f20) + @test "1.00014165e-36" == Ryu.writeshortest(1.00014165f-36) + @test "200.0" == Ryu.writeshortest(200f0) + @test "3.3554432e7" == Ryu.writeshortest(3.3554432f7) +end + +@testset "LooksLikePow5" begin + # These numbers have a mantissa that is the largest power of 5 that fits, + # and an exponent that causes the computation for q to result in 10, which is a corner + # case for Ryu. + @test "6.7108864e17" == Ryu.writeshortest(Core.bitcast(Float32, 0x5D1502F9)) + @test "1.3421773e18" == Ryu.writeshortest(Core.bitcast(Float32, 0x5D9502F9)) + @test "2.6843546e18" == Ryu.writeshortest(Core.bitcast(Float32, 0x5E1502F9)) +end + +@testset "OutputLength" begin + @test "1.0" == Ryu.writeshortest(Float32(1.0)) + @test "1.2" == Ryu.writeshortest(Float32(1.2)) + @test "1.23" == Ryu.writeshortest(Float32(1.23)) + @test "1.234" == Ryu.writeshortest(Float32(1.234)) + @test "1.2345" == Ryu.writeshortest(Float32(1.2345)) + @test "1.23456" == Ryu.writeshortest(Float32(1.23456)) + @test "1.234567" == Ryu.writeshortest(Float32(1.234567)) + @test "1.2345678" == Ryu.writeshortest(Float32(1.2345678)) + @test "1.23456735e-36" == Ryu.writeshortest(Float32(1.23456735e-36)) +end + +end # Float32 + +@testset "Float16" begin + +@testset "Basic" begin + @test "0.0" == Ryu.writeshortest(Float16(0.0)) + @test "-0.0" == Ryu.writeshortest(Float16(-0.0)) + @test "1.0" == Ryu.writeshortest(Float16(1.0)) + @test "-1.0" == Ryu.writeshortest(Float16(-1.0)) + @test "NaN" == Ryu.writeshortest(Float16(NaN)) + @test "Inf" == Ryu.writeshortest(Float16(Inf)) + @test "-Inf" == Ryu.writeshortest(Float16(-Inf)) +end + +@testset "OutputOptions" begin + # typed + @test "Float16(1.0)" == Ryu.writeshortest(Float16(1.0), false, false, true, -1, UInt8('e'), false, UInt8('.'), true) + @test "Inf16" == Ryu.writeshortest(Float16(Inf), false, false, true, -1, UInt8('e'), false, UInt8('.'), true) + @test "NaN16" == Ryu.writeshortest(Float16(NaN), false, false, true, -1, UInt8('e'), false, UInt8('.'), true) + @test "Float16(3.14)" == Ryu.writeshortest(Float16(3.14), false, false, true, -1, UInt8('e'), false, UInt8('.'), true) + + # typed and no-hash + @test "Float16(1)" == Ryu.writeshortest(Float16(1.0), false, false, false, -1, UInt8('e'), false, UInt8('.'), true) +end + +let x=floatmin(Float16) + while x <= floatmax(Float16) + @test parse(Float16, Ryu.writeshortest(x)) == x + x = nextfloat(x) + end +end + +# function testfloats(T) +# x = floatmin(T) +# i = 0 +# fails = 0 +# success = 0 +# while x < floatmax(T) +# test = parse(T, Ryu.writeshortest(x)) == x +# if !test + +# fails += 1 +# else +# success += 1 +# end +# x = nextfloat(x) +# i += 1 + +# end +# return fails / (fails + success) +# end + +end # Float16 + +@testset "writeshortest(::AbstractVector, pos, ...)" begin + @testset for Vec in test_array_types + buf = Vec(undef, 4) + @test Ryu.writeshortest(buf, 1, -0.0) == 5 + @test String(buf) == "-0.0" + + buf = Vec(undef, 100) + xx = 4.7223665f21 + expected = "4.7223665e21" + start_pos = 42 + nwritten = length(expected) + end_pos = start_pos + nwritten + @test Ryu.writeshortest(buf, start_pos, xx) == end_pos + @test String(buf[start_pos:end_pos-1]) == expected + end +end + +@testset "Ryu.writefixed" begin + @testset "Basic" begin + @test Ryu.writefixed(todouble(false, 1234, 99999), 0) == + "3291009114715486435425664845573426149758869524108446525879746560" + end + @testset "Zero" begin + @test Ryu.writefixed(0.0, 4) == "0.0000" + @test Ryu.writefixed(0.0, 3) == "0.000" + @test Ryu.writefixed(0.0, 2) == "0.00" + @test Ryu.writefixed(0.0, 1) == "0.0" + @test Ryu.writefixed(0.0, 0) == "0" + end + @testset "MinMax" begin + @test Ryu.writefixed(todouble(false, 0, 1), 1074) == + "0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" * + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" * + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" * + "000000000000000000000000000000000000000000000000000000049406564584124654417656879286822137" * + "236505980261432476442558568250067550727020875186529983636163599237979656469544571773092665" * + "671035593979639877479601078187812630071319031140452784581716784898210368871863605699873072" * + "305000638740915356498438731247339727316961514003171538539807412623856559117102665855668676" * + "818703956031062493194527159149245532930545654440112748012970999954193198940908041656332452" * + "475714786901472678015935523861155013480352649347201937902681071074917033322268447533357208" * + "324319360923828934583680601060115061698097530783422773183292479049825247307763759272478746" * + "560847782037344696995336470179726777175851256605511991315048911014510378627381672509558373" * + "89733598993664809941164205702637090279242767544565229087538682506419718265533447265625" + @test Ryu.writefixed(todouble(false, 2046, 0xFFFFFFFFFFFFF), 0) == + "179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558" * + "632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245" * + "490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168" * + "738177180919299881250404026184124858368" + end + @testset "RoundToEven" begin + @test Ryu.writefixed(0.125, 3) == "0.125" + @test Ryu.writefixed(0.125, 2) == "0.12" + @test Ryu.writefixed(0.375, 3) == "0.375" + @test Ryu.writefixed(0.375, 2) == "0.38" + end + @testset "RoundToEvenInteger" begin + @test Ryu.writefixed(2.5, 1) == "2.5" + @test Ryu.writefixed(2.5, 0) == "2" + @test Ryu.writefixed(3.5, 1) == "3.5" + @test Ryu.writefixed(3.5, 0) == "4" + end + @testset "NonRoundToEvenScenarios" begin + @test Ryu.writefixed(0.748046875, 3) == "0.748" + @test Ryu.writefixed(0.748046875, 2) == "0.75" + @test Ryu.writefixed(0.748046875, 1) == "0.7" + + @test Ryu.writefixed(0.2509765625, 3) == "0.251" + @test Ryu.writefixed(0.2509765625, 2) == "0.25" + @test Ryu.writefixed(0.2509765625, 1) == "0.3" + + @test Ryu.writefixed(todouble(false, 1021, 1), 54) == "0.250000000000000055511151231257827021181583404541015625" + @test Ryu.writefixed(todouble(false, 1021, 1), 3) == "0.250" + @test Ryu.writefixed(todouble(false, 1021, 1), 2) == "0.25" + @test Ryu.writefixed(todouble(false, 1021, 1), 1) == "0.3" + end + @testset "VaryingPrecision" begin + @test Ryu.writefixed(1729.142857142857, 47) == "1729.14285714285711037518922239542007446289062500000" + @test Ryu.writefixed(1729.142857142857, 46) == "1729.1428571428571103751892223954200744628906250000" + @test Ryu.writefixed(1729.142857142857, 45) == "1729.142857142857110375189222395420074462890625000" + @test Ryu.writefixed(1729.142857142857, 44) == "1729.14285714285711037518922239542007446289062500" + @test Ryu.writefixed(1729.142857142857, 43) == "1729.1428571428571103751892223954200744628906250" + @test Ryu.writefixed(1729.142857142857, 42) == "1729.142857142857110375189222395420074462890625" + @test Ryu.writefixed(1729.142857142857, 41) == "1729.14285714285711037518922239542007446289062" + @test Ryu.writefixed(1729.142857142857, 40) == "1729.1428571428571103751892223954200744628906" + @test Ryu.writefixed(1729.142857142857, 39) == "1729.142857142857110375189222395420074462891" + @test Ryu.writefixed(1729.142857142857, 38) == "1729.14285714285711037518922239542007446289" + @test Ryu.writefixed(1729.142857142857, 37) == "1729.1428571428571103751892223954200744629" + @test Ryu.writefixed(1729.142857142857, 36) == "1729.142857142857110375189222395420074463" + @test Ryu.writefixed(1729.142857142857, 35) == "1729.14285714285711037518922239542007446" + @test Ryu.writefixed(1729.142857142857, 34) == "1729.1428571428571103751892223954200745" + @test Ryu.writefixed(1729.142857142857, 33) == "1729.142857142857110375189222395420074" + @test Ryu.writefixed(1729.142857142857, 32) == "1729.14285714285711037518922239542007" + @test Ryu.writefixed(1729.142857142857, 31) == "1729.1428571428571103751892223954201" + @test Ryu.writefixed(1729.142857142857, 30) == "1729.142857142857110375189222395420" + @test Ryu.writefixed(1729.142857142857, 29) == "1729.14285714285711037518922239542" + @test Ryu.writefixed(1729.142857142857, 28) == "1729.1428571428571103751892223954" + @test Ryu.writefixed(1729.142857142857, 27) == "1729.142857142857110375189222395" + @test Ryu.writefixed(1729.142857142857, 26) == "1729.14285714285711037518922240" + @test Ryu.writefixed(1729.142857142857, 25) == "1729.1428571428571103751892224" + @test Ryu.writefixed(1729.142857142857, 24) == "1729.142857142857110375189222" + @test Ryu.writefixed(1729.142857142857, 23) == "1729.14285714285711037518922" + @test Ryu.writefixed(1729.142857142857, 22) == "1729.1428571428571103751892" + @test Ryu.writefixed(1729.142857142857, 21) == "1729.142857142857110375189" + @test Ryu.writefixed(1729.142857142857, 20) == "1729.14285714285711037519" + @test Ryu.writefixed(1729.142857142857, 19) == "1729.1428571428571103752" + @test Ryu.writefixed(1729.142857142857, 18) == "1729.142857142857110375" + @test Ryu.writefixed(1729.142857142857, 17) == "1729.14285714285711038" + @test Ryu.writefixed(1729.142857142857, 16) == "1729.1428571428571104" + @test Ryu.writefixed(1729.142857142857, 15) == "1729.142857142857110" + @test Ryu.writefixed(1729.142857142857, 14) == "1729.14285714285711" + @test Ryu.writefixed(1729.142857142857, 13) == "1729.1428571428571" + @test Ryu.writefixed(1729.142857142857, 12) == "1729.142857142857" + @test Ryu.writefixed(1729.142857142857, 11) == "1729.14285714286" + @test Ryu.writefixed(1729.142857142857, 10) == "1729.1428571429" + @test Ryu.writefixed(1729.142857142857, 9) == "1729.142857143" + @test Ryu.writefixed(1729.142857142857, 8) == "1729.14285714" + @test Ryu.writefixed(1729.142857142857, 7) == "1729.1428571" + @test Ryu.writefixed(1729.142857142857, 6) == "1729.142857" + @test Ryu.writefixed(1729.142857142857, 5) == "1729.14286" + @test Ryu.writefixed(1729.142857142857, 4) == "1729.1429" + @test Ryu.writefixed(1729.142857142857, 3) == "1729.143" + @test Ryu.writefixed(1729.142857142857, 2) == "1729.14" + @test Ryu.writefixed(1729.142857142857, 1) == "1729.1" + @test Ryu.writefixed(1729.142857142857, 0) == "1729" + end + + @testset "Carrying" begin + @test Ryu.writefixed( 0.0009, 4) == "0.0009" + @test Ryu.writefixed( 0.0009, 3) == "0.001" + @test Ryu.writefixed( 0.0029, 4) == "0.0029" + @test Ryu.writefixed( 0.0029, 3) == "0.003" + @test Ryu.writefixed( 0.0099, 4) == "0.0099" + @test Ryu.writefixed( 0.0099, 3) == "0.010" + @test Ryu.writefixed( 0.0299, 4) == "0.0299" + @test Ryu.writefixed( 0.0299, 3) == "0.030" + @test Ryu.writefixed( 0.0999, 4) == "0.0999" + @test Ryu.writefixed( 0.0999, 3) == "0.100" + @test Ryu.writefixed( 0.2999, 4) == "0.2999" + @test Ryu.writefixed( 0.2999, 3) == "0.300" + @test Ryu.writefixed( 0.9999, 4) == "0.9999" + @test Ryu.writefixed( 0.9999, 3) == "1.000" + @test Ryu.writefixed( 2.9999, 4) == "2.9999" + @test Ryu.writefixed( 2.9999, 3) == "3.000" + @test Ryu.writefixed( 9.9999, 4) == "9.9999" + @test Ryu.writefixed( 9.9999, 3) == "10.000" + @test Ryu.writefixed( 29.9999, 4) == "29.9999" + @test Ryu.writefixed( 29.9999, 3) == "30.000" + @test Ryu.writefixed( 99.9999, 4) == "99.9999" + @test Ryu.writefixed( 99.9999, 3) == "100.000" + @test Ryu.writefixed(299.9999, 4) == "299.9999" + @test Ryu.writefixed(299.9999, 3) == "300.000" + + @test Ryu.writefixed( 0.09, 2) == "0.09" + @test Ryu.writefixed( 0.09, 1) == "0.1" + @test Ryu.writefixed( 0.29, 2) == "0.29" + @test Ryu.writefixed( 0.29, 1) == "0.3" + @test Ryu.writefixed( 0.99, 2) == "0.99" + @test Ryu.writefixed( 0.99, 1) == "1.0" + @test Ryu.writefixed( 2.99, 2) == "2.99" + @test Ryu.writefixed( 2.99, 1) == "3.0" + @test Ryu.writefixed( 9.99, 2) == "9.99" + @test Ryu.writefixed( 9.99, 1) == "10.0" + @test Ryu.writefixed( 29.99, 2) == "29.99" + @test Ryu.writefixed( 29.99, 1) == "30.0" + @test Ryu.writefixed( 99.99, 2) == "99.99" + @test Ryu.writefixed( 99.99, 1) == "100.0" + @test Ryu.writefixed(299.99, 2) == "299.99" + @test Ryu.writefixed(299.99, 1) == "300.0" + + @test Ryu.writefixed( 0.9, 1) == "0.9" + @test Ryu.writefixed( 0.9, 0) == "1" + @test Ryu.writefixed( 2.9, 1) == "2.9" + @test Ryu.writefixed( 2.9, 0) == "3" + @test Ryu.writefixed( 9.9, 1) == "9.9" + @test Ryu.writefixed( 9.9, 0) == "10" + @test Ryu.writefixed( 29.9, 1) == "29.9" + @test Ryu.writefixed( 29.9, 0) == "30" + @test Ryu.writefixed( 99.9, 1) == "99.9" + @test Ryu.writefixed( 99.9, 0) == "100" + @test Ryu.writefixed(299.9, 1) == "299.9" + @test Ryu.writefixed(299.9, 0) == "300" + end + + @testset "RoundingResultZero" begin + @test Ryu.writefixed(0.004, 3) == "0.004" + @test Ryu.writefixed(0.004, 2) == "0.00" + @test Ryu.writefixed(0.4, 1) == "0.4" + @test Ryu.writefixed(0.4, 0) == "0" + @test Ryu.writefixed(0.5, 1) == "0.5" + @test Ryu.writefixed(0.5, 0) == "0" + end + + @testset "Regression" begin + @test Ryu.writefixed(7.018232e-82, 6) == "0.000000" + end + + @testset "Trimming of trailing zeros" begin + @test Ryu.writefixed(0.0, 1, false, false, false, UInt8('.'), true) == "0" + @test Ryu.writefixed(1.0, 1, false, false, false, UInt8('.'), true) == "1" + @test Ryu.writefixed(2.0, 1, false, false, false, UInt8('.'), true) == "2" + + @test Ryu.writefixed(1.25e+5, 0, false, false, false, UInt8('.'), true) == "125000" + @test Ryu.writefixed(1.25e+5, 1, false, false, false, UInt8('.'), true) == "125000" + @test Ryu.writefixed(1.25e+5, 2, false, false, false, UInt8('.'), true) == "125000" + end + + @test Ryu.writefixed(100.0-eps(100.0), 0, false, false, true, UInt8('.'), false) == "100." + @test Ryu.writefixed(-100.0+eps(-100.0), 0, false, false, true, UInt8('.'), false) == "-100." + @test Ryu.writefixed(100.0-eps(100.0), 1, false, false, true, UInt8('.'), false) == "100.0" + @test Ryu.writefixed(-100.0+eps(-100.0), 1, false, false, true, UInt8('.'), false) == "-100.0" + + @testset "writefixed(::AbstractVector, pos, ...)" begin + @testset for Vec in test_array_types + buf = Vec(undef, 6) + @test Ryu.writefixed(buf, 1, 0.0, 4) == 7 + @test String(buf) == "0.0000" + + buf = Vec(undef, 100) + xx = 1729.142857142857 + prec = 8 + start_pos = 42 + nwritten = 4 + 1 + prec + end_pos = start_pos + nwritten + @test Ryu.writefixed(buf, start_pos, xx, prec) == end_pos + @test String(buf[start_pos:end_pos-1]) == "1729.14285714" + end + end +end # fixed + +@testset "Ryu.writeexp" begin + +@testset "Basic" begin + @test Ryu.writeexp(todouble(false, 1234, 99999), 62) == + "3.29100911471548643542566484557342614975886952410844652587974656e+63" +end + +@testset "Zero" begin + @test Ryu.writeexp(0.0, 4) == "0.0000e+00" + @test Ryu.writeexp(0.0, 3) == "0.000e+00" + @test Ryu.writeexp(0.0, 2) == "0.00e+00" + @test Ryu.writeexp(0.0, 1) == "0.0e+00" + @test Ryu.writeexp(0.0, 0) == "0e+00" +end + +@testset "MinMax" begin + @test Ryu.writeexp(todouble(false, 0, 1), 750) == + "4.9406564584124654417656879286822137236505980261432476442558568250067550727020875186529983" * + "636163599237979656469544571773092665671035593979639877479601078187812630071319031140452784" * + "581716784898210368871863605699873072305000638740915356498438731247339727316961514003171538" * + "539807412623856559117102665855668676818703956031062493194527159149245532930545654440112748" * + "012970999954193198940908041656332452475714786901472678015935523861155013480352649347201937" * + "902681071074917033322268447533357208324319360923828934583680601060115061698097530783422773" * + "183292479049825247307763759272478746560847782037344696995336470179726777175851256605511991" * + "315048911014510378627381672509558373897335989936648099411642057026370902792427675445652290" * + "87538682506419718265533447265625e-324" + + @test Ryu.writeexp(todouble(false, 2046, 0xFFFFFFFFFFFFF), 308) == + "1.7976931348623157081452742373170435679807056752584499659891747680315726078002853876058955" * + "863276687817154045895351438246423432132688946418276846754670353751698604991057655128207624" * + "549009038932894407586850845513394230458323690322294816580855933212334827479782620414472316" * + "8738177180919299881250404026184124858368e+308" +end + +@testset "RoundToEven" begin + @test Ryu.writeexp(0.125, 2) == "1.25e-01" + @test Ryu.writeexp(0.125, 1) == "1.2e-01" + @test Ryu.writeexp(0.375, 2) == "3.75e-01" + @test Ryu.writeexp(0.375, 1) == "3.8e-01" +end + +@testset "RoundToEvenInteger" begin + @test Ryu.writeexp(2.5, 1) == "2.5e+00" + @test Ryu.writeexp(2.5, 0) == "2e+00" + @test Ryu.writeexp(3.5, 1) == "3.5e+00" + @test Ryu.writeexp(3.5, 0) == "4e+00" +end + +@testset "NonRoundToEvenScenarios" begin + @test Ryu.writeexp(0.748046875, 2) == "7.48e-01" + @test Ryu.writeexp(0.748046875, 1) == "7.5e-01" + @test Ryu.writeexp(0.748046875, 0) == "7e-01" # 0.75 would round to "8e-01", but this is smaller + + @test Ryu.writeexp(0.2509765625, 2) == "2.51e-01" + @test Ryu.writeexp(0.2509765625, 1) == "2.5e-01" + @test Ryu.writeexp(0.2509765625, 0) == "3e-01" # 0.25 would round to "2e-01", but this is larger + + @test Ryu.writeexp(todouble(false, 1021, 1), 53) == + "2.50000000000000055511151231257827021181583404541015625e-01" + @test Ryu.writeexp(todouble(false, 1021, 1), 2) == + "2.50e-01" + @test Ryu.writeexp(todouble(false, 1021, 1), 1) == + "2.5e-01" + @test Ryu.writeexp(todouble(false, 1021, 1), 0) == + "3e-01" # 0.25 would round to "2e-01", but this is larger (again) +end + +@testset "VaryingPrecision" begin + @test Ryu.writeexp(1729.142857142857, 50) == "1.72914285714285711037518922239542007446289062500000e+03" + @test Ryu.writeexp(1729.142857142857, 49) == "1.7291428571428571103751892223954200744628906250000e+03" + @test Ryu.writeexp(1729.142857142857, 48) == "1.729142857142857110375189222395420074462890625000e+03" + @test Ryu.writeexp(1729.142857142857, 47) == "1.72914285714285711037518922239542007446289062500e+03" + @test Ryu.writeexp(1729.142857142857, 46) == "1.7291428571428571103751892223954200744628906250e+03" + @test Ryu.writeexp(1729.142857142857, 45) == "1.729142857142857110375189222395420074462890625e+03" + @test Ryu.writeexp(1729.142857142857, 44) == "1.72914285714285711037518922239542007446289062e+03" + @test Ryu.writeexp(1729.142857142857, 43) == "1.7291428571428571103751892223954200744628906e+03" + @test Ryu.writeexp(1729.142857142857, 42) == "1.729142857142857110375189222395420074462891e+03" + @test Ryu.writeexp(1729.142857142857, 41) == "1.72914285714285711037518922239542007446289e+03" + @test Ryu.writeexp(1729.142857142857, 40) == "1.7291428571428571103751892223954200744629e+03" + @test Ryu.writeexp(1729.142857142857, 39) == "1.729142857142857110375189222395420074463e+03" + @test Ryu.writeexp(1729.142857142857, 38) == "1.72914285714285711037518922239542007446e+03" + @test Ryu.writeexp(1729.142857142857, 37) == "1.7291428571428571103751892223954200745e+03" + @test Ryu.writeexp(1729.142857142857, 36) == "1.729142857142857110375189222395420074e+03" + @test Ryu.writeexp(1729.142857142857, 35) == "1.72914285714285711037518922239542007e+03" + @test Ryu.writeexp(1729.142857142857, 34) == "1.7291428571428571103751892223954201e+03" + @test Ryu.writeexp(1729.142857142857, 33) == "1.729142857142857110375189222395420e+03" + @test Ryu.writeexp(1729.142857142857, 32) == "1.72914285714285711037518922239542e+03" + @test Ryu.writeexp(1729.142857142857, 31) == "1.7291428571428571103751892223954e+03" + @test Ryu.writeexp(1729.142857142857, 30) == "1.729142857142857110375189222395e+03" + @test Ryu.writeexp(1729.142857142857, 29) == "1.72914285714285711037518922240e+03" + @test Ryu.writeexp(1729.142857142857, 28) == "1.7291428571428571103751892224e+03" + @test Ryu.writeexp(1729.142857142857, 27) == "1.729142857142857110375189222e+03" + @test Ryu.writeexp(1729.142857142857, 26) == "1.72914285714285711037518922e+03" + @test Ryu.writeexp(1729.142857142857, 25) == "1.7291428571428571103751892e+03" + @test Ryu.writeexp(1729.142857142857, 24) == "1.729142857142857110375189e+03" + @test Ryu.writeexp(1729.142857142857, 23) == "1.72914285714285711037519e+03" + @test Ryu.writeexp(1729.142857142857, 22) == "1.7291428571428571103752e+03" + @test Ryu.writeexp(1729.142857142857, 21) == "1.729142857142857110375e+03" + @test Ryu.writeexp(1729.142857142857, 20) == "1.72914285714285711038e+03" + @test Ryu.writeexp(1729.142857142857, 19) == "1.7291428571428571104e+03" + @test Ryu.writeexp(1729.142857142857, 18) == "1.729142857142857110e+03" + @test Ryu.writeexp(1729.142857142857, 17) == "1.72914285714285711e+03" + @test Ryu.writeexp(1729.142857142857, 16) == "1.7291428571428571e+03" + @test Ryu.writeexp(1729.142857142857, 15) == "1.729142857142857e+03" + @test Ryu.writeexp(1729.142857142857, 14) == "1.72914285714286e+03" + @test Ryu.writeexp(1729.142857142857, 13) == "1.7291428571429e+03" + @test Ryu.writeexp(1729.142857142857, 12) == "1.729142857143e+03" + @test Ryu.writeexp(1729.142857142857, 11) == "1.72914285714e+03" + @test Ryu.writeexp(1729.142857142857, 10) == "1.7291428571e+03" + @test Ryu.writeexp(1729.142857142857, 9) == "1.729142857e+03" + @test Ryu.writeexp(1729.142857142857, 8) == "1.72914286e+03" + @test Ryu.writeexp(1729.142857142857, 7) == "1.7291429e+03" + @test Ryu.writeexp(1729.142857142857, 6) == "1.729143e+03" + @test Ryu.writeexp(1729.142857142857, 5) == "1.72914e+03" + @test Ryu.writeexp(1729.142857142857, 4) == "1.7291e+03" + @test Ryu.writeexp(1729.142857142857, 3) == "1.729e+03" + @test Ryu.writeexp(1729.142857142857, 2) == "1.73e+03" + @test Ryu.writeexp(1729.142857142857, 1) == "1.7e+03" + @test Ryu.writeexp(1729.142857142857, 0) == "2e+03" +end + +@testset "Carrying" begin + @test Ryu.writeexp(2.0009, 4) == "2.0009e+00" + @test Ryu.writeexp(2.0009, 3) == "2.001e+00" + @test Ryu.writeexp(2.0029, 4) == "2.0029e+00" + @test Ryu.writeexp(2.0029, 3) == "2.003e+00" + @test Ryu.writeexp(2.0099, 4) == "2.0099e+00" + @test Ryu.writeexp(2.0099, 3) == "2.010e+00" + @test Ryu.writeexp(2.0299, 4) == "2.0299e+00" + @test Ryu.writeexp(2.0299, 3) == "2.030e+00" + @test Ryu.writeexp(2.0999, 4) == "2.0999e+00" + @test Ryu.writeexp(2.0999, 3) == "2.100e+00" + @test Ryu.writeexp(2.2999, 4) == "2.2999e+00" + @test Ryu.writeexp(2.2999, 3) == "2.300e+00" + @test Ryu.writeexp(2.9999, 4) == "2.9999e+00" + @test Ryu.writeexp(2.9999, 3) == "3.000e+00" + @test Ryu.writeexp(9.9999, 4) == "9.9999e+00" + @test Ryu.writeexp(9.9999, 3) == "1.000e+01" + + @test Ryu.writeexp(2.09, 2) == "2.09e+00" + @test Ryu.writeexp(2.09, 1) == "2.1e+00" + @test Ryu.writeexp(2.29, 2) == "2.29e+00" + @test Ryu.writeexp(2.29, 1) == "2.3e+00" + @test Ryu.writeexp(2.99, 2) == "2.99e+00" + @test Ryu.writeexp(2.99, 1) == "3.0e+00" + @test Ryu.writeexp(9.99, 2) == "9.99e+00" + @test Ryu.writeexp(9.99, 1) == "1.0e+01" + + @test Ryu.writeexp(2.9, 1) == "2.9e+00" + @test Ryu.writeexp(2.9, 0) == "3e+00" + @test Ryu.writeexp(9.9, 1) == "9.9e+00" + @test Ryu.writeexp(9.9, 0) == "1e+01" +end + +@testset "Exponents" begin + @test Ryu.writeexp(9.99e-100, 2) == "9.99e-100" + @test Ryu.writeexp(9.99e-99 , 2) == "9.99e-99" + @test Ryu.writeexp(9.99e-10 , 2) == "9.99e-10" + @test Ryu.writeexp(9.99e-09 , 2) == "9.99e-09" + @test Ryu.writeexp(9.99e-01 , 2) == "9.99e-01" + @test Ryu.writeexp(9.99e+00 , 2) == "9.99e+00" + @test Ryu.writeexp(9.99e+01 , 2) == "9.99e+01" + @test Ryu.writeexp(9.99e+09 , 2) == "9.99e+09" + @test Ryu.writeexp(9.99e+10 , 2) == "9.99e+10" + @test Ryu.writeexp(9.99e+99 , 2) == "9.99e+99" + @test Ryu.writeexp(9.99e+100, 2) == "9.99e+100" + + @test Ryu.writeexp(9.99e-100, 1) == "1.0e-99" + @test Ryu.writeexp(9.99e-99 , 1) == "1.0e-98" + @test Ryu.writeexp(9.99e-10 , 1) == "1.0e-09" + @test Ryu.writeexp(9.99e-09 , 1) == "1.0e-08" + @test Ryu.writeexp(9.99e-01 , 1) == "1.0e+00" + @test Ryu.writeexp(9.99e+00 , 1) == "1.0e+01" + @test Ryu.writeexp(9.99e+01 , 1) == "1.0e+02" + @test Ryu.writeexp(9.99e+09 , 1) == "1.0e+10" + @test Ryu.writeexp(9.99e+10 , 1) == "1.0e+11" + @test Ryu.writeexp(9.99e+99 , 1) == "1.0e+100" + @test Ryu.writeexp(9.99e+100, 1) == "1.0e+101" +end + +@testset "PrintDecimalPoint" begin + # These values exercise each codepath. + @test Ryu.writeexp(1e+54, 0) == "1e+54" + @test Ryu.writeexp(1e+54, 1) == "1.0e+54" + @test Ryu.writeexp(1e-63, 0) == "1e-63" + @test Ryu.writeexp(1e-63, 1) == "1.0e-63" + @test Ryu.writeexp(1e+83, 0) == "1e+83" + @test Ryu.writeexp(1e+83, 1) == "1.0e+83" +end + +@testset "Consistency of trimtrailingzeros" begin + @test Ryu.writeexp(0.0, 1, false, false, false, UInt8('e'), UInt8('.'), true) == "0e+00" + @test Ryu.writeexp(1.0, 1, false, false, false, UInt8('e'), UInt8('.'), true) == "1e+00" + @test Ryu.writeexp(2.0, 1, false, false, false, UInt8('e'), UInt8('.'), true) == "2e+00" +end + +@testset "writeexp(::AbstractVector, pos, ...)" begin + @testset for Vec in test_array_types + buf = Vec(undef, 10) + @test Ryu.writeexp(buf, 1, 0.0, 4) == 11 + @test String(buf) == "0.0000e+00" + + buf = Vec(undef, 100) + xx = 1729.142857142857 + prec = 8 + start_pos = 42 + nwritten = 1 + 1 + prec + 4 + end_pos = start_pos + nwritten + @test Ryu.writeexp(buf, start_pos, xx, prec) == end_pos + @test String(buf[start_pos:end_pos-1]) == "1.72914286e+03" + end +end + +end # exp + +@testset "compact" begin + + stringcompact(x) = sprint(show, x; context=:compact => true) + + @test stringcompact(0.49999999) == "0.5" + @test stringcompact(0.459999999) == "0.46" + @test stringcompact(0.20058603493384108) == "0.200586" + @test stringcompact(0.9999999) == "1.0" + @test stringcompact(0.1999999) == "0.2" + @test stringcompact(123.4567) == "123.457" + @test stringcompact(0.001234567) == "0.00123457" + @test stringcompact(0.1234567) == "0.123457" + @test stringcompact(1234567.0) == "1.23457e6" + @test stringcompact(12345678910.0) == "1.23457e10" + @test stringcompact(12345678.0) == "1.23457e7" + @test stringcompact(0.10000049) == "0.1" + @test stringcompact(22.89825) == "22.8983" + @test stringcompact(0.646690981531646) == "0.646691" + @test stringcompact(6.938893903907228e-17) == "6.93889e-17" + @test stringcompact(1.015625) == "1.01562" + @test stringcompact(1.046875) == "1.04688" + @test stringcompact(0.025621074) == "0.0256211" + + # subnormals + @test stringcompact(eps(0.0)) == "5.0e-324" + @test stringcompact(eps(0f0)) == "1.0f-45" + @test stringcompact(eps(Float16(0.0))) == "6.0e-8" +end + +end # Ryu