Forcing the E or F format without losing precision

I need a string decimal representation of a Float64 and I’d like to specify either the E format, e.g., “3.14e-3”, or the F format, “0.00314”, without having to worry about how many digits are necessary to preserve precision. To illustrate the problem, here is a pseudo code:

a = 0.19607361594437356e-3
sprintf("%e", a) # -> "0.000196" -> would lose precision
sprintf("%f", a) # ->  "1.960736e-04"  -> would lose precision

(I obtained the above result using a shell-built-in printf command.)

Is there an “easier” solution than to explicitly specify the number of digits?

The purpose of the string representation is to feed it to Fortran, which requires a format like “3.14d-3”. Note the “d” to designate Double Precision. I’ll use replace(s, 'e' => 'd') to generate this string, but for that purpose I always need “e” in the original Julia string.

Edit: Basically what I want is a function which can use the E format and the F format and which parse(Float64, _) is the inverse of:

f = rand() # or any Float64 value
st = float_to_string(f, format=:E) # use the "3.14e-3" type of format.
ff = parse(Float64, st)
@assert(f == ff) # should always hold

one answer is to use hexadecimal floats i.e. 0x1.e4fp5. that way you know your input has no rounding since you are just writing the bits

With Format.jl,

using Format
julia> printfmtln("{1}", sqrt(2))
1.4142135623730951

hexadecimal floats 0x1.e4fp5

Sounds promising but I don’t quite get it. I would be doing

f = sqrt(2)
st = hexrep(f) # => hexadecimal representation of f
# use st to generate a Fortran statement like
# real(8), parameter:: f = real(Z'<hex>', kind=kind(f))

(I don’t know whether this is correct Fortran. All I know is that Fortran 2008 allows for some hex constants.) My question here is how can one write the function hexrep() above. The traditional printf() function does that only for integers, as far as I know. Which Julia formatter does it for floating point numbers?

Also sounds promising, but I haven’t been able to find how to specify the E format with “sufficient number of digits” in the documentation (quoted below). Here is my first attempt:

julia> using Format

julia> printfmtln("{1:e}", sqrt(2))
1.414214e+00

julia> printfmtln("{1:24.16e}", sqrt(2))
  1.4142135623730952e+00

(I’m not sure whether 16 digits is always good enough.)

https://docs.python.org/2/library/string.html#formatspec

EDIT: Well, disregard everything below. While this probably works well enough, it relies on the assumption that string(f) is sufficiently precise when 1 \leq f < 10, which is a very arbitrary choice of precision. It’s better to just make this explicit in format(f, precision=(...)) for the F format, and format("{:.(...)e}", f) for the E format.

(You could calculate the number of decimal digits which can represent every Float64 exactly, but I’m clearly too tired, so I’m not going to bother :). As @Oscar_Smith highlighted, it will be more compact to stick to binary(/hexadecimal) as much as possible anyway. Additionally, while I know next to nothing about Fortran, I highly doubt it will actually use all digits if you would supply, say, a hundred of them.)


'Manually' formatting the exponent / zeros

I’d say just computing the exponent / placing the zeros is conceptually quite easy, so you could do this ‘manually’. (Here I’m starting from string(f) for a float in [1,10) assuming this is sufficiently accurate. But if you want to have fun, you could also start directly from the bits in f :slight_smile: .)

function float_to_string(f, format)
    exponent = floor(Int, log10(abs(f)))
    f_base_str = string(f / 10. ^ exponent)  # e.g. -1.23456789
    if format === :E || format == :e
        return f_base_str * "e$exponent"
    elseif format === :F || format === :f
        if exponent == 0
            return f_base_str
        else
            digits_str = replace(f_base_str, '.' => "", '-' => "")  # 123456789
            sign_str = f < 0 ? '-' : ""
            if exponent <= -1
                return sign_str * "0." * '0' ^ (-exponent - 1) * rstrip(digits_str, '0')
                # e.g. exponent == -2: -0.00123456789
                # rstrip for when f == n⋅10^exponent for some integer n, as then f_base_str == "n.0"
            else
                if length(digits_str) >= exponent + 1   # e.g. exponent == 2
                    before_decimal_point = sign_str * digits_str[begin:exponent+1]  # -123
                    after_decimal_point = (length(digits_str) == exponent + 1) ? '0' : digits_str[exponent+2:end]  # 456789
                    # Make sure an integer ends with '.0' (e.g. 123.0)
                    return before_decimal_point * '.' * after_decimal_point  # -123.456789
                else
                    return sign_str * digits_str * '0' ^ (exponent - length(digits_str) + 1) * ".0"
                     # e.g. exponent == 12: -1234567890000.0
                end
            end
        end
    else
        throw("Invalid format option $format")
    end
end

But the F format is a tad annoying to get right, so just using the maximum precision in e.g. a @sprintf (from Printf.jl) or in Format.jl 's format will be easier, and probably more performant, if that’s relevant.

julia> float_to_string(1000., :E)
"1.0e3"

julia> float_to_string(123456789.01, :e)
"1.2345678901000001e8"

julia> float_to_string(1.23e6, :f)
"1230000.0"

julia> x = sqrt(2) / 1e10
1.414213562373095e-10

julia> s = float_to_string(x, :F)
"0.0000000001414213562373095"

julia> parse(Float64, s) ≈ sqrt(2) / 1e10
true

I think that a format string like "%.16e" is far and away the easiest. 16 decimal digits will always be sufficient to uniquely identify every possible Float64 value.

using Printf
float_to_string(f) = @sprintf("%.16e", f)
2 Likes

The printf code for hex float appears to be "a":

julia> using Printf

julia> @sprintf("%a", sqrt(2))
"0x1.6a09e667f3bcdp+0"
1 Like