BigFloat number conversion problem


#1

Here is the problem in a nutshell:
julia> BigFloat(3.825)
3.82500000000000017763568394002504646778106689453125
julia>BigFloat(3825//1000)
3.825000000000000000000000000000000000000000000000000000000000000000000000000007

In the first one it looks like a Float64 is taking place before BigFloat gets it and the rest is noise.


#2

Use the big"..." non-standard literal, eg

julia> big"3.825"
3.825000000000000000000000000000000000000000000000000000000000000000000000000007

#3

That’s true - as far as I remember, this already happens at parsing level, long before the call converts its argument.


#4

Seems like a problem to me. Any sane way to get around this other than turning the number into a rational


#5

Related to this if you try to convert the 3.825 into a rational you do not get 3825//1000 or reduced 153//40 so the fault is at the string to Float64 conversion since it should be exact in binary. I see 2 problems, a Float64 conversion that is wrong and a Float64 conversion that shouldn’t happen.


#6

Thanks Tamas for big"8.325" that works correctly
julia>big"8.325"
3.825000000000000000000000000000000000000000000000000000000000000000000000000007
In the online manual they show big() which would indicate
big(3.825) or big(“3.825”)
julia> big(3.825)
3.82500000000000017763568394002504646778106689453125
julia> big(“3.825”)
ERROR: MethodError: no method matching big(::String)
Closest candidates are:
big(::Type{Complex{T<:Real}}) where T<:Real at complex.jl:969
big(::Type{#s561} where #s561<:Integer) at gmp.jl:403
big(::Type{#s561} where #s561<:Rational) at gmp.jl:404

Stacktrace:
[1] top-level scope at none:0
There is some language inconsistency here that probably needs to be addressed.


#7

Could you please post a link to where you found this? If you want to create a BigFloat from a regular string, a correct way would be this:

julia> BigFloat("3.825")
3.825000000000000000000000000000000000000000000000000000000000000000000000000007

the big() function converts an existing number to its “big” (i.e. maximum precision) representation, according to its type:

julia> big(1)
1

julia> typeof(big(1))
BigInt

julia> big(1.0)
1.0

julia> typeof(big(1.0))
BigFloat

#8

Why do you say that 3.825 should be exact in binary?

I don’t think it is: 3.825 = \frac{153}{40} = \frac1{2^3} \left(30 + \frac35\right), and the binary representation of \frac35 has an infinite cyclic sequence (much like the infamous 1/10):

\frac35 =(0.1001100110011\ldots)_2

So the mantissa cannot be represented exactly with a finite number of bits.


#9

@ffevotte you are certainly correct. I should have expanded it in binary before making such a dumb statement.

That blunder aside, I still think there is an issue using BigFloat but don’t have any good suggestion for a clean solution. However, there might be some way to pass the initial string as well as the converted number and let the function decide which to take and convert. I know that sounds very clumsy but using big"3.825" just seems very wrong and inconsistent language-wise.

big is under base.big in the julialang.org manual and yes, BigFloat(“3.825”) also works and is better than using big"3.825" which is rather hokey syntax.


#10

It is part of the supported API. If anything, the odd one out is BigFloat(::AbstractString), as other float types just have a (try)parse, but it makes sense since one may need to specify the precision.


#11

Note that you can get a rational with

rationalize(3.825)

#12

Yes, but in this case, 3.825 gets first parsed as a Float64 (and therefore rounded), and only then is it converted to a rational. Depending on the tolerance given to rationalize, one may not always get \frac{153}{40} as one might expect.

This happens to work, because the round-off error caused by the conversion to Float64 is compensated by the approximation in rationalize:

julia> rationalize(3.825, tol=1e-15)
 153//40

When decreasing the tolerance, rationalize produces a better approximation of Float64(3.825), but not of 3.825 itself.

julia> rationalize(3.825, tol=1e-20)
 538290589893019//140729565985103

Whereas for big"3.825" and with the same level of tolerance, rationalize produces the “correct” result (which, again, is not guaranteed, but happens to be the case because round-off errors compensate)

julia> rationalize(big"3.825", tol=1e-20)
153//40

#13

I’m not sure I understand what you find inconsistent. Would you rather have a special syntax understood by the parser to define BigFloat literals, like we have 1.0f0 to define Float32 literals instead of Float64? I might be missing something, but I would say that Float32 is the exception here (perhaps because it is both very useful and supported by the hardware).

All other numeric types that I know of (big integers, 32-bit integers on a 64-bit architecture, big floats, half-precision floats…) must be built explicitly, either using their constructor (e.g. BigInt, Int32, BigFloat…) to convert them from another numeric representation, or using parse to read them from a string. So all this seems rather consistent to me.

I see the big"..." string macro merely as syntactic sugar to avoid calling parse (or rather tryparse in this case) for BigInt or BigFloat literals.


#14

At least unsigned integers, hexadecimals, binary and octal also have special syntax: https://docs.julialang.org/en/v1/manual/integers-and-floating-point-numbers/#Integers-1


#15

True. I guess I was not explicit enough, then. What I wanted to say is that, if you want to build a numeric value of a specific type, based on its decimal representation* (an important mention that I forgot), I think that you have to explicitly parse a string except for very few specific types: Int (32 or 64 depending on your architecture), Float32 and Float64.

It’s true that you can build an UInt with special syntax, but then you have to change the radix and use a binary, octal or hexadecimal representation (and BTW I would consider hexadecimal, binary and octal to be number representations, not types). If you want to represent the value 25 as an UInt (as opposed to creating a bitmask), you either have to make the conversion in your head and write 0x19 or be explicit and call the relevant constructor UInt(25) or parse(UInt, "25").



* I make the distinction here because I think most mere humans nowadays think of numeric values in terms of their decimal representations. I would even venture to say that developers (although they are much more than mere humans :wink:) only depart from radix 10 in very specific situations, for example when building bitmasks (in which case the value of the number is of somewhat lesser importance…) Of course I might be wrong.


#16

No definitely not. The only thing you need to be explicitly about is the type.

Is by no mean parsing a string and

Don’t use this as literal.