Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NODE-6087): add Int32.fromString method #670

Merged
merged 10 commits into from
Apr 17, 2024
37 changes: 37 additions & 0 deletions src/int_32.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { BSONValue } from './bson_value';
import { BSON_INT32_MAX, BSON_INT32_MIN } from './constants';
import { BSONError } from './error';
import type { EJSONOptions } from './extended_json';
import { type InspectFn, defaultInspect } from './parser/utils';

Expand Down Expand Up @@ -32,6 +34,41 @@ export class Int32 extends BSONValue {
this.value = +value | 0;
}

/**
* Attempt to create an Int32 type from string.
*
* This method will throw a BSONError on any string input that is not representable as an Int32.
* Notably, this method will also throw on the following string formats:
* - Strings in non-decimal formats (exponent notation, binary, hex, or octal digits)
* - Strings non-numeric and non-leading sign characters (ex: '2.0', '24,000')
* - Strings with leading and/or trailing whitespace
*
* Strings with leading zeros, however, are allowed.
*
* @param value - the string we want to represent as an int32.
*/
static fromString(value: string): Int32 {
const cleanedValue = !/[^0]+/.test(value)
? value.replace(/^0+/, '0') // all zeros case
: value[0] === '-'
? value.replace(/^-0+/, '-') // negative number with leading zeros
: value.replace(/^\+?0+/, ''); // positive number with leading zeros

const coercedValue = Number(value);

if (BSON_INT32_MAX < coercedValue) {
throw new BSONError(`Input: '${value}' is larger than the maximum value for Int32`);
} else if (BSON_INT32_MIN > coercedValue) {
throw new BSONError(`Input: '${value}' is smaller than the minimum value for Int32`);
} else if (!Number.isSafeInteger(coercedValue)) {
throw new BSONError(`Input: '${value}' is not a safe integer`);
} else if (coercedValue.toString() !== cleanedValue) {
// catch all case
throw new BSONError(`Input: '${value}' is not a valid Int32 string`);
}
return new Int32(coercedValue);
}

/**
* Access the number value.
*
Expand Down
47 changes: 47 additions & 0 deletions test/node/int_32_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const BSON = require('../register-bson');
const Int32 = BSON.Int32;
const BSONError = BSON.BSONError;

describe('Int32', function () {
context('Constructor', function () {
Expand Down Expand Up @@ -97,4 +98,50 @@ describe('Int32', function () {
});
}
});

describe('fromString', () => {
const acceptedInputs = [
['Int32.max', '2147483647', 2147483647],
['Int32.min', '-2147483648', -2147483648],
['zero', '0', 0],
['a string with non-leading consecutive zeros', '45000000', 45000000],
['a string with zero with leading zeros', '000000', 0],
['a string with positive leading zeros', '000000867', 867],
['a string with explicity positive leading zeros', '+000000867', 867],
['a string with negative leading zeros', '-00007', -7]
];
const errorInputs = [
['Int32.max + 1', '2147483648', 'larger than the maximum value for Int32'],
['Int32.min - 1', '-2147483649', 'smaller than the minimum value for Int32'],
['positive integer with decimal', '2.0', 'not a valid Int32 string'],
['zero with decimals', '0.0', 'not a valid Int32 string'],
['negative zero', '-0', 'not a valid Int32 string'],
['Infinity', 'Infinity', 'larger than the maximum value for Int32'],
['-Infinity', '-Infinity', 'smaller than the minimum value for Int32'],
['NaN', 'NaN', 'not a safe integer'],
['a fraction', '2/3', 'not a safe integer'],
['a string containing commas', '34,450', 'not a safe integer'],
['a string in exponentiation notation', '1e1', 'not a valid Int32 string'],
['a octal string', '0o1', 'not a valid Int32 string'],
['a binary string', '0b1', 'not a valid Int32 string'],
['a hexadecimal string', '0x1', 'not a valid Int32 string'],
['a empty string', '', 'not a valid Int32 string'],
['a leading and trailing whitespace', ' 89 ', 'not a valid Int32 string']
];

for (const [testName, value, expectedInt32] of acceptedInputs) {
context(`when the input is ${testName}`, () => {
it(`should successfully return an Int32 representation`, () => {
expect(Int32.fromString(value).value).to.equal(expectedInt32);
});
});
}
for (const [testName, value, expectedErrMsg] of errorInputs) {
context(`when the input is ${testName}`, () => {
it(`should throw an error containing '${expectedErrMsg}'`, () => {
expect(() => Int32.fromString(value)).to.throw(BSONError, expectedErrMsg);
});
});
}
});
});