4

Here is a sample function:

function step(x, min, max) {
  return x >= min && x <= max ? x : 0;
}

console.log(step(-3 - Number.EPSILON, -3, 5)); // Expected 0, actual -3
console.log(step(5 + Number.EPSILON, -3, 5)); // Expected 0, actual 5

I need to check, that it returns zero for values outside [min, max] interval. For sure I can subtract/add a bigger number, for example 1. But I'm pretty sure, that there should exist a function returning previous/next floating point number. Could you please suggest the function or how to implement it?

Denis
  • 1,167
  • 1
  • 10
  • 30
  • 1
    *"But I'm pretty sure, that there should exist a function returning previous/next floating point number."* What makes you think that? It would almost certainly be on [`Math`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math) if so, and there isn't one there, so... – T.J. Crowder May 10 '22 at 11:11
  • 2
    I don't understand your code. What is the given number in your code? – jabaa May 10 '22 at 11:12
  • Why is this function called `step`? I think I'm missing something... – T.J. Crowder May 10 '22 at 11:14
  • Would clamp cut it together with `Math.round()`? https://stackoverflow.com/questions/11409895/whats-the-most-elegant-way-to-cap-a-number-to-a-segment – E S May 10 '22 at 11:14
  • @T.J.Crowder https://en.wikipedia.org/wiki/Step_function – Denis May 10 '22 at 11:17
  • Your code is not the step function. The step function doesn't return `0` or `x`. It returns a rounded `x`. Why do you need the next floating point in a step function? – jabaa May 10 '22 at 11:19
  • @T.J.Crowder Remembered, I saw this function in other languages, for example in Java https://docs.oracle.com/javase/8/docs/api/java/lang/Math.html#nextAfter-double-double- I was sure that JavaScript has such a function too, but it seems I'm wrong – Denis May 10 '22 at 11:20

1 Answers1

8

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);
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Thanks a lot! The simple fix for big scales is to use the follwoing increment `const ep = x * Number.EPSILON` or maybe `const ep = x * Number.EPSILON / 1000` However it doesn't guarantee that the increment is smallest possible. It seems that Java and C++ implementations converts a number to a binnary representation and adds 1 to the lowerest bit. But I guess it's not trivial to implement in JavaScript. – Denis May 10 '22 at 11:49
  • @Denis - If it were **always** adding 1 to the lowest bit, that would be fairly easy to implement in JavaScript, but I think it's much more complicated than that in reality, because of how the format works. (I could be wrong, though.) Now I'm curious and looking at Java's `nextAfter`... :-) – T.J. Crowder May 10 '22 at 11:54
  • 1
    @Denis - I just had to look to see how it worked, The OpenJDK's source shows it really is very nearly as simple as adding one to the 64-bit two's complement interpretation of the double's bits. I've updated the answer with the implementation. – T.J. Crowder May 10 '22 at 12:38
  • 1
    It's really cool! Thanks! – Denis May 10 '22 at 13:02