diff --git a/src/long.ts b/src/long.ts index 0e66c9e2..07b2b2db 100644 --- a/src/long.ts +++ b/src/long.ts @@ -119,42 +119,57 @@ export class Long extends BSONValue { /** * The high 32 bits as a signed value. */ - high!: number; + high: number; /** * The low 32 bits as a signed value. */ - low!: number; + low: number; /** * Whether unsigned or not. */ - unsigned!: boolean; + unsigned: boolean; /** * Constructs a 64 bit two's-complement integer, given its low and high 32 bit values as *signed* integers. - * See the from* functions below for more convenient ways of constructing Longs. - * - * Acceptable signatures are: - * - Long(low, high, unsigned?) - * - Long(bigint, unsigned?) - * - Long(string, unsigned?) * * @param low - The low (signed) 32 bits of the long * @param high - The high (signed) 32 bits of the long * @param unsigned - Whether unsigned or not, defaults to signed */ - constructor(low: number | bigint | string = 0, high?: number | boolean, unsigned?: boolean) { + constructor(low: number, high?: number, unsigned?: boolean); + /** + * Constructs a 64 bit two's-complement integer, given a bigint representation. + * + * @param value - BigInt representation of the long value + * @param unsigned - Whether unsigned or not, defaults to signed + */ + constructor(value: bigint, unsigned?: boolean); + /** + * Constructs a 64 bit two's-complement integer, given a string representation. + * + * @param value - String representation of the long value + * @param unsigned - Whether unsigned or not, defaults to signed + */ + constructor(value: string, unsigned?: boolean); + constructor( + lowOrValue: number | bigint | string = 0, + highOrUnsigned?: number | boolean, + unsigned?: boolean + ) { super(); - if (typeof low === 'bigint') { - Object.assign(this, Long.fromBigInt(low, !!high)); - } else if (typeof low === 'string') { - Object.assign(this, Long.fromString(low, !!high)); - } else { - this.low = low | 0; - this.high = (high as number) | 0; - this.unsigned = !!unsigned; - } + const unsignedBool = typeof highOrUnsigned === 'boolean' ? highOrUnsigned : Boolean(unsigned); + const high = typeof highOrUnsigned === 'number' ? highOrUnsigned : 0; + const res = + typeof lowOrValue === 'string' + ? Long.fromString(lowOrValue, unsignedBool) + : typeof lowOrValue === 'bigint' + ? Long.fromBigInt(lowOrValue, unsignedBool) + : { low: lowOrValue | 0, high: high | 0, unsigned: unsignedBool }; + this.low = res.low; + this.high = res.high; + this.unsigned = res.unsigned; } static TWO_PWR_24 = Long.fromInt(TWO_PWR_24_DBL); @@ -243,7 +258,15 @@ export class Long extends BSONValue { * @returns The corresponding Long value */ static fromBigInt(value: bigint, unsigned?: boolean): Long { - return Long.fromString(value.toString(), unsigned); + // eslint-disable-next-line no-restricted-globals + const FROM_BIGINT_BIT_MASK = BigInt(0xffffffff); + // eslint-disable-next-line no-restricted-globals + const FROM_BIGINT_BIT_SHIFT = BigInt(32); + return new Long( + Number(value & FROM_BIGINT_BIT_MASK), + Number((value >> FROM_BIGINT_BIT_SHIFT) & FROM_BIGINT_BIT_MASK), + unsigned + ); } /** diff --git a/test/node/bson_type_classes.test.ts b/test/node/bson_type_classes.test.ts index 7b4aede5..1f843eb7 100644 --- a/test/node/bson_type_classes.test.ts +++ b/test/node/bson_type_classes.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { inspect } from 'node:util'; -import { __isWeb__ } from '../register-bson'; +import { __isWeb__, __noBigInt__ } from '../register-bson'; import { Binary, BSONRegExp, @@ -44,7 +44,7 @@ const BSONTypeClassCtors = new Map BSONValue>([ ['Decimal128', () => new Decimal128('1.23')], ['Double', () => new Double(1.23)], ['Int32', () => new Int32(1)], - ['Long', () => new Long(1n)], + ['Long', () => (__noBigInt__ ? new Long(1) : new Long(1n))], ['MinKey', () => new MinKey()], ['MaxKey', () => new MaxKey()], ['ObjectId', () => new ObjectId('00'.repeat(12))], diff --git a/test/node/long.test.ts b/test/node/long.test.ts index 00377de6..85056ad3 100644 --- a/test/node/long.test.ts +++ b/test/node/long.test.ts @@ -1,5 +1,6 @@ import { expect } from 'chai'; import { Long, BSONError, __noBigInt__ } from '../register-bson'; +import { BSON_INT32_MAX, BSON_INT32_MIN } from '../../src/constants'; describe('Long', function () { it('accepts strings in the constructor', function () { @@ -16,14 +17,15 @@ describe('Long', function () { it('accepts BigInts in Long constructor', function () { if (__noBigInt__) { this.currentTest?.skip(); + } else { + expect(new Long(0n).toString()).to.equal('0'); + expect(new Long(-1n).toString()).to.equal('-1'); + expect(new Long(-1n, true).toString()).to.equal('18446744073709551615'); + expect(new Long(123456789123456789n).toString()).to.equal('123456789123456789'); + expect(new Long(123456789123456789n, true).toString()).to.equal('123456789123456789'); + expect(new Long(13835058055282163712n).toString()).to.equal('-4611686018427387904'); + expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712'); } - expect(new Long(0n).toString()).to.equal('0'); - expect(new Long(-1n).toString()).to.equal('-1'); - expect(new Long(-1n, true).toString()).to.equal('18446744073709551615'); - expect(new Long(123456789123456789n).toString()).to.equal('123456789123456789'); - expect(new Long(123456789123456789n, true).toString()).to.equal('123456789123456789'); - expect(new Long(13835058055282163712n).toString()).to.equal('-4611686018427387904'); - expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712'); }); describe('static fromExtendedJSON()', function () { @@ -164,6 +166,52 @@ describe('Long', function () { }); }); + describe('static fromBigInt()', function () { + const inputs: [ + name: string, + input: bigint, + unsigned: boolean | undefined, + expectedLong?: Long + ][] = [ + ['0', BigInt('0'), false, Long.ZERO], + ['-0 (bigint coerces this to 0)', BigInt('-0'), false, Long.ZERO], + [ + 'max unsigned input', + BigInt(Long.MAX_UNSIGNED_VALUE.toString(10)), + true, + Long.MAX_UNSIGNED_VALUE + ], + ['max signed input', BigInt(Long.MAX_VALUE.toString(10)), false, Long.MAX_VALUE], + ['min signed input', BigInt(Long.MIN_VALUE.toString(10)), false, Long.MIN_VALUE], + [ + 'negative greater than 32 bits', + BigInt(-9228915101), + false, + Long.fromBits(0xd9e9ee63, 0xfffffffd) + ], + ['less than 32 bits', BigInt(245666), false, new Long(245666)], + ['unsigned less than 32 bits', BigInt(245666), true, new Long(245666, true)], + ['negative less than 32 bits', BigInt(-245666), false, new Long(-245666, -1)], + ['max int32', BigInt(BSON_INT32_MAX), false, new Long(BSON_INT32_MAX)], + ['max int32 unsigned', BigInt(BSON_INT32_MAX), true, new Long(BSON_INT32_MAX, 0, true)], + ['min int32', BigInt(BSON_INT32_MIN), false, new Long(BSON_INT32_MIN, -1)] + ]; + + beforeEach(function () { + if (__noBigInt__) { + this.currentTest?.skip(); + } + }); + + for (const [testName, num, unsigned, expectedLong] of inputs) { + context(`when the input is ${testName}`, () => { + it(`should return a Long representation of the input`, () => { + expect(Long.fromBigInt(num, unsigned)).to.deep.equal(expectedLong); + }); + }); + } + }); + describe('static fromString()', function () { const successInputs: [ name: string, diff --git a/test/node/timestamp.test.ts b/test/node/timestamp.test.ts index 608a217f..41ae7f4e 100644 --- a/test/node/timestamp.test.ts +++ b/test/node/timestamp.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import * as BSON from '../register-bson'; -import { Timestamp } from '../register-bson'; +import { Timestamp, __noBigInt__ } from '../register-bson'; describe('Timestamp', () => { describe('static MAX_VALUE', () => { @@ -10,11 +10,14 @@ describe('Timestamp', () => { }); it('should always be an unsigned value', () => { + let bigIntInputs: Timestamp[] = []; + if (!__noBigInt__) { + bigIntInputs = [new BSON.Timestamp(0xffffffffffn), new BSON.Timestamp(0xffffffffffffffffn)]; + } const table = [ // @ts-expect-error: Not advertized by the types, but constructs a 0 timestamp new BSON.Timestamp(), - new BSON.Timestamp(0xffffffffffn), - new BSON.Timestamp(0xffffffffffffffffn), + ...bigIntInputs, new BSON.Timestamp(new BSON.Long(0xffff_ffff, 0xffff_ffff, false)), new BSON.Timestamp(new BSON.Long(0xffff_ffff, 0xffff_ffff, true)), new BSON.Timestamp({ t: 0xffff_ffff, i: 0xffff_ffff }), @@ -29,22 +32,29 @@ describe('Timestamp', () => { }); context('output formats', () => { - const timestamp = new BSON.Timestamp(0xffffffffffffffffn); + beforeEach(function () { + if (__noBigInt__) { + this.currentTest?.skip(); + } + }); context('when converting toString', () => { it('exports an unsigned number', () => { + const timestamp = new BSON.Timestamp(0xffffffffffffffffn); expect(timestamp.toString()).to.equal('18446744073709551615'); }); }); context('when converting toJSON', () => { it('exports an unsigned number', () => { + const timestamp = new BSON.Timestamp(0xffffffffffffffffn); expect(timestamp.toJSON()).to.deep.equal({ $timestamp: '18446744073709551615' }); }); }); context('when converting toExtendedJSON', () => { it('exports an unsigned number', () => { + const timestamp = new BSON.Timestamp(0xffffffffffffffffn); expect(timestamp.toExtendedJSON()).to.deep.equal({ $timestamp: { t: 4294967295, i: 4294967295 } });