From 8fd15a20a571c293c3c8c9b9e6b496c89e59c810 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Tue, 16 Apr 2024 17:28:54 -0400 Subject: [PATCH 1/5] feat(NODE-6086): add Double.fromString method --- src/double.ts | 29 ++++++++++++++++++++++ test/node/double.test.ts | 52 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/src/double.ts b/src/double.ts index 6dcec2e90..21fc3793d 100644 --- a/src/double.ts +++ b/src/double.ts @@ -1,4 +1,5 @@ import { BSONValue } from './bson_value'; +import { BSONError } from './error'; import type { EJSONOptions } from './extended_json'; import { type InspectFn, defaultInspect } from './parser/utils'; @@ -32,6 +33,34 @@ export class Double extends BSONValue { this.value = +value; } + /** + * Attempt to create an double type from string. + * + * This method will throw a BSONError on any string input that is not representable as a IEEE-754 64-bit double. + * Notably, this method will also throw on the following string formats: + * - Strings in non-decimal formats (exponent notation, binary, hex, or octal digits) + * - Strings with non-numeric, floating point, or slash characters; however, 'Infinity', '-Infinity', and 'NaN' input strings are allowed + * - Strings with leading and/or trailing whitespace + * + * Strings with leading zeros, however, are also allowed + * + * @param value - the string we want to represent as an double. + */ + static fromString(value: string): number { + const coercedValue = Number(value); + const nonFiniteValidInputs = ['Infinity', '-Infinity', 'NaN']; + if ( + (!Number.isFinite(coercedValue) && !nonFiniteValidInputs.includes(value)) || + (Number.isNaN(coercedValue) && value !== 'NaN') || + value === '' || + (/[^-0-9.]/.test(value) && !nonFiniteValidInputs.includes(value)) || + value.trim() !== value + ) { + throw new BSONError(`Input: '${value}' is not a valid Double string`); + } + return coercedValue; + } + /** * Access the number value. * diff --git a/test/node/double.test.ts b/test/node/double.test.ts index baf27b8db..19f460f6b 100644 --- a/test/node/double.test.ts +++ b/test/node/double.test.ts @@ -225,6 +225,58 @@ describe('BSON Double Precision', function () { }); }); }); + + describe('fromString', () => { + const acceptedInputs = [ + ['zero', '0', 0], + ['non-leading zeros', '45000000', 45000000], + ['zero with leading zeros', '000000.0000', 0], + ['positive leading zeros', '000000867.1', 867.1], + ['negative leading zeros', '-00007.980', -7.98], + ['positive integer with decimal', '2.0', 2], + ['zero with decimal', '0.0', 0.0], + ['Infinity', 'Infinity', Infinity], + ['-Infinity', '-Infinity', -Infinity], + ['NaN', 'NaN', NaN], + ['basic floating point', '-4.556000', -4.556] + ]; + + const errorInputs = [ + ['commas', '34,450'], + ['exponentiation notation', '1.34e16'], + ['octal', '0o1'], + ['binary', '0b1'], + ['hex', '0x1'], + ['empty string', ''], + ['leading and trailing whitespace', ' 89 ', 89], + ['fake positive infinity', '2e308'], + ['fake negative infinity', '-2e308'], + ['fraction', '3/4'], + ['foo', 'foo'] + ]; + + for (const [testName, value, expectedDouble] of acceptedInputs) { + context(`when case is ${testName}`, () => { + it(`should return Double that matches expected value`, () => { + if (value === 'NaN') { + expect(isNaN(Double.fromString(value))).to.be.true; + } else { + expect(Double.fromString(value)).to.equal(expectedDouble); + } + }); + }); + } + for (const [testName, value] of errorInputs) { + context(`when case is ${testName}`, () => { + it(`should throw correct error`, () => { + expect(() => Double.fromString(value)).to.throw( + BSON.BSONError, + /not a valid Double string/ + ); + }); + }); + } + }); }); function serializeThenDeserialize(value) { From 8147e8712b1832cc276f371ef14af23a926ccbfc Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Tue, 16 Apr 2024 17:41:03 -0400 Subject: [PATCH 2/5] added negative zero success case' --- src/double.ts | 2 +- test/node/double.test.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/double.ts b/src/double.ts index 21fc3793d..cc83516da 100644 --- a/src/double.ts +++ b/src/double.ts @@ -39,7 +39,7 @@ export class Double extends BSONValue { * This method will throw a BSONError on any string input that is not representable as a IEEE-754 64-bit double. * Notably, this method will also throw on the following string formats: * - Strings in non-decimal formats (exponent notation, binary, hex, or octal digits) - * - Strings with non-numeric, floating point, or slash characters; however, 'Infinity', '-Infinity', and 'NaN' input strings are allowed + * - Strings with characters other than sign, numeric, floating point, or slash characters (Note: 'Infinity', '-Infinity', and 'NaN' input strings are still allowed) * - Strings with leading and/or trailing whitespace * * Strings with leading zeros, however, are also allowed diff --git a/test/node/double.test.ts b/test/node/double.test.ts index 19f460f6b..93af18029 100644 --- a/test/node/double.test.ts +++ b/test/node/double.test.ts @@ -226,7 +226,7 @@ describe('BSON Double Precision', function () { }); }); - describe('fromString', () => { + describe.only('fromString', () => { const acceptedInputs = [ ['zero', '0', 0], ['non-leading zeros', '45000000', 45000000], @@ -238,7 +238,8 @@ describe('BSON Double Precision', function () { ['Infinity', 'Infinity', Infinity], ['-Infinity', '-Infinity', -Infinity], ['NaN', 'NaN', NaN], - ['basic floating point', '-4.556000', -4.556] + ['basic floating point', '-4.556000', -4.556], + ['negative zero', '-0', -0] ]; const errorInputs = [ From 907a8dd86f32f7694d7029751eff4494cfd41128 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Tue, 16 Apr 2024 17:43:34 -0400 Subject: [PATCH 3/5] return Double type --- src/double.ts | 4 ++-- test/node/double.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/double.ts b/src/double.ts index cc83516da..5c004b995 100644 --- a/src/double.ts +++ b/src/double.ts @@ -46,7 +46,7 @@ export class Double extends BSONValue { * * @param value - the string we want to represent as an double. */ - static fromString(value: string): number { + static fromString(value: string): Double { const coercedValue = Number(value); const nonFiniteValidInputs = ['Infinity', '-Infinity', 'NaN']; if ( @@ -58,7 +58,7 @@ export class Double extends BSONValue { ) { throw new BSONError(`Input: '${value}' is not a valid Double string`); } - return coercedValue; + return new Double(coercedValue); } /** diff --git a/test/node/double.test.ts b/test/node/double.test.ts index 93af18029..343b69c9a 100644 --- a/test/node/double.test.ts +++ b/test/node/double.test.ts @@ -226,7 +226,7 @@ describe('BSON Double Precision', function () { }); }); - describe.only('fromString', () => { + describe('fromString', () => { const acceptedInputs = [ ['zero', '0', 0], ['non-leading zeros', '45000000', 45000000], @@ -262,7 +262,7 @@ describe('BSON Double Precision', function () { if (value === 'NaN') { expect(isNaN(Double.fromString(value))).to.be.true; } else { - expect(Double.fromString(value)).to.equal(expectedDouble); + expect(Double.fromString(value).value).to.equal(expectedDouble); } }); }); From 8c69efa6afe1e98394cd19bbcbbf3374d1f41731 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Tue, 16 Apr 2024 17:51:39 -0400 Subject: [PATCH 4/5] fixed wording of docs comment --- src/double.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/double.ts b/src/double.ts index 5c004b995..6dcc6ef05 100644 --- a/src/double.ts +++ b/src/double.ts @@ -39,7 +39,7 @@ export class Double extends BSONValue { * This method will throw a BSONError on any string input that is not representable as a IEEE-754 64-bit double. * Notably, this method will also throw on the following string formats: * - Strings in non-decimal formats (exponent notation, binary, hex, or octal digits) - * - Strings with characters other than sign, numeric, floating point, or slash characters (Note: 'Infinity', '-Infinity', and 'NaN' input strings are still allowed) + * - Strings with characters other than numeric, floating point, or leading sign characters (Note: 'Infinity', '-Infinity', and 'NaN' input strings are still allowed) * - Strings with leading and/or trailing whitespace * * Strings with leading zeros, however, are also allowed From 0b5da295c56c02c04a2dcd907f351f8228a672ad Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Wed, 17 Apr 2024 17:37:25 -0400 Subject: [PATCH 5/5] requested changes --- src/double.ts | 16 ++++++++++------ test/node/double.test.ts | 37 +++++++++++++++++-------------------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/double.ts b/src/double.ts index 6dcc6ef05..8f7d31b8a 100644 --- a/src/double.ts +++ b/src/double.ts @@ -49,14 +49,18 @@ export class Double extends BSONValue { static fromString(value: string): Double { const coercedValue = Number(value); const nonFiniteValidInputs = ['Infinity', '-Infinity', 'NaN']; - if ( + + if (value.trim() !== value) { + throw new BSONError(`Input: '${value}' contains whitespace`); + } else if (value === '') { + throw new BSONError(`Input is an empty string`); + } else if (/[^-0-9.]/.test(value) && !nonFiniteValidInputs.includes(value)) { + throw new BSONError(`Input: '${value}' contains invalid characters`); + } else if ( (!Number.isFinite(coercedValue) && !nonFiniteValidInputs.includes(value)) || - (Number.isNaN(coercedValue) && value !== 'NaN') || - value === '' || - (/[^-0-9.]/.test(value) && !nonFiniteValidInputs.includes(value)) || - value.trim() !== value + (Number.isNaN(coercedValue) && value !== 'NaN') ) { - throw new BSONError(`Input: '${value}' is not a valid Double string`); + throw new BSONError(`Input: ${value} is not representable as a Double`); // generic case } return new Double(coercedValue); } diff --git a/test/node/double.test.ts b/test/node/double.test.ts index 343b69c9a..4fa6e03d9 100644 --- a/test/node/double.test.ts +++ b/test/node/double.test.ts @@ -243,22 +243,22 @@ describe('BSON Double Precision', function () { ]; const errorInputs = [ - ['commas', '34,450'], - ['exponentiation notation', '1.34e16'], - ['octal', '0o1'], - ['binary', '0b1'], - ['hex', '0x1'], - ['empty string', ''], - ['leading and trailing whitespace', ' 89 ', 89], - ['fake positive infinity', '2e308'], - ['fake negative infinity', '-2e308'], - ['fraction', '3/4'], - ['foo', 'foo'] + ['commas', '34,450', 'contains invalid characters'], + ['exponentiation notation', '1.34e16', 'contains invalid characters'], + ['octal', '0o1', 'contains invalid characters'], + ['binary', '0b1', 'contains invalid characters'], + ['hex', '0x1', 'contains invalid characters'], + ['empty string', '', 'is an empty string'], + ['leading and trailing whitespace', ' 89 ', 'contains whitespace'], + ['fake positive infinity', '2e308', 'contains invalid characters'], + ['fake negative infinity', '-2e308', 'contains invalid characters'], + ['fraction', '3/4', 'contains invalid characters'], + ['foo', 'foo', 'contains invalid characters'] ]; for (const [testName, value, expectedDouble] of acceptedInputs) { - context(`when case is ${testName}`, () => { - it(`should return Double that matches expected value`, () => { + context(`when the input is ${testName}`, () => { + it(`should successfully return a Double representation`, () => { if (value === 'NaN') { expect(isNaN(Double.fromString(value))).to.be.true; } else { @@ -267,13 +267,10 @@ describe('BSON Double Precision', function () { }); }); } - for (const [testName, value] of errorInputs) { - context(`when case is ${testName}`, () => { - it(`should throw correct error`, () => { - expect(() => Double.fromString(value)).to.throw( - BSON.BSONError, - /not a valid Double string/ - ); + for (const [testName, value, expectedErrMsg] of errorInputs) { + context(`when the input is ${testName}`, () => { + it(`should throw an error containing '${expectedErrMsg}'`, () => { + expect(() => Double.fromString(value)).to.throw(BSON.BSONError, expectedErrMsg); }); }); }