Rounding to a multiple of float

I am trying to solve a simple problem - rounding a number to a nearest multiple of a given float. As an example, I would like roundmult(3811.47123123, 0.01) = 3811.47. However, the simplest implementation of roundmult(val, prec) = round(val / prec) * prec backfires by outputting roundmult(3811.47123123, 0.01) = 3811.4700000000003.

Is there any simple way of achieving this using some built-in functionality, or some magic of rounding the above result once more to the precision given by prec? Something like
roundmult(val, prec) = round(round(val / prec) * prec, digits=ceil(Integer, -log10(prec)+1)) would probably work, but it is so ugly.

cheeky idea… Move everything up by two decimal places… then use integer arithmetic.

I guess that assumes that the second argument is always less that 1.0 of course.

Cheeky :slight_smile: I do not like the idea of changing the whole application due to this small issue :slight_smile:
Also, the precision is not fixed upfront, it changes based on the current data. So your suggestion would not really work.

If you don’t mind using internal functions, try this:

julia> Base._round_invstep(3811.47123123, 1/0.01, RoundNearest)
3811.47

julia> Base._round_invstep(3811.47123123, 1/0.02, RoundNearest)
3811.48

julia> Base._round_invstep(3811.47123123, 1/0.05, RoundNearest)
3811.45
2 Likes

Go through rationals:

roundmult(val::Rational, prec::Rational) = round(val / prec) * prec

function roundmult(val::T, prec::S) where {T <: AbstractFloat, S <: AbstractFloat}
    R = promote_type(T, S)
    R(roundmult(rationalize(val), rationalize(prec)))
end

julia> roundmult(3811.47123123, 0.01)
3811.47

It will be slower, but may be justified for pretty printing.

What is your problem with roundmult?

E.g. roundmult(0.3, 0.1) == 0.30000000000000004. This is correct: 3*0.1 == 0.30000000000000004. 0.3 is not an integer multiple of 0.1 in Float64.

Julia float literals are base-2 floating point. This means that 1//10 cannot be expressed (has an infinite number of nonzero digits), just like you would encounter rounding for 1//3 if you used decimal floats.

2 Likes

@Tamas_Papp Thanks for the suggestion! It is definitely a cleaner approach than the one I pointed out in the original post, but I would still expect something cleaner to be around.

@foobar_lv2 I understand your point. If I would use this value just in my Julia application, it makes no sense to consider this question at all. However, I am sending the result to an external API, which will only accept numbers rounded to these given floats. If I send the result 3811.4700000000003, my request will be simply declined. I really need to send 3811.47, therefore my question.

@bennedich Thanks for the suggestion, a simple wrapper around these should suffice!

If it is only about rounding to a certain number of digits after the decimal, then your problem is simply about printing floats, which is much simpler than the on you describe (since prec is of the form 10^n).

Ultimately, I’d think that you should make the number of decimals part of your API.

Example:

julia> roundmult(val, prec, digits=6) = round(round(val / prec) * prec, digits=digits, base=10)
julia> roundmult(3811.47123123, 0.01)
3811.47

Not really. I am sending a JSON, where the field is actually a float. Maybe it would make more sense if it would actually accept printed floats.

The issue is that the prec variable does not have to be of the form 10^{-n}. As in my original post, it could easily be 0.005. The simplest way to figure out the number of decimals is probably using the logarithm, which I also suggested in the original post.

A clean solution was already provided by bennedich - it works like charm.

JSON is a text format. You are effectively printing a number.

True, but there still is a slight difference.

JSON.json(Dict("a"=>"12.12")) = "{\"a\":\"12.12\"}"
JSON.json(Dict("a"=>12.12)) = "{\"a\":12.12}"

The other side will also approach it differently based on whether it gets a printed number or an actual number.

The question is not how the JSON.jl library accepts it, which is an implementation detail, but how you can print it the way you like (since JSON is trivially easy to generate). See eg Base.Grisu.grisu for low-level float digits printing, which would allow you to generate either of the strings above.

1 Like

Good point, I agree that rounding during the serialization would feel more natural than rounding the float. How would you round to an arbitrary step with grisu? I didn’t find any documentation about this. I.e. to accomplish this:

julia> Base._round_invstep(3.141592, 1/0.25, RoundNearest)
3.25

I’d advice against implementing one’s own JSON serializer, it can be hard to get all escaping, separators, etc, correct. I’ve seen bugs introduced this way before. The way to do it IMO would be to use a custom JSON serializer. Unless I’m missing something, the support for that doesn’t seem great in JSON.jl – I don’t think we can use JSON.lower for this. I guess we can do something like the below, although arguably the rounding should belong to the serializer not each individual float:

using JSON

struct MyRoundedFloat
    x::Float64
    step::Float64
end

function JSON.show_json(io::JSON.Writer.StructuralContext, s::JSON.Serializations.CommonSerialization, x::MyRoundedFloat)
    print(io, "formatting code goes here")
end

With result:

julia> JSON.json(Dict("a" => MyRoundedFloat(3.141592, 0.25)))
"{\"a\":formatting code goes here}"

All in all, this route doesn’t seem pretty, but perhaps there’s something I’m not seeing.

It only does decimal. But Grisu.grisu is documented.

Oh my god. This means that you need figure out whatever the endpoint does.

Javascript has the same floating-point semantics:

s={"a": "0.1", "b":"0.3"}; 3*s.a 
0.30000000000000004

This means that whatever endpoint you are talking to does crazy stuff you need to figure out (if you are lucky, then by reading code; if unlucky, by looking at a disassembly; if very unlucky, then by sending a bazillion requests and black-box reversing whatever they are doing).

I mean, isn’t it easier to just send the truncated version? In Julia we have

julia> 0.1 * 3
0.30000000000000004
julia> 0.1 * 3 == 0.3
false

If I would send 0.1 * 3 to the endpoint, something could go wrong, but it might work out, depending on the processing on the other side. But if I send actual 0.3, I should be safe.

As far as I understand, the problem isn’t that Julia cannot represent 0.3. The problem is that 0.1 * 3 does not give 0.3, due to the representation and arithmetic on these two numbers. Am I wrong?

Wasn’t there recently a discussion about the appropriateness of recommending internal functions? At least it should come with a warning, especially to new users.

2 Likes

Neither 0.1 nor 0.3 can be represented exactly in (binary-based) floating point.

Designing an interface that accepts 0.3 but not 0.30000000000000004 but otherwise works with Float64 or similar internally is just absurd.

1 Like