Not all adjacent representable numbers are the same mathematical distance from one another. Floating point arcana isn't my strong suit, but if you want to find the next representable number, I think you need to keep increasing what you add/subtract from it by Number.EPSILON
for as long as you keep getting the same number.
The very naive, simplistic approach would look like this (but keep reading):
// DON'T USE THIS
function next(x) {
const ep = x < 0 ? -Number.EPSILON : Number.EPSILON;
let adder = ep;
let result;
do {
result = x + adder;
adder += ep;
} while (result === x);
return result;
}
console.log(`Next for -3: ${next(-3)}`);
console.log(`Next for 5: ${next(5)}`);
(That's assuming direction based on the sign of the number given, which is probably not what you really want, but is easily switched up.)
But, that would take hours (at least) to handle next(Number.MAX_SAFE_INTEGER)
.
When I posted my caveat on the above originally, I said a better approach would take the magnitude of x
into account "...or do bit twiddling (which definitely takes us into floating point arcana land)..." and you pointed to Java's Math.nextAfter
operation, so I had to find out what they do. And indeed, it's bit twiddling, and it's wonderfully simple. Here's a re-implementation of the OpenJDK's version from here (the line number in that link will rot):
// A JavaScript implementation of OpenJDK's `Double.nextAfter` method.
function nextAfter(start, direction) {
// These arrays share their underlying memory, letting us use them to do what
// Java's `Double.doubleToRawLongBits` and `Double.longBitsToDouble` do.
const f64 = new Float64Array(1);
const b64 = new BigInt64Array(f64.buffer);
// Comments from https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/lang/Math.java:
/*
* The cases:
*
* nextAfter(+infinity, 0) == MAX_VALUE
* nextAfter(+infinity, +infinity) == +infinity
* nextAfter(-infinity, 0) == -MAX_VALUE
* nextAfter(-infinity, -infinity) == -infinity
*
* are naturally handled without any additional testing
*/
/*
* IEEE 754 floating-point numbers are lexicographically
* ordered if treated as signed-magnitude integers.
* Since Java's integers are two's complement,
* incrementing the two's complement representation of a
* logically negative floating-point value *decrements*
* the signed-magnitude representation. Therefore, when
* the integer representation of a floating-point value
* is negative, the adjustment to the representation is in
* the opposite direction from what would initially be expected.
*/
// Branch to descending case first as it is more costly than ascending
// case due to start != 0.0d conditional.
if (start > direction) {
// descending
if (start !== 0) {
f64[0] = start;
const transducer = b64[0];
b64[0] = transducer + (transducer > 0n ? -1n : 1n);
return f64[0];
} else {
// start == 0.0d && direction < 0.0d
return -Number.MIN_VALUE;
}
} else if (start < direction) {
// ascending
// Add +0.0 to get rid of a -0.0 (+0.0 + -0.0 => +0.0)
// then bitwise convert start to integer.
f64[0] = start + 0;
const transducer = b64[0];
b64[0] = transducer + (transducer >= 0n ? 1n : -1n);
return f64[0];
} else if (start == direction) {
return direction;
} else {
// isNaN(start) || isNaN(direction)
return start + direction;
}
}
function test(start, direction) {
const result = nextAfter(start, direction);
console.log(`${start} ${direction > 0 ? "up" : "down"} is ${result}`);
}
test(-3, -Infinity);
test(5, Infinity);
test(Number.MAX_SAFE_INTEGER, Infinity);
test(Number.MAX_SAFE_INTEGER + 2, Infinity);