Search code examples
javascriptfloating-pointinterval-arithmetic

Rounding Modes in JavaScript


This is a pretty niche question, but I'm implementing interval arithmetic in JS and I'd like to have correct rounding. Therefore, I need to be able to add two Numbers and have it round towards infinity, -infinity, zero, etc. As far as I can tell JS always rounds towards zero and this behavior isn't changeable, unlike in C/C++ with fesetround.

How can I get around this? I'm willing to have significant performance impacts if it means correct rounding; probably the feature will be toggleable to balance speed and correctness. Perhaps one way to do this would be to somehow make functions roundUp and roundDown which round up/down to the next float value.

As an example of how roundUp/roundDown could be implemented:


const floatStore = new Float64Array(1)

const intView = new Uint32Array(floatStore.buffer)

function roundUp(x) {
  if (x === Infinity)
    return Infinity
  if (x === -Infinity)
    return -Infinity
  if (isNaN(x))
    return NaN
    
  floatStore[0] = x

  let leastSignificantGroup = ++intView[0]

  if (leastSignificantGroup === 0)
    intView[1]++

  return floatStore[0]
}

(5.1).toPrecision(100) // -> 5.0999999999999996447...
roundUp(5.1).toPrecision(100) // -> 5.100000000000000532...

Solution

  • For anyone looking for a fast way to get consecutive floats, this works:

    const MAGIC_ROUND_C = 1.1113332476497816e-16 // just above machine epsilon / 2
    const POSITIVE_NORMAL_MIN = 2.2250738585072014e-308
    const POSITIVE_DENORMAL_MIN = Number.MIN_VALUE
    
    function roundUp (x) {
      if (x >= -POSITIVE_NORMAL_MIN && x < POSITIVE_NORMAL_MIN) {
        // denormal numbers
        return x + POSITIVE_DENORMAL_MIN
      } else if (x === -Infinity) {
        // special case
        return -Number.MAX_VALUE
      }
    
      return x + Math.abs(x) * MAGIC_ROUND_C
    }
    
    function roundDown (x) {
      if (x > -POSITIVE_NORMAL_MIN && x <= POSITIVE_NORMAL_MIN) {
        return x - POSITIVE_DENORMAL_MIN
      } else if (x === Infinity) {
        return Number.MAX_VALUE
      }
    
      return x - Math.abs(x) * MAGIC_ROUND_C
    }
    

    The only oddity is that it treats +0 and -0 as the same, so rounding them up both gives the minimum denormal, and same for rounding down.