4

Given a 64-bit (8-byte) little-endian ArrayBuffer of bytes, how can we read 64-bit integer values in JavaScript?

I experimented and came up with this, but is there a more elegant solution given that DataView does not yet provide getUint64()?

const bytes = new Uint8Array([ 0xff,0xff,0xff,0xff,   0xff,0xff,0xff,0xff ]);
//                             [-----  left  -----]   [----  right  ----] 

const view = new DataView(bytes.buffer);

// split 64-bit number into two 32-bit numbers
const left = view.getUint32(0, true);  // 4294967295
const right = view.getUint32(4, true); // 4294967295

// combine the 2 32-bit numbers using max 32-bit val 0xffffffff
const combined = left + 2**32*right;

console.log('combined', combined);
// 18,446,744,073,709,552,000  is returned Javascript for "combined"
// 18,446,744,073,709,551,615  is max uint64 value
// some precision is lost since JS doesn't support 64-bit ints, but it's close enough
anthumchris
  • 8,245
  • 2
  • 28
  • 53
  • 1
    JS doesn't have 64 bit integers only 32 bit ones and 64 bit floats. – Sebastian Speitel Nov 01 '18 at 14:51
  • 1
    Thanks, I'm aware and curious how to make the code more concise, perhaps with bitwise? – anthumchris Nov 01 '18 at 14:59
  • 2
    Wouldn't be `sum = 2**32*right + left` enough? – Sebastian Speitel Nov 01 '18 at 15:01
  • If you need the precision you can use the new BigInt. Reference: https://developers.google.com/web/updates/2018/05/bigint – Sebastian Speitel Nov 01 '18 at 15:03
  • As `getUint32()` always gets a non-negative number, luckily the bogus part of the ternary will never kick in. Still, you could remove it completely, or at least make it `==` – tevemadar Nov 01 '18 at 15:10
  • Are you still looking for an answer after the most recent edit…? – Ry- Nov 01 '18 at 16:43
  • @SebastianSpeitel Great suggestion with `**` which also fixes a bug in my code where sum was -1 off. Current precision is good enough for extracting 64-bit Ogg [`granule_position`](https://tools.ietf.org/html/rfc3533#section-6) – anthumchris Nov 01 '18 at 16:45
  • `BigInt` would be great but is not yet backwards compatible with Firefox. @Ry: sure, I just posted an answer with prototype function based on current suggestions. – anthumchris Nov 01 '18 at 18:46

2 Answers2

4

Based on the original experiment and Sebastian Speitel's suggestion/fix, this function returns a 64-bit value until precision is lost after Number.MAX_SAFE_INTEGER

DataView.prototype.getUint64 = function(byteOffset, littleEndian) {
  // split 64-bit number into two 32-bit parts
  const left =  this.getUint32(byteOffset, littleEndian);
  const right = this.getUint32(byteOffset+4, littleEndian);

  // combine the two 32-bit values
  const combined = littleEndian? left + 2**32*right : 2**32*left + right;

  if (!Number.isSafeInteger(combined))
    console.warn(combined, 'exceeds MAX_SAFE_INTEGER. Precision may be lost');

  return combined;
}


Tested below:

// [byteArray, littleEndian, expectedValue]
const testValues = [
  // big-endian
  [new Uint8Array([0x00, 0x00, 0x00, 0x00,  0x00, 0x00, 0x00, 0xff]),  false, 255], 
  [new Uint8Array([0x00, 0x00, 0x00, 0x00,  0x00, 0x00, 0xff, 0xff]),  false, 65535],
  [new Uint8Array([0x00, 0x00, 0x00, 0x00,  0xff, 0xff, 0xff, 0xff]),  false, 4294967295],
  [new Uint8Array([0x00, 0x00, 0x00, 0x01,  0x00, 0x00, 0x00, 0x00]),  false, 4294967296],
  [new Uint8Array([0x00, 0x1f, 0xff, 0xff,  0xff, 0xff, 0xff, 0xff]),  false, 9007199254740991], // maximum precision
  [new Uint8Array([0x00, 0x20, 0x00, 0x00,  0x00, 0x00, 0x00, 0x00]),  false, 9007199254740992], // precision lost
  [new Uint8Array([0x00, 0x20, 0x00, 0x00,  0x00, 0x00, 0x00, 0x01]),  false, 9007199254740992], // precision lost

  // little-endian
  [new Uint8Array([0xff, 0x00, 0x00, 0x00,  0x00, 0x00, 0x00, 0x00]),  true, 255], 
  [new Uint8Array([0xff, 0xff, 0x00, 0x00,  0x00, 0x00, 0x00, 0x00]),  true, 65535],
  [new Uint8Array([0xff, 0xff, 0xff, 0xff,  0x00, 0x00, 0x00, 0x00]),  true, 4294967295],
  [new Uint8Array([0x00, 0x00, 0x00, 0x00,  0x01, 0x00, 0x00, 0x00]),  true, 4294967296],
  [new Uint8Array([0x00, 0x00, 0x00, 0x00,  0x00, 0x01, 0x00, 0x00]),  true, 1099511627776],
  [new Uint8Array([0x00, 0x00, 0x00, 0x00,  0x00, 0x00, 0x01, 0x00]),  true, 281474976710656],
  [new Uint8Array([0xff, 0xff, 0xff, 0xff,  0xff, 0xff, 0x1f, 0x00]),  true, 9007199254740991], // maximum precision
];

testValues.forEach(testGetUint64);

function testGetUint64([bytes, littleEndian, expectedValue]) {
  const val = new DataView(bytes.buffer).getUint64(0, littleEndian);
  console.log(val === expectedValue? 'pass' : 'FAIL. expected '+expectedValue+', received '+val);
}
anthumchris
  • 8,245
  • 2
  • 28
  • 53
  • Thanks for this. I was finding in my case that I wasn't getting the level of precision I needed, but I was able to achieve it with BigInts. Just replace the const combined declaration line with this: const combined = littleEndian ? BigInt(left) + 2n*_*32n*BigInt(right) : 2n*_*32n *BigInt(left)+ BigInt(right); – Joe Coyle Jun 21 '19 at 21:56
  • Yes, BigInt is a great option for the newer browsers https://caniuse.com/#search=bigint – anthumchris Jun 22 '19 at 03:39
2

Some browsers are starting to support the experimental BigInt global object:

BigInt is a built-in object that provides a way to represent whole numbers larger than 253, which is the largest number JavaScript can reliably represent with the Number primitive.

If you are only targeting these browsers then you can use this to get larger values than can be supported by a Number. Additionally, Chrome currently supports the DataView.getBigInt64( position, littleEndian ) and DataView.getBigUint64( position, littleEndian ) functions that return BigInt values.

Read 64-bit unsigned values

function getBigUint64( view, position, littleEndian = false )
{
  if ( "getBigUint64" in DataView.prototype )
  {
    return view.getBigUint64( position, littleEndian );
  }
  else
  {
    const lsb = BigInt( view.getUint32( position + (littleEndian ? 0 : 4), littleEndian ) );
    const gsb = BigInt( view.getUint32( position + (littleEndian ? 4 : 0), littleEndian ) );
    return lsb + 4294967296n * gsb;
  }
}

Read 64-bit signed values:

function getBigInt64( view, position, littleEndian = false )
{
  if ( "getBigInt64" in DataView.prototype )
  {
    return view.getBigInt64( position, littleEndian );
  }
  else
  {
    let value = 0n;
    let isNegative = ( view.getUint8( position + ( littleEndian ? 7 : 0 ) ) & 0x80 ) > 0;
    let carrying = true;
    for ( let i = 0; i < 8; i++ )
    {
      let byte = view.getUint8( position + ( littleEndian ? i : 7 - i ) );
      if ( isNegative )
      {
        if ( carrying )
        {
          if ( byte != 0x00 )
          {
            byte = (~(byte - 1))&0xFF;
            carrying = false;
          }
        }
        else
        {
          byte = (~byte)&0xFF;
        }
      }
      value += BigInt(byte) * 256n**BigInt(i);
    }
    if ( isNegative )
    {
      value = -value;
    }
    return value;
  }
}

Tests:

function getBigInt64( view, position, littleEndian = false )
{
  if ( "getBigInt64" in DataView.prototype )
  {
    return view.getBigInt64( position, littleEndian );
  }
  else
  {
    let value = 0n;
    let isNegative = ( view.getUint8( position + ( littleEndian ? 7 : 0 ) ) & 0x80 ) > 0;
    let carrying = true;
    for ( let i = 0; i < 8; i++ )
    {
      let byte = view.getUint8( position + ( littleEndian ? i : 7 - i ) );
      if ( isNegative )
      {
        if ( carrying )
        {
          if ( byte != 0x00 )
          {
            byte = (~(byte - 1))&0xFF;
            carrying = false;
          }
        }
        else
        {
          byte = (~byte)&0xFF;
        }
      }
      value += BigInt(byte) * 256n**BigInt(i);
    }
    if ( isNegative )
    {
      value = -value;
    }
    return value;
  }
}

// [byteArray, littleEndian, expectedValue]
const testValues = [
  // big-endian
  [new Uint8Array([0x00, 0x00, 0x00, 0x00,  0x00, 0x00, 0x00, 0xff]),  false, 255n], 
  [new Uint8Array([0x00, 0x00, 0x00, 0x00,  0x00, 0x00, 0xff, 0xff]),  false, 65535n],
  [new Uint8Array([0x00, 0x00, 0x00, 0x00,  0xff, 0xff, 0xff, 0xff]),  false, 4294967295n],
  [new Uint8Array([0x00, 0x00, 0x00, 0x01,  0x00, 0x00, 0x00, 0x00]),  false, 4294967296n],
  [new Uint8Array([0x00, 0x1f, 0xff, 0xff,  0xff, 0xff, 0xff, 0xff]),  false, 9007199254740991n],
  [new Uint8Array([0x00, 0x20, 0x00, 0x00,  0x00, 0x00, 0x00, 0x00]),  false, 9007199254740992n],
  [new Uint8Array([0x00, 0x20, 0x00, 0x00,  0x00, 0x00, 0x00, 0x01]),  false, 9007199254740993n],
  [new Uint8Array([0x7F, 0xFF, 0xFF, 0xFF,  0xFF, 0xFF, 0xFF, 0xFF]),  false, (2n**63n)-1n],
  [new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF,  0xFF, 0xFF, 0xFF, 0xFF]),  false, -1n],
  [new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF,  0xFF, 0xFF, 0xFF, 0x00]),  false, -256n],
  [new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF,  0xFF, 0xFF, 0xFE, 0xFF]),  false, -257n],
  [new Uint8Array([0x80, 0x00, 0x00, 0x00,  0x00, 0x00, 0x00, 0x00]),  false, -(2n**63n)],

  // little-endian
  [new Uint8Array([0xff, 0x00, 0x00, 0x00,  0x00, 0x00, 0x00, 0x00]),  true, 255n], 
  [new Uint8Array([0xff, 0xff, 0x00, 0x00,  0x00, 0x00, 0x00, 0x00]),  true, 65535n],
  [new Uint8Array([0xff, 0xff, 0xff, 0xff,  0x00, 0x00, 0x00, 0x00]),  true, 4294967295n],
  [new Uint8Array([0x00, 0x00, 0x00, 0x00,  0x01, 0x00, 0x00, 0x00]),  true, 4294967296n],
  [new Uint8Array([0x00, 0x00, 0x00, 0x00,  0x00, 0x01, 0x00, 0x00]),  true, 1099511627776n],
  [new Uint8Array([0x00, 0x00, 0x00, 0x00,  0x00, 0x00, 0x01, 0x00]),  true, 281474976710656n],
  [new Uint8Array([0xff, 0xff, 0xff, 0xff,  0xff, 0xff, 0x1f, 0x00]),  true, 9007199254740991n],
  [new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF,  0xFF, 0xFF, 0xFF, 0x7F]),  true, (2n**63n)-1n],
  [new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF,  0xFF, 0xFF, 0xFF, 0xFF]),  true, -1n],
  [new Uint8Array([0x00, 0xFF, 0xFF, 0xFF,  0xFF, 0xFF, 0xFF, 0xFF]),  true, -256n],
  [new Uint8Array([0xFF, 0xFE, 0xFF, 0xFF,  0xFF, 0xFF, 0xFF, 0xFF]),  true, -257n],
  [new Uint8Array([0x00, 0x00, 0x00, 0x00,  0x00, 0x00, 0x00, 0x80]),  true, -(2n**63n)]
];

testValues.forEach(
  function( [bytes, littleEndian, expectedValue] ) {
    const val = getBigInt64( new DataView(bytes.buffer), 0, littleEndian );
    console.log(
      val === expectedValue
                ? 'pass'
                : `FAIL. expected ${expectedValue}, received ${val}` );
  }
);
MT0
  • 143,790
  • 11
  • 59
  • 117