11

Introduction

For some calculations I need to find the smallest possible number I can add/subtract from a specified number without JavaScript getting in trouble with the internal used data type.

Goal

I tried to write a function which is able to return the next nearest number to VALUE in the direction of value DIR.

function nextNearest(value, direction) {
    // Special cases for value==0 or value==direction removed

    if (direction < value) {
        return value - Number.MIN_VALUE;
    } else {
        return value + Number.MIN_VALUE;
    }
}

The problem with this is, that JavaScript uses a 64-bit float type (I think) which has different minimum step sizes depending on its current exponent.

Problem in detail

The problem is the step size depending on its current exponent:

var a = Number.MIN_VALUE;

console.log(a);
// 5e-324

console.log(a + Number.MIN_VALUE);
// 1e-323 (changed, as expected)


var a = Number.MAX_VALUE;

console.log(a);
// 1.7976931348623157e+308

console.log(a - Number.MIN_VALUE);
// 1.7976931348623157e+308 (that's wrong)

console.log(a - Number.MIN_VALUE == a);
// true (which also is wrong)

Summary

So how can I find the smallest possible number I can add/subtract from a value specified in a parameter in any direction? In C++ this would be easily possible by accessing the numbers binary values.

Benjamin Schulte
  • 863
  • 1
  • 6
  • 16
  • 3
    You can take advantage of [typed arrays](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays) to get at the low-level representation of floating point values in JavaScript. Make a 1-element Float64Array and an 8-element UInt8 array both over the same buffer. – Pointy Dec 26 '14 at 17:06

5 Answers5

8

I tried to implement Pointy's suggestion from the comments (using typed arrays). This is loosely adapted from glibc's implementation of nextafter. Should be good enough.

You can actually just increment/decrement the 64-bit integer representation of a double to get the wanted result. A mantissa overflow will overflow to the exponent which happens to be just what you want.

Since JavaScript doesn't provide a Uint64Array I had to implement a manual overflow over two 32-bit integers.

This works on little-endian architectures, but I've left out big-endian since I have no way to test it. If you need this to work on big-endian architectures you'll have to adapt this code.

// Return the next representable double from value towards direction
function nextNearest(value, direction) {
  if (typeof value != "number" || typeof direction != "number")
    return NaN;
  
  if (isNaN(value) || isNaN(direction))
    return NaN;
  
  if (!isFinite(value))
    return value;
  
  if (value === direction)
    return value;
  
  var buffer = new ArrayBuffer(8);
  var f64 = new Float64Array(buffer);
  var u32 = new Uint32Array(buffer);
  
  f64[0] = value;
  
  if (value === 0) {
    u32[0] = 1;
    u32[1] = direction < 0 ? 1 << 31 : 0;
  } else if ((value > 0) && (value < direction) || (value < 0) && (value > direction)) {
    if (u32[0]++ === 0xFFFFFFFF)
      u32[1]++;
  } else {
    if (u32[0]-- === 0)
      u32[1]--;
  }
  
  return f64[0];
}

var testCases = [0, 1, -1, 0.1,
                 -1, 10, 42e42,
                 0.9999999999999999, 1.0000000000000002,
                 10.00000762939453, // overflows between dwords
                 5e-324, -5e-324, // minimum subnormals (around zero)
                 Number.MAX_VALUE, -Number.MAX_VALUE,
                 Infinity, -Infinity, NaN];

document.write("<table><tr><th>n</th><th>next</th><th>prev</th></tr>");
testCases.forEach(function(n) {
  var next = nextNearest(n, Infinity);
  var prev = nextNearest(n, -Infinity);
  document.write("<tr><td>" + n + "</td><td>" + next + "</td><td>" + prev + "</td></tr>");
});
document.write("</table>");
Community
  • 1
  • 1
Lucas Trzesniewski
  • 50,214
  • 11
  • 107
  • 158
  • 1
    `DataView.getFloat()` has a `isLittleEndian` parameter which might help. http://www.ecma-international.org/ecma-262/6.0/#sec-dataview.prototype.getfloat64 – le_m Jun 16 '16 at 14:50
1

If you don't want the hacky way,

function precision( n ) { 
  return Math.max( Number.MIN_VALUE, 2 ** Math.floor( Math.log2( n ) ) * Number.EPSILON ) 
} 

works aswell.

This gives you the increment to the next (larger absolute value) floating point number. Stepping to zero always works as the precision increases.

Since Number.EPSILON is the floating point precision at n=1, we can from this calculate all epsilon values by using 2**x and log2() this function will return the smallest safe increment from a number for all floating point numbers. Only edge case are denormalized numbers (smaller than 2**-1023) where the step will be larger.

DrDesten
  • 11
  • 2
0

Number.MIN_VALUE is the smallest possible representable number, not the smallest possible difference between representable numbers. Because of the way javascript handles floating point numbers the smallest possible difference between representable numbers changes with the size of the number. As the number gets larger the precision gets smaller. Thus there is no one number that will solve your problem. I suggest you either rethink how you're going to solve your problem or choose a subset of numbers to use and not the full range of MAX and MIN values.

for example: 1.7976931348623156e+308 == 1.7976931348623155e+308 //true

Also, by subtracting MIN_VALUE from MAX_VALUE you're trying to get javascript to accurately represent a number with over 600 significant figures. That's a bit too much.

greggreg
  • 11,945
  • 6
  • 37
  • 52
  • 4
    You're just explaining the OP's problem here. For each finite number `x` there is one smallest number `e` such as `x+e != e || x-e != e`. The OP is asking how to find `e` for a given number `x`. – Lucas Trzesniewski Dec 26 '14 at 17:13
  • Sure, but there is no answer to this problem that seems reasonable. Up at Max_Value you need at least 2. Down at min value if you add 2 you get 2. So you effectively lose all distinction between very small numbers and have to skip integers with the very big. This problem has no useful solution that doesn't include a sliding scale of values. – greggreg Dec 26 '14 at 17:21
  • Yes that's what I'm trying to say. My *number* `e` above is really a function... Write a `function epsilon(x)` such as `x + epsilon(x) != x`. The *epsilon* depends on the scale of `x`. – Lucas Trzesniewski Dec 26 '14 at 18:25
0

To find the (smallest) increment value of a given float:

For example, useful to set the step attribute of an html input type=number on the fly!

function getIncrement(number) {
  function getDecimals(number) {
    let d = parseFloat(number).toString().split('.')[1]
    if (d) {return d.length}
    return 0
  }
  return (0 + "." + Array(getDecimals(number)-1).fill(0).join("") + "1").toLocaleString('fullwide', {useGrouping:false})
}

// Tests
console.log(
  getIncrement(0.00000105),
  getIncrement(455.105),
  getIncrement(455.050000)
)

// Test
// Create input type number
function createInput(value){
  let p = document.createElement("input")
  p.type = "number"
  p.step = getIncrement(value)
  p.min = 0
  p.max = 1000
  p.value = value
  panel.appendChild(p)
}

// Tests 
createInput(0.00000105)
createInput(455.105)
createInput(455.050000)
<b>TEST.</b><br>
<div id="panel"></div>

The step attribute is set based on the base value increment.
<hr>


<b>TEST DEFAULT BEHAVIOR. </b><br>
<input type="number" min="0" max="100" value="0.00000105">
<input type="number" step="0.001" min="0" max="100" value="0.00000105">
<input type="number" step="0.000001" min="0" max="100" value="0.00000105">
<br>
This is not usable. The step atttribute needs to be set based on the value it will contains. But this require to know the value in advance and if a new dynamic value has a different decimal place, it become also unusable.

Get only the decimal place.

function getDecimalplace(number) {
  let d = parseFloat(number).toString().split('.')[1]
  return d.length
}

// Tests 
console.log(
  getDecimalplace(8.0001) // 4
)  

console.log(
  getDecimalplace(4.555433) // 6
)
NVRM
  • 11,480
  • 1
  • 88
  • 87
0

Here's a quick and dirty method using Math.log2() that doesn't require heap allocation and is much faster:

function getNext(num) {
    return num + 2 ** (Math.log2(num) - 52);
}

or

function getPrevious(num) {
    return num - 2 ** (Math.log2(num) - 52);
}

The disadvantage is that it's not perfect, just very close. It doesn't work well for absurdly small values as a result. (num < 2 ** -1000)

It's possible that there's failure case for a number > 9.332636185032189e-302, but I haven't found one. There are failure cases for numbers smaller than the above, for example:

console.log(getNext(Number.MIN_VALUE) === Number.MIN_VALUE); // true

However, the following works just fine:

let counter = 0;

// 2 ** -1000 === 9.332636185032189e-302
for (let i = -1000; i < 1000; i += 0.00001) {
    counter++;
    if (getNext(2 ** i) === 2 ** i)
        throw 'err';
    if (getPrevious(2 ** i) === 2 ** i)
        throw 'err';

}

console.log(counter.toLocaleString()); // 200,000,001

But note that "a bunch of test cases work" is not a proof of correctness.

Joshua Wade
  • 4,755
  • 2
  • 24
  • 44
  • It's worth mentioning: the performance comparison above isn't completely fair as it doesn't remove dead weight from the typed array test. That said, given how much the two tests differ, I suspect the typed array test will remain slower even if perfectly optimized. – Joshua Wade Jun 09 '20 at 23:02