Skip to content

Commit a469e91

Browse files
authored
fix(NODE-4464):stringify and parse negative zero to and from $numberDouble: -0.0 (#531)
1 parent f1cccf2 commit a469e91

File tree

4 files changed

+49
-17
lines changed

4 files changed

+49
-17
lines changed

src/extended_json.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,15 @@ function deserializeValue(value: any, options: EJSON.Options = {}) {
7373
return value;
7474
}
7575

76-
// if it's an integer, should interpret as smallest BSON integer
77-
// that can represent it exactly. (if out of range, interpret as double.)
78-
if (Math.floor(value) === value) {
79-
if (value >= BSON_INT32_MIN && value <= BSON_INT32_MAX) return new Int32(value);
80-
if (value >= BSON_INT64_MIN && value <= BSON_INT64_MAX) return Long.fromNumber(value);
76+
if (Number.isInteger(value) && !Object.is(value, -0)) {
77+
// interpret as being of the smallest BSON integer type that can represent the number exactly
78+
if (value >= BSON_INT32_MIN && value <= BSON_INT32_MAX) {
79+
return new Int32(value);
80+
}
81+
if (value >= BSON_INT64_MIN && value <= BSON_INT64_MAX) {
82+
// TODO(NODE-4377): EJSON js number handling diverges from BSON
83+
return Long.fromNumber(value);
84+
}
8185
}
8286

8387
// If the number is a non-integer or out of integer range, should interpret as BSON Double.
@@ -216,16 +220,17 @@ function serializeValue(value: any, options: EJSONSerializeOptions): any {
216220
}
217221

218222
if (typeof value === 'number' && (!options.relaxed || !isFinite(value))) {
219-
// it's an integer
220-
if (Math.floor(value) === value) {
221-
const int32Range = value >= BSON_INT32_MIN && value <= BSON_INT32_MAX,
222-
int64Range = value >= BSON_INT64_MIN && value <= BSON_INT64_MAX;
223-
223+
if (Number.isInteger(value) && !Object.is(value, -0)) {
224224
// interpret as being of the smallest BSON integer type that can represent the number exactly
225-
if (int32Range) return { $numberInt: value.toString() };
226-
if (int64Range) return { $numberLong: value.toString() };
225+
if (value >= BSON_INT32_MIN && value <= BSON_INT32_MAX) {
226+
return { $numberInt: value.toString() };
227+
}
228+
if (value >= BSON_INT64_MIN && value <= BSON_INT64_MAX) {
229+
// TODO(NODE-4377): EJSON js number handling diverges from BSON
230+
return { $numberLong: value.toString() };
231+
}
227232
}
228-
return { $numberDouble: value.toString() };
233+
return { $numberDouble: Object.is(value, -0) ? '-0.0' : value.toString() };
229234
}
230235

231236
if (value instanceof RegExp || isRegExp(value)) {

test/node/bson_corpus.spec.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ function nativeToREJSON(native) {
4747
}
4848

4949
function normalize(cEJ) {
50+
// TODO(NODE-3396): loses information about the original input
51+
// ex. parse will preserve -0 but stringify will output +0
5052
return JSON.stringify(JSON.parse(cEJ));
5153
}
5254

test/node/double_tests.js renamed to test/node/double.test.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
'use strict';
2-
3-
const BSON = require('../register-bson');
1+
import * as BSON from '../register-bson';
42
const Double = BSON.Double;
53

64
describe('BSON Double Precision', function () {
@@ -89,4 +87,30 @@ describe('BSON Double Precision', function () {
8987
expect(type).to.not.equal(BSON.BSON_DATA_NUMBER);
9088
expect(type).to.equal(BSON.BSON_DATA_INT);
9189
});
90+
91+
describe('extended JSON', () => {
92+
describe('stringify()', () => {
93+
it('preserves negative zero in canonical format', () => {
94+
const result = BSON.EJSON.stringify({ a: -0.0 }, { relaxed: false });
95+
expect(result).to.equal(`{"a":{"$numberDouble":"-0.0"}}`);
96+
});
97+
98+
it('loses negative zero in relaxed format', () => {
99+
const result = BSON.EJSON.stringify({ a: -0.0 }, { relaxed: true });
100+
expect(result).to.equal(`{"a":0}`);
101+
});
102+
});
103+
104+
describe('parse()', () => {
105+
it('preserves negative zero in deserialization with relaxed false', () => {
106+
const result = BSON.EJSON.parse(`{ "a": -0.0 }`, { relaxed: false });
107+
expect(result.a).to.have.property('_bsontype', 'Double');
108+
});
109+
110+
it('preserves negative zero in deserialization with relaxed true', () => {
111+
const result = BSON.EJSON.parse(`{ "a": -0.0 }`, { relaxed: true });
112+
expect(Object.is(result.a, -0), 'expected prop a to be negative zero').to.be.true;
113+
});
114+
});
115+
});
92116
});

test/node/extended_json_tests.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ describe('Extended JSON', function () {
7979
});
8080

8181
it('should correctly extend an existing mongodb module', function () {
82-
// Serialize the document
82+
// TODO(NODE-4377): doubleNumberIntFit should be a double not a $numberLong
8383
var json =
8484
'{"_id":{"$numberInt":"100"},"gh":{"$numberInt":"1"},"binary":{"$binary":{"base64":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==","subType":"00"}},"date":{"$date":{"$numberLong":"1488372056737"}},"code":{"$code":"function() {}","$scope":{"a":{"$numberInt":"1"}}},"dbRef":{"$ref":"tests","$id":{"$numberInt":"1"},"$db":"test"},"decimal":{"$numberDecimal":"100"},"double":{"$numberDouble":"10.1"},"int32":{"$numberInt":"10"},"long":{"$numberLong":"200"},"maxKey":{"$maxKey":1},"minKey":{"$minKey":1},"objectId":{"$oid":"111111111111111111111111"},"objectID":{"$oid":"111111111111111111111111"},"oldObjectID":{"$oid":"111111111111111111111111"},"regexp":{"$regularExpression":{"pattern":"hello world","options":"i"}},"symbol":{"$symbol":"symbol"},"timestamp":{"$timestamp":{"t":0,"i":1000}},"int32Number":{"$numberInt":"300"},"doubleNumber":{"$numberDouble":"200.2"},"longNumberIntFit":{"$numberLong":"7036874417766400"},"doubleNumberIntFit":{"$numberLong":"19007199250000000"}}';
8585

@@ -103,6 +103,7 @@ describe('Extended JSON', function () {
103103
expect(doc1.int32Number._bsontype).to.equal('Int32');
104104
expect(doc1.doubleNumber._bsontype).to.equal('Double');
105105
expect(doc1.longNumberIntFit._bsontype).to.equal('Long');
106+
// TODO(NODE-4377): EJSON should not try to make Longs from large ints
106107
expect(doc1.doubleNumberIntFit._bsontype).to.equal('Long');
107108
});
108109

0 commit comments

Comments
 (0)