Skip to content

Commit

Permalink
implement better support for floats (#71)
Browse files Browse the repository at this point in the history
this change uses `fround` when available to make a better decision on
if a number is a 32 or 64 bit floating point number.  This means that
we can more safely use floats in msgpack without losing precision.

In the case where `Math.fround` is not defined (I'M LOOKIN AT YOU, IE10),
this code defaults to using a float64 so that we don't accidentally lose
precision like we did with the previous implementation
  • Loading branch information
imnotjames authored and mcollina committed May 20, 2018
1 parent db540b3 commit 52e8753
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 20 deletions.
30 changes: 21 additions & 9 deletions lib/encoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

var Buffer = require('safe-buffer').Buffer
var bl = require('bl')
var TOLERANCE = 0.1

module.exports = function buildEncode (encodingTypes, forceFloat64, compatibilityMode, disableTimestampEncoding) {
function encode (obj, avoidSlice) {
Expand Down Expand Up @@ -301,22 +300,35 @@ function write64BitInt (buf, offset, num) {
}

function isFloat (n) {
return n !== Math.floor(n)
return n % 1 !== 0
}

function encodeFloat (obj, forceFloat64) {
var buf
var useDoublePrecision = true

// If `fround` is supported, we can check if a float
// is double or single precision by rounding the object
// to single precision and comparing the difference.
// If it's not supported, it's safer to use a 64 bit
// float so we don't lose precision without meaning to.
if (Math.fround) {
useDoublePrecision = Math.fround(obj) !== obj
}

buf = Buffer.allocUnsafe(5)
buf[0] = 0xca
buf.writeFloatBE(obj, 1)
if (forceFloat64) {
useDoublePrecision = true
}

var buf

// FIXME is there a way to check if a
// value fits in a float?
if (forceFloat64 || Math.abs(obj - buf.readFloatBE(1)) > TOLERANCE) {
if (useDoublePrecision) {
buf = Buffer.allocUnsafe(9)
buf[0] = 0xcb
buf.writeDoubleBE(obj, 1)
} else {
buf = Buffer.allocUnsafe(5)
buf[0] = 0xca
buf.writeFloatBE(obj, 1)
}

return buf
Expand Down
74 changes: 63 additions & 11 deletions test/floats.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,67 @@ var bl = require('bl')

test('encoding/decoding 32-bits float numbers', function (t) {
var encoder = msgpack()
var allNum = []
var float32 = [
1.5,
0.15625,
-2.5
]

allNum.push(-222.42)
allNum.push(748364.2)
allNum.push(2.2)
var float64 = [
2 ** 150,
1.337,
2.2
]

allNum.forEach(function (num) {
float64.forEach(function (num) {
t.test('encoding ' + num, function (t) {
var buf = encoder.encode(num)
t.equal(buf.length, 9, 'must have 5 bytes')
t.equal(buf[0], 0xcb, 'must have the proper header')

var dec = buf.readDoubleBE(1)
t.equal(dec, num, 'must decode correctly')
t.end()
})

t.test('decoding ' + num, function (t) {
var buf = Buffer.allocUnsafe(9)
var dec
buf[0] = 0xcb
buf.writeDoubleBE(num, 1)

dec = encoder.decode(buf)
t.equal(dec, num, 'must decode correctly')
t.end()
})

t.test('mirror test ' + num, function (t) {
var dec = encoder.decode(encoder.encode(num))
t.equal(dec, num, 'must decode correctly')
t.end()
})
})

float32.forEach(function (num) {
t.test('encoding ' + num, function (t) {
var buf = encoder.encode(num)
var dec = buf.readFloatBE(1)
t.equal(buf.length, 5, 'must have 5 bytes')
t.equal(buf[0], 0xca, 'must have the proper header')
t.true(Math.abs(dec - num) < 0.1, 'must decode correctly')

var dec = buf.readFloatBE(1)
t.equal(dec, num, 'must decode correctly')
t.end()
})

t.test('forceFloat64 encoding ' + num, function (t) {
var enc = msgpack({ forceFloat64: true })
var buf = enc.encode(num)
var dec = buf.readDoubleBE(1)

t.equal(buf.length, 9, 'must have 9 bytes')
t.equal(buf[0], 0xcb, 'must have the proper header')
t.true(Math.abs(dec - num) < 0.1, 'must decode correctly')

var dec = buf.readDoubleBE(1)
t.equal(dec, num, 'must decode correctly')
t.end()
})

Expand All @@ -38,14 +76,15 @@ test('encoding/decoding 32-bits float numbers', function (t) {
var dec
buf[0] = 0xca
buf.writeFloatBE(num, 1)

dec = encoder.decode(buf)
t.true(Math.abs(dec - num) < 0.1, 'must decode correctly')
t.equal(dec, num, 'must decode correctly')
t.end()
})

t.test('mirror test ' + num, function (t) {
var dec = encoder.decode(encoder.encode(num))
t.true(Math.abs(dec - num) < 0.1, 'must decode correctly')
t.equal(dec, num, 'must decode correctly')
t.end()
})
})
Expand All @@ -65,3 +104,16 @@ test('decoding an incomplete 32-bits float numbers', function (t) {
t.equals(buf.length, origLength, 'must not consume any byte')
t.end()
})

test('decoding an incomplete 64-bits float numbers', function (t) {
var encoder = msgpack()
var buf = Buffer.allocUnsafe(8)
buf[0] = 0xcb
buf = bl().append(buf)
var origLength = buf.length
t.throws(function () {
encoder.decode(buf)
}, encoder.IncompleteBufferError, 'must throw IncompleteBufferError')
t.equals(buf.length, origLength, 'must not consume any byte')
t.end()
})

0 comments on commit 52e8753

Please sign in to comment.