31

For example, I want to show a list of buttons from 0,0.5,... 5, which jumps for each 0.5. I use a for loop to do that, and have different color at button STANDARD_LINE:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0;i<=MAX;i=i+DIFF){
    button.text=i+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

At this case there should be no rounding errors as each value is exact in IEEE 754.But I'm struggling if I should change it to avoid floating point equality comparison:

var MAX=10;
var STANDARD_LINE=3;

for(var i=0;i<=MAX;i++){
    button.text=i/2.0+'';
    if(i==STANDARD_LINE/2.0){
      button.color='red';
    }
}

On one hand, the original code is more simple and forward to me. But there is one thing I'm considering : is i==STANDARD_LINE misleads junior teammates? Does it hide the fact that floating point numbers may have rounding errors? After reading comments from this post:

https://stackoverflow.com/questions/33646148/is-hardcode-float-precise-if-it-can-be-represented-by-binary-format-in-ieee-754

it seems there are many developers don't know some float numbers are exact. Should I avoid float number equality comparisons even if it is valid in my case? Or am I over thinking about this?

16
  • 24
    The behavior of these two code listings is not equivalent. 3/2.0 is 1.5 but i will only ever be whole numbers in the second listing. Try removing the second /2.0. Jan 31, 2018 at 7:52
  • 29
    If you absolutely need to compare two FPs for equality (which isn't required as others pointed out in their fine answers since you can just use a loop counter comparison with integers), but if you did, then a comment ought to suffice. Personally I've been working with IEEE FP for a long time and I'd still be confused if I saw, say, direct SPFP comparison without any kind of comment or anything. It's just very delicate code -- worth a comment at least every time IMHO.
    – user204677
    Jan 31, 2018 at 8:04
  • 14
    Regardless of which you choose, this is one of those cases where a comment explaining the how and why is absolutely essential. A later developer may not even consider the subtleties without a comment to bring them to their attention. Also, I'm heavily distracted by the fact that button doesn't change anywhere in your loop. How is the list of buttons accessed? Via index into array or some other mechanism? If it's by index access into an array, this is another argument in favor of switching to integers.
    – jpmc26
    Jan 31, 2018 at 12:00
  • 10
    Write that code. Until someone thinks that 0.6 would be a better step size and simply changes that constant.
    – tofro
    Jan 31, 2018 at 17:15
  • 13
    "...mislead junior developers" You will mislead senior developers also. Despite the amount of thought you have put into this, they will assume that you didn't know what you were doing, and will likely change it to the integer version anyway. Feb 1, 2018 at 7:20

9 Answers 9

117

I would always avoid successive floating-point operations unless the model I'm computing requires them. Floating-point arithmetic is unintuitive to most and a major source of errors. And telling the cases in which it causes errors from those where it doesn't is an even more subtle distinction!

Therefore, using floats as loop counters is a defect waiting to happen and would require at the very least a fat background comment explaining why it's okay to use 0.5 here, and that this depends on the specific numeric value. At that point, rewriting the code to avoid float counters will probably be the more readable option. And readability is next to correctness in the hierarchy of professional requirements.

4
  • 50
    I like "a defect waiting to happen". Sure, it might work now, but a light breeze from someone walking by will break it.
    – AakashM
    Jan 31, 2018 at 10:39
  • 11
    For example, suppose the requirements change so that instead of 11 equally spaced buttons from 0 to 5 with the "standard line" on the 4th button, you have 16 equally-spaced buttons from 0 to 5 with the "standard line" on the 6th button. So whoever inherited this code from you changes 0.5 to 1.0/3.0 and changes 1.5 to 5.0/3.0. What happens then?
    – David K
    Jan 31, 2018 at 12:49
  • 9
    Yeah, I'm uncomfortable with the idea that changing what seems to be an arbitrary number (as "normal" as a number could be) to another arbitrary number (that seems equally "normal") actually introduces a defect.
    – Alexander
    Jan 31, 2018 at 15:06
  • 9
    @Alexander: right, you'd need a comment that said DIFF must be an exactly-representable double that evenly divides STANDARD_LINE. If you don't want to write that comment (and rely on all future developers to know enough about IEEE754 binary64 floating point to understand it), then don't write the code this way. i.e. don't write the code this way. Especially because it's probably not even more efficient: FP addition has higher latency than integer addition, and it's a loop-carried dependency. Also, compilers (even JIT compilers?) probably do better at making loops with integer counters. Feb 1, 2018 at 12:04
41

As a general rule, loops should be written in such a way as to think of doing something n times. If you're using floating point indices, it is no longer a question of doing something n times but rather running until a condition is met. If this condition happens to be very similar to the i<n that so many programmers expect, then the code appears to be doing one thing when it is actually doing another which can be easily misinterpreted by programmers skimming the code.

It's somewhat subjective, but in my humble opinion, if you can rewrite a loop to use an integer index to loop a fixed number of times, you should do so. So consider the following alternative:

var DIFF=0.5;                           // pixel increment
var MAX=Math.floor(5.0/DIFF);           // 5.0 is max pixel width
var STANDARD_LINE=Math.floor(1.5/DIFF); // 1.5 is pixel width

for(var i=0;i<=MAX;i++){
    button.text=(i*DIFF)+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

The loop works in terms of whole numbers. In this case i is an integer and STANDARD_LINE is coerced to an integer as well. This would of course change the position of your standard line if there happened to be roundoff and likewise for MAX, so you should still strive to prevent roundoff for accurate rendering. However you still have the advantage of changing parameters in terms of pixels and not whole numbers without having to worry about the comparison of floating points.

5
  • 3
    You may also want to consider rounding instead of flooring in the assignments, depending on what you want. If the division is supposed to give an integer result, floor might give surprises if you stumble upon numbers where the division happens to be slightly off.
    – ilkkachu
    Jan 31, 2018 at 12:51
  • 1
    @ilkkachu True. My thoughts were that if you're setting 5.0 as the maximum pixel amount, then through rounding, you'd preferably like to be on the lower side of that 5.0 rather than slightly more. 5.0 would effectively be a maximum. Though rounding may be preferable according to what you need to do. In either case, it makes little difference if the division creates a whole number anyway.
    – Neil
    Jan 31, 2018 at 12:53
  • 4
    I strongly disagree. The best way to stop a loop is by the condition that most naturally expresses the business logic. If the business logic is that you need 11 buttons, the loop should stop at iteration 11. If the business logic is that the buttons are 0.5 apart until the line is full, the loop should stop when the line is full. There are other considerations that can push the choice toward one mechanism or the other but absent those considerations, choose the mechanism that closest matches the business requirement. Jan 31, 2018 at 19:04
  • Your explanation would be completely correct for Java/C++/Ruby/Python/... But Javascript doesn't have integers, so i and STANDARD_LINE only look like integers. There's no coercion at all, and DIFF, MAX and STANDARD_LINE are all just Numbers. Numbers used as integers should be safe below 2**53, they're still floating point numbers, though. Feb 1, 2018 at 8:17
  • @EricDuminil Yes, but this is the half of it. The other half is readability. I do mention it as the principle reason for doing it this way, not for optimization.
    – Neil
    Feb 1, 2018 at 8:40
21

I agree with all the other answers that using a non-integer loop variable is generally bad style even in cases like this one where it will work correctly. But it seems to me that there's another reason why it's bad style here.

Your code "knows" that the available line widths are precisely the multiples of 0.5 from 0 up to 5.0. Should it? It seems like that's a user-interface decision that might easily change (e.g., maybe you want the gaps between available widths to become larger as the widths do. 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0 or something).

Your code "knows" that the available line widths all have "nice" representations both as floating-point numbers and as decimals. That also seems like something that might change. (You might want 0.1, 0.2, 0.3, ... at some point.)

Your code "knows" that the text to put on the buttons is simply what Javascript turns those floating-point values into. That also seems like something that might change. (E.g., perhaps some day you'll want widths like 1/3, which you probably wouldn't want to display as 0.33333333333333 or whatever. Or perhaps you want to see "1.0" instead of "1" for consistency with "1.5".)

These all feel to me like manifestations of a single weakness, which is a sort of mixing of layers. Those floating-point numbers are part of the internal logic of the software. The text shown on the buttons is part of the user interface. They should be more separate than they are in the code here. Notions like "which of these is the default that should be highlighted?" are user-interface matters, and they probably shouldn't be tied to those floating-point values. And your loop here is really (or at least should be) a loop over buttons, not over line widths. Written that way, the temptation to use a loop variable taking non-integer values disappears: you'd just be using successive integers or a for...in/for...of loop.

My feeling is that most cases where one might be tempted to loop over non-integer numbers are like this: there are other reasons, entirely unrelated to numerical issues, why the code should be organized differently. (Not all cases; I can imagine some mathematical algorithms might be most neatly expressed in terms of a loop over non-integer values.)

8

One code smell is using floats in loop like that.

Looping can be done in a lot of ways, but in 99.9% of the cases you should stick to an increment of 1 or there will definitely be confusion, not only by junior developers.

1
  • I disagree, I think integer multiples of 1 are not confusing in a for-loop. I wouldn't consider that a code smell. Only fractions.
    – CodeMonkey
    Feb 2, 2018 at 7:12
3

Yes, you do want to avoid this.

Floating point numbers are one of the biggest trap for the unsuspecting programmer (which means, in my experience, almost everybody). From depending on floating point equality tests, to representing money as floating point, it's all a big quagmire. Adding up one float upon the other is one of the biggest offenders. There are whole volumes of scientific literature about things like this.

Use floating point numbers exactly in places where they are appropriate, for example when doing actual mathematical calculations where you need them (like trigonometry, plotting function graphs etc.) and be super careful when doing serial operations. Equality is right out. Knowledge about which particular set of numbers is exact by IEEE standards is very arcane and I would never depend on it.

In your case, there will, by Murphys Law, come the point where management wants you to not have 0.0, 0.5, 1.0 ... but 0.0, 0.4, 0.8 ... or whatever; you you will be immediately borked, and your junior programmer (or yourself) will debug long and hard until you find the problem.

In your particular code, I would indeed have an integer loop variable. It represents the ith button, not the running number.

And I would probably, for the sake of extra clarity, not write i/2 but i*0.5 which makes it abundantly clear what is going on.

var BUTTONS=11;
var STANDARD_LINE=3;

for(var i=0; i<BUTTONS; i++) {
    button.text = (i*0.5)+'';
    if (i==STANDARD_LINE) {
      button.color='red';
    }
}

Note: as pointed out in the comments, JavaScript does not actually have a separate type for integers. But integers up to 15 digits are guaranteed to be accurate/safe (see https://www.ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer ), hence for arguments like this ("is it more confusing/error prone to work with integers or non-integers") this is appropriately close to having a separate type "in spirit"; in daily use (loops, screen coordinates, array indices etc.) there will be no surprises with integer numbers represented as Number as JavaScript.

2
  • I'd change the name BUTTONS to something else - there are 11 buttons after all and not 10. Maybe FIRST_BUTTON = 0, LAST_BUTTON = 10, STANDARD_LINE_BUTTON = 3. Apart from that, yes, that's how you should do it.
    – gnasher729
    Jan 31, 2018 at 23:22
  • It is true, @EricDuminil, and I have added a bit about this into the answer. Thank you!
    – AnoE
    Feb 1, 2018 at 11:19
1

I don't think that either of your suggestions is good. Instead, I would introduce a variable for the number of buttons based on the maximum value and the spacing. Then, it is simple enough to loop over the indices of the button themselves.

function precisionRound(number, precision) {
  let factor = Math.pow(10, precision);
  return Math.round(number * factor) / factor;
}

var maxButtonValue = 5.0;
var buttonSpacing = 0.5;

let countEstimate = precisionRound(maxButtonValue / buttonSpacing, 5);
var buttonCount = Math.floor(countEstimate) + 1;

var highlightPosition = 3;
var highlightColor = 'red';

for (let i=0; i < buttonCount; i++) {
    let buttonValue = i / buttonSpacing;
    button.text = buttonValue.toString();
    if (i == highlightPosition) {
        button.color = highlightColor;
    }
}

It might be more code, but it is also more readable and more robust.

0

You can avoid the whole thing by computing the value you are showing rather than using the loop counter as the value:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0; (i*DIFF) < MAX ; i=i+1){
    var val = i * DIFF

    button.text=val+'';

    if(val==STANDARD_LINE){
      button.color='red';
    }
}
1
  • You need to change MAX = DIFF * 10 and STANDARD_LINE = DIFF * 3, or it will break if DIFF is changed.
    – gnasher729
    Oct 17, 2022 at 7:16
0

I would display buttons at positions numbered first = 0 to last = 10, starting at start = 0.0 and ending at end = 5.0.

So button #3 is red. And button #i is displayed at start + (end - start) / (last -first) * (i - first).

Now you have zero problems if you change the number of buttons, their numbering, their positions a distances.

The original code works right now because of which constants were used. But it can break in many ways if you change the constant. There is no guarantee that 3*0.4 == 1.2, for example. (Bizarrely you can prove that x+x+x == 3 * x, but that would break for the next button).

-1

Floating point arithmetic is slow and integer arithmetic is fast, so when I use floating point, I would not use it unnecessarily where integers can be used. It is useful to always think of floating point numbers, even constants, as approximate, with some small error. It is very useful during debugging to replace native floating point numbers with plus/minus floating point objects where you treat each number as a range instead of a point. That way you uncover progressive growing inaccuracies after each arithmetic operation. So "1.5" should be thought of as "some number between 1.45 and 1.55", and "1.50" should be thought of as "some number between 1.495 and 1.505".

5
  • 5
    The performance difference between integers and floats is important when writing C code for a small microprocessor, but modern x86-derived CPUs do floating point so fast that any penalty is easily eclipsed by the overhead of using a dynamic language. In particular, does not Javascript actually represent every number as floating-point anyway, using the NaN payload when necessary? Jan 31, 2018 at 23:04
  • 1
    "Floating point arithmetic is slow and integer arithmetic is fast" is a historical truism that you should not retain as gospel moving forward. To add to what @leftaroundabout said, it's not just true that the penalty would be almost irrelevant, you may well find floating-point operations to be faster than their equivalent integer operations, thanks to the magic of autovectorizing compilers and instruction sets that can crunch large amounts of floats in one cycle. For this question it's not relevant, but the basic "integer is faster than float" assumption hasn't been true for quite a while. Feb 1, 2018 at 11:01
  • 1
    @JeroenMostert SSE/AVX have vectorised operations for both integers and floats, and you may be able to use smaller integers (because no bits are wasted on exponent), so in principle one might often still squeeze out more performance from highly-optimised integer code than with floats. But again, this isn't relevant for most applications and definitely not for this question. Feb 1, 2018 at 11:12
  • 1
    @leftaroundabout: Sure. My point wasn't about which one is definitely faster in any given situation, just that "I know FP is slow and integer is fast so I'll use integers if at all possible" isn't a good motivation even before you tackle the question of whether the thing you're doing needs optimization. Feb 1, 2018 at 11:19
  • Putting a button on a screen takes ages compared to calculating its position. So performance is a complete non-argument.
    – gnasher729
    Oct 16, 2022 at 20:48

Not the answer you're looking for? Browse other questions tagged or ask your own question.