I'm not sure if I'm using the right terminology, but occasionally I find myself needing to canonicalize a floating-point value to a range in a cyclic manner. (This can be useful, for instance, for values that represent rotations, to special-case rotations by whole quadrants or eliminate redundant rotations by whole turns.)
For example, assuming a cycle range of 1.0, the result should be: 0 <= result < 1.0.
Inputs 0.25, 1.25, 2.25 and so on should all become 0.25.
Of course, this is what the modulo operator % supposedly does:
System.out.println(1.25 % 1); // 0.25
But it can still produce a negative result for negative input:
System.out.println(-1.75 % 1); // -0.75, rather than 0.25 as wanted
So for years I've been writing this pattern whenever I needed this functionality:
x %= range;
if (x < 0) x += range;
Today I discovered a bug: starting from very small negative inputs (e.g., x = -1e-20), it turns out that the floating-point result of x += range rounds up to exactly range, so, out of range.
Which has led to this:
static double cyclic(double x, double range) {
x %= range;
if (x < 0) {
x += range;
if (x >= range) x = 0;
}
return x;
}
Even though each line has a justification for being there, the whole is grotesque. (And it comes in two flavors, one for float and one for double, for double the grotesquery.)
I would like to know:
Is my "cyclic" function above finally robust against all floating-point oddities?
Is there a nicer solution? This doesn't feel like it should be so complex. I feel like I'm missing a trick.
The known workaround is to use a modulo that round the quotient to nearest integer, instead of truncating the quotient.
The result is a modulo in interval (-range/2,range/2] or [-range/2,range/2) depending on the parity of the quotient (odd or even).
This is the purpose of IEEE754 remainder function, std::remainder in C++, IEEEremainder in java (https://www.w3schools.com/java/ref_math_ieeeremainder.asp) function name may vary in other languages/math libraries
A correct implementation of such remainder function shall be exact (not subject to rounding errors).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With