diff --git a/src/double.ts b/src/double.ts index 6dcec2e9..8f7d31b8 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,38 @@ 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 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 + * + * @param value - the string we want to represent as an double. + */ + static fromString(value: string): Double { + const coercedValue = Number(value); + const nonFiniteValidInputs = ['Infinity', '-Infinity', 'NaN']; + + 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') + ) { + throw new BSONError(`Input: ${value} is not representable as a Double`); // generic case + } + return new Double(coercedValue); + } + /** * Access the number value. * diff --git a/test/node/double.test.ts b/test/node/double.test.ts index baf27b8d..4fa6e03d 100644 --- a/test/node/double.test.ts +++ b/test/node/double.test.ts @@ -225,6 +225,56 @@ 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], + ['negative zero', '-0', -0] + ]; + + const errorInputs = [ + ['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 the input is ${testName}`, () => { + it(`should successfully return a Double representation`, () => { + if (value === 'NaN') { + expect(isNaN(Double.fromString(value))).to.be.true; + } else { + expect(Double.fromString(value).value).to.equal(expectedDouble); + } + }); + }); + } + 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); + }); + }); + } + }); }); function serializeThenDeserialize(value) {