Skip to content

Commit e0dbb17

Browse files
nbbeekendurran
andauthored
fix(NODE-4905): double precision accuracy in canonical EJSON (#548)
Co-authored-by: Durran Jordan <[email protected]>
1 parent 9ff60ba commit e0dbb17

File tree

3 files changed

+66
-15
lines changed

3 files changed

+66
-15
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/node/bson_corpus.spec.test.js

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

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

190208
// invalid, but still parseable, EJSON. if provided, make sure that we
191209
// properly convert it to canonical EJSON and BSON.
@@ -205,8 +223,22 @@ describe('BSON Corpus', function () {
205223
expect(nativeToBson(jsonToNative(cEJ))).to.deep.equal(cB);
206224
}
207225

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

211243
if (v.relaxed_extjson) {
212244
let rEJ = normalize(v.relaxed_extjson);

test/node/double.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expect } from 'chai';
22
import { BSON, Double } from '../register-bson';
33

44
import { BSON_DATA_NUMBER, BSON_DATA_INT } from '../../src/constants';
5+
import { inspect } from 'node:util';
56

67
describe('BSON Double Precision', function () {
78
context('class Double', function () {
@@ -36,6 +37,30 @@ describe('BSON Double Precision', function () {
3637
});
3738
}
3839
});
40+
41+
describe('.toExtendedJSON()', () => {
42+
const tests = [
43+
{ input: new Double(0), output: { $numberDouble: '0.0' } },
44+
{ input: new Double(-0), output: { $numberDouble: '-0.0' } },
45+
{ input: new Double(3), output: { $numberDouble: '3.0' } },
46+
{ input: new Double(-3), output: { $numberDouble: '-3.0' } },
47+
{ input: new Double(3.4), output: { $numberDouble: '3.4' } },
48+
{ input: new Double(Number.EPSILON), output: { $numberDouble: '2.220446049250313e-16' } },
49+
{ input: new Double(12345e7), output: { $numberDouble: '123450000000.0' } },
50+
{ input: new Double(12345e-1), output: { $numberDouble: '1234.5' } },
51+
{ input: new Double(-12345e-1), output: { $numberDouble: '-1234.5' } },
52+
{ input: new Double(Infinity), output: { $numberDouble: 'Infinity' } },
53+
{ input: new Double(-Infinity), output: { $numberDouble: '-Infinity' } },
54+
{ input: new Double(NaN), output: { $numberDouble: 'NaN' } }
55+
];
56+
57+
for (const { input, output } of tests) {
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+
});
3964
});
4065

4166
function serializeThenDeserialize(value) {

0 commit comments

Comments
 (0)