Skip to content

Commit d86bd52

Browse files
authored
fix(NODE-4905): double precision accuracy in canonical EJSON (#549)
1 parent 853bbb0 commit d86bd52

File tree

5 files changed

+69
-17
lines changed

5 files changed

+69
-17
lines changed

src/double.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,17 @@ export class Double {
5252
return this.value;
5353
}
5454

55-
// NOTE: JavaScript has +0 and -0, apparently to model limit calculations. If a user
56-
// explicitly provided `-0` then we need to ensure the sign makes it into the output
5755
if (Object.is(Math.sign(this.value), -0)) {
58-
return { $numberDouble: `-${this.value.toFixed(1)}` };
56+
// NOTE: JavaScript has +0 and -0, apparently to model limit calculations. If a user
57+
// explicitly provided `-0` then we need to ensure the sign makes it into the output
58+
return { $numberDouble: '-0.0' };
5959
}
6060

61-
let $numberDouble: string;
6261
if (Number.isInteger(this.value)) {
63-
$numberDouble = this.value.toFixed(1);
64-
if ($numberDouble.length >= 13) {
65-
$numberDouble = this.value.toExponential(13).toUpperCase();
66-
}
62+
return { $numberDouble: `${this.value}.0` };
6763
} else {
68-
$numberDouble = this.value.toString();
64+
return { $numberDouble: `${this.value}` };
6965
}
70-
71-
return { $numberDouble };
7266
}
7367

7468
/** @internal */

test/mocha.opts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
--require ts-node/register
2-
--require chai/register-expect
32
--require source-map-support/register
43
--timeout 10000

test/node/bson_corpus.spec.test.js

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,26 @@ describe('BSON Corpus', function () {
181181
// convert inputs to native Javascript objects
182182
const nativeFromCB = bsonToNative(cB);
183183

184-
// round tripped EJSON should match the original
185-
expect(nativeToCEJSON(jsonToNative(cEJ))).to.equal(cEJ);
184+
if (cEJ.includes('1.2345678921232E+18')) {
185+
// The following is special test logic for a "Double type" bson corpus test that uses a different
186+
// string format for the resulting double value
187+
// The test does not have a loss in precision, just different exponential output
188+
// We want to ensure that the stringified value when interpreted as a double is equal
189+
// as opposed to the string being precisely the same
190+
if (description !== 'Double type') {
191+
throw new Error('Unexpected test using 1.2345678921232E+18');
192+
}
193+
const eJSONParsedAsJSON = JSON.parse(cEJ);
194+
const eJSONParsed = EJSON.parse(cEJ, { relaxed: false });
195+
expect(eJSONParsedAsJSON).to.have.nested.property('d.$numberDouble');
196+
expect(eJSONParsed).to.have.nested.property('d._bsontype', 'Double');
197+
const testInputAsFloat = Number.parseFloat(eJSONParsedAsJSON.d.$numberDouble);
198+
const ejsonOutputAsFloat = eJSONParsed.d.valueOf();
199+
expect(ejsonOutputAsFloat).to.equal(testInputAsFloat);
200+
} else {
201+
// round tripped EJSON should match the original
202+
expect(nativeToCEJSON(jsonToNative(cEJ))).to.equal(cEJ);
203+
}
186204

187205
// invalid, but still parseable, EJSON. if provided, make sure that we
188206
// properly convert it to canonical EJSON and BSON.
@@ -202,8 +220,22 @@ describe('BSON Corpus', function () {
202220
expect(nativeToBson(jsonToNative(cEJ))).to.deep.equal(cB);
203221
}
204222

205-
// the reverse direction, BSON -> native -> EJSON, should match canonical EJSON.
206-
expect(nativeToCEJSON(nativeFromCB)).to.equal(cEJ);
223+
if (cEJ.includes('1.2345678921232E+18')) {
224+
// The round tripped value should be equal in interpreted value, not in exact character match
225+
const eJSONFromBSONAsJSON = JSON.parse(
226+
EJSON.stringify(BSON.deserialize(cB), { relaxed: false })
227+
);
228+
const eJSONParsed = EJSON.parse(cEJ, { relaxed: false });
229+
// TODO(NODE-4377): EJSON transforms large doubles into longs
230+
expect(eJSONFromBSONAsJSON).to.have.nested.property('d.$numberLong');
231+
expect(eJSONParsed).to.have.nested.property('d._bsontype', 'Double');
232+
const testInputAsFloat = Number.parseFloat(eJSONFromBSONAsJSON.d.$numberLong);
233+
const ejsonOutputAsFloat = eJSONParsed.d.valueOf();
234+
expect(ejsonOutputAsFloat).to.equal(testInputAsFloat);
235+
} else {
236+
// the reverse direction, BSON -> native -> EJSON, should match canonical EJSON.
237+
expect(nativeToCEJSON(nativeFromCB)).to.equal(cEJ);
238+
}
207239

208240
if (v.relaxed_extjson) {
209241
let rEJ = normalize(v.relaxed_extjson);

test/node/double_tests.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const BSON = require('../register-bson');
44
const Double = BSON.Double;
5+
const inspect = require('util').inspect;
56

67
describe('BSON Double Precision', function () {
78
context('class Double', function () {
@@ -34,6 +35,32 @@ describe('BSON Double Precision', function () {
3435
});
3536
}
3637
});
38+
39+
describe('.toExtendedJSON()', () => {
40+
const tests = [
41+
{ input: new Double(0), output: { $numberDouble: '0.0' } },
42+
{ input: new Double(-0), output: { $numberDouble: '-0.0' } },
43+
{ input: new Double(3), output: { $numberDouble: '3.0' } },
44+
{ input: new Double(-3), output: { $numberDouble: '-3.0' } },
45+
{ input: new Double(3.4), output: { $numberDouble: '3.4' } },
46+
{ input: new Double(Number.EPSILON), output: { $numberDouble: '2.220446049250313e-16' } },
47+
{ input: new Double(12345e7), output: { $numberDouble: '123450000000.0' } },
48+
{ input: new Double(12345e-1), output: { $numberDouble: '1234.5' } },
49+
{ input: new Double(-12345e-1), output: { $numberDouble: '-1234.5' } },
50+
{ input: new Double(Infinity), output: { $numberDouble: 'Infinity' } },
51+
{ input: new Double(-Infinity), output: { $numberDouble: '-Infinity' } },
52+
{ input: new Double(NaN), output: { $numberDouble: 'NaN' } }
53+
];
54+
55+
for (const test of tests) {
56+
const input = test.input;
57+
const output = test.output;
58+
const title = `returns ${inspect(output)} when Double is ${input}`;
59+
it(title, () => {
60+
expect(output).to.deep.equal(input.toExtendedJSON({ relaxed: false }));
61+
});
62+
}
63+
});
3764
});
3865

3966
function serializeThenDeserialize(value) {

test/register-bson.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// and make sure you run mocha using our .mocharc.json or with --require ts-node/register
88

99
// This should be done by mocha --require, but that isn't supported until mocha version 7+
10-
require('chai/register-expect');
10+
global.expect = require('chai').expect;
1111
require('array-includes/auto');
1212
require('object.entries/auto');
1313
require('array.prototype.flatmap/auto');

0 commit comments

Comments
 (0)