Compare numbers at the stated precision

In tests I’ll like to be able to write the following and the test to pass:


@test aresame(pi, 3.14)

The following works:


@test isapprox(pi, 3.14, atol = 0.005)

But then I need to adjust the absolute tolerance manually.

I need a function to find the “number of significant digits” of a numeric literal and use that.

The following apparently works, but I’m wondering if this already exists?


function sigdigs(x)
    x_string = string(convert(Float64,x))
    length(x_string) - findlast('.',x_string)
end

function aresame(x,y)
    tol_digits = min(sigdigs(x), sigdigs(y))
    tol = .49*0.1^(tol_digits)
    isapprox(x,y,atol=tol)
end

julia> aresame(pi,3.1)
true

julia> aresame(pi,3.14)
true

julia> aresame(pi,3.141)
false

julia> aresame(pi,3.1415)
false

julia> aresame(pi,3.14159)
true

Are there standard functions or a package that does this?

Can this be adapted to make use of the rtol keyword in isapprox?

Nope:

julia> sigdigs(1.00000000)
1

The basic problem here is that the Julia parser does not preserve the number of digits that were used when a literal floating-point value was entered.

The only way I can think of to do this is to define a new type of numeric literal that you construct with a string macro. e.g. you could define a type such that:

@test pi ≈ t"3.14"

works, i.e. t"3.14" constructs a type TestNumber(3.14, 0.005) and there is an overloaded isapprox for TestNumber.

3 Likes

For example:

struct TestNumber{T<:Number}
    val::T
    atol::Float64
end
macro t_str(s)
    val = parse(Float64, s)
    parts = split(s, r"\.|[Ee]")
    atol = exp10(length(parts) > 1 ? -length(parts[2]) : 0) * 0.5 *
           (length(parts) > 2 ? exp10(parse(Int, parts[3])) : 1.0)
    return TestNumber(val, atol)
end
Base.isapprox(x::Number, t::TestNumber) = isapprox(x, t.val, atol=t.atol)
Base.isapprox(t::TestNumber, x::Number) = isapprox(x, t)
Base.show(io::IO, t::TestNumber) = print(io, t.val, " ± ", t.atol)

So that e.g.

julia> t"3.14"
3.14 ± 0.005

julia> t"3.1400"
3.14 ± 5.0e-5

julia> t"3.1400e-10"
3.14e-10 ± 5.000000000000001e-15

julia> pi ≈ t"3.14"
true

julia> pi ≈ t"3.140"
false
4 Likes

Thank you @stevengj .
Looks like it is a bit more complicated than I imagined.

Could using the sigdigits parameter of the round function be useful?
I ask this because I have not understood what exactly the need is.

I would like to “automatically” get the number of significant digits of a numeric literal.
As @stevengj has shown, that is probably not possible in general without using strings or string macros.
Sorry for not being precise.

Could you give some examples of expected result (negative = false or positive = true)?
I have tried the following examples and I don’t know I can’t understand why one is true and one is false.

julia> pi ≈ t"3.1415"
false

julia> pi ≈ t"3.14159"
true

edit
ok. understood. depends on whether the next digit is less than or greater than 5

is this equivalent?

aresame(x,s)=isapprox(round(x,sigdigits=length(strip(s,'0'))-1),parse(Float64, s))

Thank you for the suggestion @rocco_sprmnt21 .
Your function captures the idea well. I would prefer it to be symmetric, but that should not be too hard to fix.

Clearly my original post was not precise enough. My main question is if there is already some package (or standard function) that does this.

well … then @steveng’sj solution is right for you, in the absence of a library solution?

maybe something like that if it were available working would be closer to your expectations?

Base.isapprox(x::Number, y::Number, sigdigits:: Bool) = 
if sigdigits
# here the expressions you used in 
# sigdigs
# and
# aresame
   return isapprox(x, y, atol = .49*0.1^(tol_digits))`
end