@@ -2,7 +2,6 @@ import { expect } from 'chai';
22import { BSON , Double } from '../register-bson' ;
33
44import { BSON_DATA_NUMBER , BSON_DATA_INT } from '../../src/constants' ;
5- import { inspect } from 'node:util' ;
65
76describe ( 'BSON Double Precision' , function ( ) {
87 context ( 'class Double' , function ( ) {
@@ -40,24 +39,158 @@ describe('BSON Double Precision', function () {
4039
4140 describe ( '.toExtendedJSON()' , ( ) => {
4241 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' } }
42+ {
43+ title : 'returns "0.0" when input is a number 0' ,
44+ input : 0 ,
45+ output : { $numberDouble : '0.0' }
46+ } ,
47+ {
48+ title : 'returns "-0.0" when input is a number -0' ,
49+ input : - 0 ,
50+ output : { $numberDouble : '-0.0' }
51+ } ,
52+ {
53+ title : 'returns "0.0" when input is a string "-0.0"' ,
54+ input : '-0.0' ,
55+ output : { $numberDouble : '-0.0' }
56+ } ,
57+ {
58+ title : 'returns "3.0" when input is a number 3' ,
59+ input : 3 ,
60+ output : { $numberDouble : '3.0' }
61+ } ,
62+ {
63+ title : 'returns "-3.0" when input is a number -3' ,
64+ input : - 3 ,
65+ output : { $numberDouble : '-3.0' }
66+ } ,
67+ {
68+ title : 'returns "3.4" when input is a number 3.4' ,
69+ input : 3.4 ,
70+ output : { $numberDouble : '3.4' }
71+ } ,
72+ {
73+ title : 'returns "2.220446049250313e-16" when input is Number.EPSILON' ,
74+ input : Number . EPSILON ,
75+ output : { $numberDouble : '2.220446049250313e-16' }
76+ } ,
77+ {
78+ title : 'returns "123450000000.0" when input is a number 12345e7' ,
79+ input : 12345e7 ,
80+ output : { $numberDouble : '123450000000.0' }
81+ } ,
82+ {
83+ title : 'returns "1234.5" when input is a number 12345e-1' ,
84+ input : 12345e-1 ,
85+ output : { $numberDouble : '1234.5' }
86+ } ,
87+ {
88+ title : 'returns "-1234.5" when input is a number -12345e-1' ,
89+ input : - 12345e-1 ,
90+ output : { $numberDouble : '-1234.5' }
91+ } ,
92+ {
93+ title : 'returns "Infinity" when input is a number Infinity' ,
94+ input : Infinity ,
95+ output : { $numberDouble : 'Infinity' }
96+ } ,
97+ {
98+ title : 'returns "-Infinity" when input is a number -Infinity' ,
99+ input : - Infinity ,
100+ output : { $numberDouble : '-Infinity' }
101+ } ,
102+ {
103+ title : 'returns "NaN" when input is a number NaN' ,
104+ input : NaN ,
105+ output : { $numberDouble : 'NaN' }
106+ } ,
107+ {
108+ title : 'returns "1.7976931348623157e+308" when input is a number Number.MAX_VALUE' ,
109+ input : Number . MAX_VALUE ,
110+ output : { $numberDouble : '1.7976931348623157e+308' }
111+ } ,
112+ {
113+ title : 'returns "5e-324" when input is a number Number.MIN_VALUE' ,
114+ input : Number . MIN_VALUE ,
115+ output : { $numberDouble : '5e-324' }
116+ } ,
117+ {
118+ title : 'returns "-1.7976931348623157e+308" when input is a number -Number.MAX_VALUE' ,
119+ input : - Number . MAX_VALUE ,
120+ output : { $numberDouble : '-1.7976931348623157e+308' }
121+ } ,
122+ {
123+ title : 'returns "-5e-324" when input is a number -Number.MIN_VALUE' ,
124+ input : - Number . MIN_VALUE ,
125+ output : { $numberDouble : '-5e-324' }
126+ } ,
127+ {
128+ // Reference: https://docs.oracle.com/cd/E19957-01/806-3568/ncg_math.html
129+ // min positive normal number
130+ title :
131+ 'returns "2.2250738585072014e-308" when input is a number the minimum positive normal value' ,
132+ input : '2.2250738585072014e-308' ,
133+ output : { $numberDouble : '2.2250738585072014e-308' }
134+ } ,
135+ {
136+ // max subnormal number
137+ title :
138+ 'returns "2.225073858507201e-308" when input is a number the maximum positive subnormal value' ,
139+ input : '2.225073858507201e-308' ,
140+ output : { $numberDouble : '2.225073858507201e-308' }
141+ } ,
142+ {
143+ // min positive subnormal number (NOTE: JS does not output same input string, but numeric values are equal)
144+ title : 'returns "5e-324" when input is a number the minimum positive subnormal value' ,
145+ input : '4.9406564584124654e-324' ,
146+ output : { $numberDouble : '5e-324' }
147+ } ,
148+ {
149+ // https://262.ecma-international.org/13.0/#sec-number.prototype.tofixed
150+ // Note: calling toString on this integer returns 1000000000000000100, so toFixed is more precise
151+ // This test asserts we do not change _current_ behavior, however preserving this value is not
152+ // something that is possible in BSON, if a future version of this library were to emit
153+ // "1000000000000000100.0" instead, it would not be incorrect from a BSON/MongoDB/Double precision perspective,
154+ // it would just constrain the string output to what is possible with 8 bytes of floating point precision
155+ title :
156+ 'returns "1000000000000000128.0" when input is an int-like number beyond 8-byte double precision' ,
157+ input : '1000000000000000128' ,
158+ output : { $numberDouble : '1000000000000000128.0' }
159+ }
55160 ] ;
56161
57- for ( const { input, output } of tests ) {
58- const title = `returns ${ inspect ( output ) } when Double is ${ input } ` ;
162+ for ( const test of tests ) {
163+ const input = test . input ;
164+ const output = test . output ;
165+ const title = test . title ;
59166 it ( title , ( ) => {
60- expect ( output ) . to . deep . equal ( input . toExtendedJSON ( { relaxed : false } ) ) ;
167+ const inputAsDouble = new Double ( input ) ;
168+ expect ( inputAsDouble . toExtendedJSON ( { relaxed : false } ) ) . to . deep . equal ( output ) ;
169+ if ( ! Number . isNaN ( inputAsDouble . value ) ) {
170+ expect ( Number ( inputAsDouble . toExtendedJSON ( { relaxed : false } ) . $numberDouble ) ) . to . equal (
171+ inputAsDouble . value
172+ ) ;
173+ }
174+ } ) ;
175+
176+ it ( `preserves the byte wise value of ${ input } (${ typeof input } ) after stringification` , ( ) => {
177+ // Asserts the same bytes can be reconstructed from the generated string,
178+ // sometimes the string changes "4.9406564584124654e-324" -> "5e-324"
179+ // but both represent the same ieee754 double bytes
180+ const ejsonDoubleString = new Double ( input ) . toExtendedJSON ( ) . $numberDouble ;
181+ const bytesFromInput = ( ( ) => {
182+ const b = Buffer . alloc ( 8 ) ;
183+ b . writeDoubleBE ( Number ( input ) ) ;
184+ return b . toString ( 'hex' ) ;
185+ } ) ( ) ;
186+
187+ const bytesFromOutput = ( ( ) => {
188+ const b = Buffer . alloc ( 8 ) ;
189+ b . writeDoubleBE ( Number ( ejsonDoubleString ) ) ;
190+ return b . toString ( 'hex' ) ;
191+ } ) ( ) ;
192+
193+ expect ( bytesFromOutput ) . to . equal ( bytesFromInput ) ;
61194 } ) ;
62195 }
63196 } ) ;
0 commit comments