-1

[Ed: reworded - sorry for the confusing initial wording. Also thanks to all those who suggested using integers, but for several reasons I must use float64.]

I have a floating point value that I want to round to 1 decimal if more than 100 and no decimals if more than 1000, otherwise round to 2 decimals. This works:

func r(f float64) float64 {
    if f >= 999.5 {
        return math.Round(f)
    }
    if f >= 99.95 {
        return math.Round(f*10) / 10
    }
    return math.Round(f*100) / 100
}

but I'm wondering if this would be better:

func r(f float64) float64 {
    if f >= 999.5 {
        return math.Round(f)
    }
    if f*10 >= 999.5 {  // **** only changed line ****
        return math.Round(f*10) / 10
    }
    return math.Round(f*100) / 100
}

This is safer as 99.95 is not represented exactly using floating point with a binary exponent. (I believe the Go language requires use of IEEE fp format which has a binary exponent.) 999.5 is exactly representable as a fp value.

However, my tests (using math.NextAfter()) show that the first solution works perfectly well for all values from 99.94999999 to 99.95000001. 1st question: I am worrying unduly.

The problem with the 2nd solution is that I am worried that f*10 may be evaluated twice. 2nd question: is there any guarantee the optimiser will ensure that it is only done once.

AJR
  • 1,547
  • 5
  • 16
  • 4
    Neither is really safer, because you should not be using floating point arithmetic for decimal values like currency. – JimB Feb 28 '21 at 23:56
  • 3
    A simple approach is to store currency as an `int` of cents, which avoids the imprecision of floats, and leave decimal point placement entirely to a *rendering* concern (which it is). – Bohemian Mar 01 '21 at 00:13
  • 1
    Using the minimal unit for the currency is a common approach, as Bohemian says, just keep in my that different currencies have different fractions for the minimal unit (or none at all like JPY). – hmoragrega Mar 01 '21 at 01:14
  • 1
    See also https://stackoverflow.com/questions/588004/is-floating-point-math-broken. There is no way to solve the problem you're trying to solve using binary floating point. – Rob Napier Mar 01 '21 at 02:17
  • Thanks @JimB for you reply: > neither is really safe... Actually I know that the 2nd is perfectly safe as it use the same rounding as `Round`. Moreover 999.5 does not have rounding problems. The question was about the optimizer - ie whether I can be sure that f*10 is not evaluated twice at runtime. I thoroughly understand fp rounding problems and that in general it should not be used for currency. However, the value is being given to me by a package that I have no control over. Moreover, 999.5 is represented exactly in a fp rep. (whether using binary or decimal exponent). – AJR Mar 01 '21 at 20:37
  • Thanks @Bohemian. My problem is actually not about rendering (sorry to mislead you with my mention of display of numbers but that was the easiest way I could think to describe the problem). Also the money value is not currency per se (again confusing) but odds in a gambling app - effectively a money value. I've reworded the question to avoid the confusion. – AJR Mar 01 '21 at 20:47
  • "odds in a gambling app" doesn't sound like currency to me. Nevertheless, it *does* sound like a float type is not suitable. Perhaps use "cents" (which comes from the latin *centum*, meaning "hundred") as "1 hundredth of a percentage (also from *centum*) then your conditions would be simple `int` comparisons. – Bohemian Mar 01 '21 at 21:24
  • @Bohemian I *never* mentioned currency (that was @JimB). I said "money value" originally but removed that as confusing. – AJR Mar 01 '21 at 21:46

1 Answers1

2

If you're concerned about the extra multiplication, rather than rely on the optimizer, you can pull it out this way:

func r(f float64) float64 {
    if f >= 999.5 {
        return math.Round(f)
    }
    
    f *= 10
    if f >= 999.5 {
        return math.Round(f) / 10
    }
    
    f *= 10
    return math.Round(f) / 100
}
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thanks @Rob Napier that is a very good point. (I must have had a brain failure.) – AJR Mar 01 '21 at 21:26
  • 1
    @AJR: also note that divides are usually an oder of magnitude more expensive, so while this is better code organization, the concern about the second multiple is likely misplaced. – JimB Mar 01 '21 at 21:29
  • 1
    @JimB you are right that a further improvement would be to use * 0.1 instead of / 10 . I guess I was really most interested about what guarantees are given about optimisation in Go. – AJR Mar 01 '21 at 21:41
  • @AJR: the language makes no guarantees about the compiler optimizations. – JimB Mar 01 '21 at 21:48