Skip to content
Merged
34 changes: 34 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,38 @@ 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 (
coercedValue.toString() !== cleanedValue ||
!Number.isSafeInteger(coercedValue) ||
BSON_INT32_MAX < coercedValue ||
BSON_INT32_MIN > coercedValue
) {
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],
['non-leading zeros', '45000000', 45000000],
['zero with leading zeros', '000000', 0],
['positive leading zeros', '000000867', 867],
['explicity positive leading zeros', '+000000867', 867],
['negative leading zeros', '-00007', -7]
];
const errorInputs = [
['Int32.max + 1', '2147483648'],
['Int32.min - 1', '-2147483649'],
['positive integer with decimal', '2.0'],
['zero with decimal', '0.0'],
['negative zero', '-0'],
['Infinity', 'Infinity'],
['-Infinity', '-Infinity'],
['NaN', 'NaN'],
['fraction', '2/3'],
['commas', '34,450'],
['exponentiation notation', '1e1'],
['octal', '0o1'],
['binary', '0b1'],
['hex', '0x1'],
['empty string', ''],
['leading and trailing whitespace', ' 89 ', 89]
];

for (const [testName, value, expectedInt32] of acceptedInputs) {
context(`when case is ${testName}`, () => {
it(`should return Int32 that matches expected value`, () => {
expect(Int32.fromString(value).value).to.equal(expectedInt32);
});
});
}
for (const [testName, value] of errorInputs) {
context(`when case is ${testName}`, () => {
it(`should throw correct error`, () => {
expect(() => Int32.fromString(value)).to.throw(BSONError, /not a valid Int32 string/);
});
});
}
});
});