From 63caecfdd759a4ea13f7bd8bdfc17ca8f246e4d0 Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 30 Aug 2023 13:06:09 -0400 Subject: [PATCH 01/12] fix(NODE-5594): wip - pre cherry pick --- src/decimal128.ts | 349 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 349 insertions(+) diff --git a/src/decimal128.ts b/src/decimal128.ts index edf74eb7..6d3b687b 100644 --- a/src/decimal128.ts +++ b/src/decimal128.ts @@ -507,7 +507,356 @@ export class Decimal128 extends BSONValue { // Return the new Decimal128 return new Decimal128(buffer); } + static fromStringCopy(representation: string): Decimal128 { + // Parse state tracking + let isNegative = false; + let sawSign = false; + let sawRadix = false; + let foundNonZero = false; + + // Total number of significant digits (no leading or trailing zero) + let significantDigits = 0; + // Total number of significand digits read + let nDigitsRead = 0; + // Total number of digits (no leading zeros) + let nDigits = 0; + // The number of the digits after radix + let radixPosition = 0; + // The index of the first non-zero in *str* + let firstNonZero = 0; + + // Digits Array + const digits = [0]; + // The number of digits in digits + let nDigitsStored = 0; + // Insertion pointer for digits + let digitsInsert = 0; + // The index of the last digit + let lastDigit = 0; + + // Exponent + let exponent = 0; + // The high 17 digits of the significand + let significandHigh = new Long(0, 0); + // The low 17 digits of the significand + let significandLow = new Long(0, 0); + // The biased exponent + let biasedExponent = 0; + + // Read index + let index = 0; + + // Naively prevent against REDOS attacks. + // TODO: implementing a custom parsing for this, or refactoring the regex would yield + // further gains. + if (representation.length >= 7000) { + throw new BSONError('' + representation + ' not a valid Decimal128 string'); + } + + // Results + const stringMatch = representation.match(PARSE_STRING_REGEXP); + const infMatch = representation.match(PARSE_INF_REGEXP); + const nanMatch = representation.match(PARSE_NAN_REGEXP); + + // Validate the string + if ((!stringMatch && !infMatch && !nanMatch) || representation.length === 0) { + throw new BSONError('' + representation + ' not a valid Decimal128 string'); + } + + if (stringMatch) { + // full_match = stringMatch[0] + // sign = stringMatch[1] + + const unsignedNumber = stringMatch[2]; + // stringMatch[3] is undefined if a whole number (ex "1", 12") + // but defined if a number w/ decimal in it (ex "1.0, 12.2") + + const e = stringMatch[4]; + const expSign = stringMatch[5]; + const expNumber = stringMatch[6]; + + // they provided e, but didn't give an exponent number. for ex "1e" + if (e && expNumber === undefined) invalidErr(representation, 'missing exponent power'); + + // they provided e, but didn't give a number before it. for ex "e1" + if (e && unsignedNumber === undefined) invalidErr(representation, 'missing exponent base'); + + if (e === undefined && (expSign || expNumber)) { + invalidErr(representation, 'missing e before exponent'); + } + } + + // Get the negative or positive sign + if (representation[index] === '+' || representation[index] === '-') { + sawSign = true; + isNegative = representation[index++] === '-'; + } + + // Check if user passed Infinity or NaN + if (!isDigit(representation[index]) && representation[index] !== '.') { + if (representation[index] === 'i' || representation[index] === 'I') { + return new Decimal128(isNegative ? INF_NEGATIVE_BUFFER : INF_POSITIVE_BUFFER); + } else if (representation[index] === 'N') { + return new Decimal128(NAN_BUFFER); + } + } + + // Read all the digits + while (isDigit(representation[index]) || representation[index] === '.') { + if (representation[index] === '.') { + if (sawRadix) invalidErr(representation, 'contains multiple periods'); + + sawRadix = true; + index = index + 1; + continue; + } + + if (nDigitsStored < MAX_DIGITS) { + if (representation[index] !== '0' || foundNonZero) { + if (!foundNonZero) { + firstNonZero = nDigitsRead; + } + + foundNonZero = true; + + // Only store 34 digits + digits[digitsInsert++] = parseInt(representation[index], 10); + nDigitsStored = nDigitsStored + 1; + } + } + + if (foundNonZero) nDigits = nDigits + 1; + if (sawRadix) radixPosition = radixPosition + 1; + + nDigitsRead = nDigitsRead + 1; + index = index + 1; + } + + if (sawRadix && !nDigitsRead) + throw new BSONError('' + representation + ' not a valid Decimal128 string'); + + // Read exponent if exists + if (representation[index] === 'e' || representation[index] === 'E') { + // Read exponent digits + const match = representation.substr(++index).match(EXPONENT_REGEX); + + // No digits read + if (!match || !match[2]) return new Decimal128(NAN_BUFFER); + + // Get exponent + exponent = parseInt(match[0], 10); + + // Adjust the index + index = index + match[0].length; + } + + // Return not a number + if (representation[index]) return new Decimal128(NAN_BUFFER); + + // Done reading input + // Find first non-zero digit in digits + if (!nDigitsStored) { + digits[0] = 0; + nDigits = 1; + nDigitsStored = 1; + significantDigits = 0; + } else { + lastDigit = nDigitsStored - 1; + significantDigits = nDigits; + if (significantDigits !== 1) { + while ( + representation[ + firstNonZero + significantDigits - 1 + Number(sawSign) + Number(sawRadix) + ] === '0' + ) { + significantDigits = significantDigits - 1; + } + } + } + + // Normalization of exponent + // Correct exponent based on radix position, and shift significand as needed + // to represent user input + + // Overflow prevention + if (exponent <= radixPosition && radixPosition > exponent + (1 << 14)) { + exponent = EXPONENT_MIN; + } else { + exponent = exponent - radixPosition; + } + + // Attempt to normalize the exponent + while (exponent > EXPONENT_MAX) { + // Shift exponent to significand and decrease + lastDigit = lastDigit + 1; + if (lastDigit >= MAX_DIGITS) { + // Check if we have a zero then just hard clamp, otherwise fail + if (significantDigits === 0) { + exponent = EXPONENT_MAX; + break; + } + + invalidErr(representation, 'overflow'); + } + exponent = exponent - 1; + } + + while (exponent < EXPONENT_MIN || nDigitsStored < nDigits) { + // Shift last digit. can only do this if < significant digits than # stored. + if (lastDigit === 0) { + if (significantDigits === 0) { + exponent = EXPONENT_MIN; + break; + } + + invalidErr(representation, 'exponent underflow'); + } + + if (nDigitsStored < nDigits) { + if ( + representation[nDigits - 1 + Number(sawSign) + Number(sawRadix)] !== '0' && + significantDigits !== 0 + ) { + invalidErr(representation, 'inexact rounding'); + } + // adjust to match digits not stored + nDigits = nDigits - 1; + } else { + if (digits[lastDigit] !== 0) { + invalidErr(representation, 'inexact rounding'); + } + // adjust to round + lastDigit = lastDigit - 1; + } + + if (exponent < EXPONENT_MAX) { + exponent = exponent + 1; + } else { + invalidErr(representation, 'overflow'); + } + } + + // Round + // We've normalized the exponent, but might still need to round. + if (lastDigit + 1 < significantDigits) { + // If we have seen a radix point, 'string' is 1 longer than we have + // documented with ndigits_read, so inc the position of the first nonzero + // digit and the position that digits are read to. + if (sawRadix) { + firstNonZero = firstNonZero + 1; + } + // if saw sign, we need to increment again to account for - or + sign at start. + if (sawSign) { + firstNonZero = firstNonZero + 1; + } + + const roundDigit = parseInt(representation[firstNonZero + lastDigit + 1], 10); + + if (roundDigit !== 0) { + invalidErr(representation, 'inexact rounding'); + } + } + + // Encode significand + // The high 17 digits of the significand + significandHigh = Long.fromNumber(0); + // The low 17 digits of the significand + significandLow = Long.fromNumber(0); + + // read a zero + if (significantDigits === 0) { + significandHigh = Long.fromNumber(0); + significandLow = Long.fromNumber(0); + } else if (lastDigit < 17) { + let dIdx = 0; + significandLow = Long.fromNumber(digits[dIdx++]); + significandHigh = new Long(0, 0); + + for (; dIdx <= lastDigit; dIdx++) { + significandLow = significandLow.multiply(Long.fromNumber(10)); + significandLow = significandLow.add(Long.fromNumber(digits[dIdx])); + } + } else { + let dIdx = 0; + significandHigh = Long.fromNumber(digits[dIdx++]); + + for (; dIdx <= lastDigit - 17; dIdx++) { + significandHigh = significandHigh.multiply(Long.fromNumber(10)); + significandHigh = significandHigh.add(Long.fromNumber(digits[dIdx])); + } + + significandLow = Long.fromNumber(digits[dIdx++]); + + for (; dIdx <= lastDigit; dIdx++) { + significandLow = significandLow.multiply(Long.fromNumber(10)); + significandLow = significandLow.add(Long.fromNumber(digits[dIdx])); + } + } + + const significand = multiply64x2(significandHigh, Long.fromString('100000000000000000')); + significand.low = significand.low.add(significandLow); + + if (lessThan(significand.low, significandLow)) { + significand.high = significand.high.add(Long.fromNumber(1)); + } + + // Biased exponent + biasedExponent = exponent + EXPONENT_BIAS; + const dec = { low: Long.fromNumber(0), high: Long.fromNumber(0) }; + // Encode combination, exponent, and significand. + if ( + significand.high.shiftRightUnsigned(49).and(Long.fromNumber(1)).equals(Long.fromNumber(1)) + ) { + // Encode '11' into bits 1 to 3 + dec.high = dec.high.or(Long.fromNumber(0x3).shiftLeft(61)); + dec.high = dec.high.or( + Long.fromNumber(biasedExponent).and(Long.fromNumber(0x3fff).shiftLeft(47)) + ); + dec.high = dec.high.or(significand.high.and(Long.fromNumber(0x7fffffffffff))); + } else { + dec.high = dec.high.or(Long.fromNumber(biasedExponent & 0x3fff).shiftLeft(49)); + dec.high = dec.high.or(significand.high.and(Long.fromNumber(0x1ffffffffffff))); + } + + dec.low = significand.low; + + // Encode sign + if (isNegative) { + dec.high = dec.high.or(Long.fromString('9223372036854775808')); + } + + // Encode into a buffer + const buffer = ByteUtils.allocate(16); + index = 0; + + // Encode the low 64 bits of the decimal + // Encode low bits + buffer[index++] = dec.low.low & 0xff; + buffer[index++] = (dec.low.low >> 8) & 0xff; + buffer[index++] = (dec.low.low >> 16) & 0xff; + buffer[index++] = (dec.low.low >> 24) & 0xff; + // Encode high bits + buffer[index++] = dec.low.high & 0xff; + buffer[index++] = (dec.low.high >> 8) & 0xff; + buffer[index++] = (dec.low.high >> 16) & 0xff; + buffer[index++] = (dec.low.high >> 24) & 0xff; + + // Encode the high 64 bits of the decimal + // Encode low bits + buffer[index++] = dec.high.low & 0xff; + buffer[index++] = (dec.high.low >> 8) & 0xff; + buffer[index++] = (dec.high.low >> 16) & 0xff; + buffer[index++] = (dec.high.low >> 24) & 0xff; + // Encode high bits + buffer[index++] = dec.high.high & 0xff; + buffer[index++] = (dec.high.high >> 8) & 0xff; + buffer[index++] = (dec.high.high >> 16) & 0xff; + buffer[index++] = (dec.high.high >> 24) & 0xff; + + // Return the new Decimal128 + return new Decimal128(buffer); + } /** Create a string representation of the raw Decimal128 value */ toString(): string { // Note: bits in this routine are referred to starting at 0, From eeb0149a6cdf6794811ba23ea2af8a3c9275a31a Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 30 Aug 2023 13:13:57 -0400 Subject: [PATCH 02/12] feat(NODE-5594): add fromStringWithRounding static method --- src/decimal128.ts | 101 +++++++++++++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 32 deletions(-) diff --git a/src/decimal128.ts b/src/decimal128.ts index 6d3b687b..b940f898 100644 --- a/src/decimal128.ts +++ b/src/decimal128.ts @@ -507,7 +507,8 @@ export class Decimal128 extends BSONValue { // Return the new Decimal128 return new Decimal128(buffer); } - static fromStringCopy(representation: string): Decimal128 { + + static fromStringWithRounding(representation: string): Decimal128 { // Parse state tracking let isNegative = false; let sawSign = false; @@ -531,11 +532,15 @@ export class Decimal128 extends BSONValue { let nDigitsStored = 0; // Insertion pointer for digits let digitsInsert = 0; + // The index of the first non-zero digit + let firstDigit = 0; // The index of the last digit let lastDigit = 0; // Exponent let exponent = 0; + // loop index over array + let i = 0; // The high 17 digits of the significand let significandHigh = new Long(0, 0); // The low 17 digits of the significand @@ -655,7 +660,11 @@ export class Decimal128 extends BSONValue { // Done reading input // Find first non-zero digit in digits + firstDigit = 0; + if (!nDigitsStored) { + firstDigit = 0; + lastDigit = 0; digits[0] = 0; nDigits = 1; nDigitsStored = 1; @@ -664,11 +673,7 @@ export class Decimal128 extends BSONValue { lastDigit = nDigitsStored - 1; significantDigits = nDigits; if (significantDigits !== 1) { - while ( - representation[ - firstNonZero + significantDigits - 1 + Number(sawSign) + Number(sawRadix) - ] === '0' - ) { + while (digits[firstNonZero + significantDigits - 1] === 0) { significantDigits = significantDigits - 1; } } @@ -679,7 +684,7 @@ export class Decimal128 extends BSONValue { // to represent user input // Overflow prevention - if (exponent <= radixPosition && radixPosition > exponent + (1 << 14)) { + if (exponent <= radixPosition && radixPosition - exponent > 1 << 14) { exponent = EXPONENT_MIN; } else { exponent = exponent - radixPosition; @@ -689,9 +694,11 @@ export class Decimal128 extends BSONValue { while (exponent > EXPONENT_MAX) { // Shift exponent to significand and decrease lastDigit = lastDigit + 1; - if (lastDigit >= MAX_DIGITS) { + + if (lastDigit - firstDigit > MAX_DIGITS) { // Check if we have a zero then just hard clamp, otherwise fail - if (significantDigits === 0) { + const digitsString = digits.join(''); + if (digitsString.match(/^0+$/)) { exponent = EXPONENT_MAX; break; } @@ -703,28 +710,16 @@ export class Decimal128 extends BSONValue { while (exponent < EXPONENT_MIN || nDigitsStored < nDigits) { // Shift last digit. can only do this if < significant digits than # stored. - if (lastDigit === 0) { - if (significantDigits === 0) { - exponent = EXPONENT_MIN; - break; - } - - invalidErr(representation, 'exponent underflow'); + if (lastDigit === 0 && significantDigits < nDigitsStored) { + exponent = EXPONENT_MIN; + significantDigits = 0; + break; } if (nDigitsStored < nDigits) { - if ( - representation[nDigits - 1 + Number(sawSign) + Number(sawRadix)] !== '0' && - significantDigits !== 0 - ) { - invalidErr(representation, 'inexact rounding'); - } // adjust to match digits not stored nDigits = nDigits - 1; } else { - if (digits[lastDigit] !== 0) { - invalidErr(representation, 'inexact rounding'); - } // adjust to round lastDigit = lastDigit - 1; } @@ -732,28 +727,70 @@ export class Decimal128 extends BSONValue { if (exponent < EXPONENT_MAX) { exponent = exponent + 1; } else { + // Check if we have a zero then just hard clamp, otherwise fail + const digitsString = digits.join(''); + if (digitsString.match(/^0+$/)) { + exponent = EXPONENT_MAX; + break; + } invalidErr(representation, 'overflow'); } } // Round // We've normalized the exponent, but might still need to round. - if (lastDigit + 1 < significantDigits) { + if (lastDigit - firstDigit + 1 < significantDigits) { + let endOfString = nDigitsRead; + // If we have seen a radix point, 'string' is 1 longer than we have // documented with ndigits_read, so inc the position of the first nonzero // digit and the position that digits are read to. if (sawRadix) { firstNonZero = firstNonZero + 1; + endOfString = endOfString + 1; } - // if saw sign, we need to increment again to account for - or + sign at start. + // if negative, we need to increment again to account for - sign at start. if (sawSign) { firstNonZero = firstNonZero + 1; + endOfString = endOfString + 1; } const roundDigit = parseInt(representation[firstNonZero + lastDigit + 1], 10); + let roundBit = 0; + + if (roundDigit >= 5) { + roundBit = 1; + if (roundDigit === 5) { + roundBit = digits[lastDigit] % 2 === 1 ? 1 : 0; + for (i = firstNonZero + lastDigit + 2; i < endOfString; i++) { + if (parseInt(representation[i], 10)) { + roundBit = 1; + break; + } + } + } + } - if (roundDigit !== 0) { - invalidErr(representation, 'inexact rounding'); + if (roundBit) { + let dIdx = lastDigit; + + for (; dIdx >= 0; dIdx--) { + if (++digits[dIdx] > 9) { + digits[dIdx] = 0; + + // overflowed most significant digit + if (dIdx === 0) { + if (exponent < EXPONENT_MAX) { + exponent = exponent + 1; + digits[dIdx] = 1; + } else { + return new Decimal128(isNegative ? INF_NEGATIVE_BUFFER : INF_POSITIVE_BUFFER); + } + } + } else { + break; + } + } } } @@ -767,8 +804,8 @@ export class Decimal128 extends BSONValue { if (significantDigits === 0) { significandHigh = Long.fromNumber(0); significandLow = Long.fromNumber(0); - } else if (lastDigit < 17) { - let dIdx = 0; + } else if (lastDigit - firstDigit < 17) { + let dIdx = firstDigit; significandLow = Long.fromNumber(digits[dIdx++]); significandHigh = new Long(0, 0); @@ -777,7 +814,7 @@ export class Decimal128 extends BSONValue { significandLow = significandLow.add(Long.fromNumber(digits[dIdx])); } } else { - let dIdx = 0; + let dIdx = firstDigit; significandHigh = Long.fromNumber(digits[dIdx++]); for (; dIdx <= lastDigit - 17; dIdx++) { From e265d73110ec72b051ce004b7aae5bb81e2f4bfc Mon Sep 17 00:00:00 2001 From: hconn-riparian <121891959+hconn-riparian@users.noreply.github.com> Date: Tue, 7 Feb 2023 14:17:05 -0500 Subject: [PATCH 03/12] unit test rounding fix --- test/node/decimal128_tests.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/node/decimal128_tests.js b/test/node/decimal128_tests.js index 3a78ea4d..d00d8ab6 100644 --- a/test/node/decimal128_tests.js +++ b/test/node/decimal128_tests.js @@ -477,6 +477,9 @@ describe('Decimal128', function () { expect(bytes).to.deep.equal(result.bytes); + result = Decimal128.fromString('37.499999999999999196428571428571375'); + expect(result.toString()).to.deep.equal('37.49999999999999919642857142857138'); + // // Create decimal from string value 15E-6177 // result = Decimal128.fromString('15E-6177'); // bytes = Buffer.from( From fa5fcd90a728a8ef5355e8ca9dd6d69a0345731d Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 30 Aug 2023 16:21:19 -0400 Subject: [PATCH 04/12] test(NODE-5594): add tests that make sure that fromStringWithRounding works like fromString excepting inexact rounding --- ...decimal128_tests.js => decimal128.test.ts} | 134 ++++++++++++++---- 1 file changed, 103 insertions(+), 31 deletions(-) rename test/node/{decimal128_tests.js => decimal128.test.ts} (89%) diff --git a/test/node/decimal128_tests.js b/test/node/decimal128.test.ts similarity index 89% rename from test/node/decimal128_tests.js rename to test/node/decimal128.test.ts index d00d8ab6..d05fd6c9 100644 --- a/test/node/decimal128_tests.js +++ b/test/node/decimal128.test.ts @@ -1,19 +1,20 @@ -'use strict'; +import { BSON } from '../register-bson'; +import { expect } from 'chai'; +import * as corpus from './tools/bson_corpus_test_loader'; -const BSON = require('../register-bson'); const Decimal128 = BSON.Decimal128; -var NAN = Buffer.from( +const NAN = Buffer.from( [ 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ].reverse() ); -var INF_NEGATIVE_BUFFER = Buffer.from( +const INF_NEGATIVE_BUFFER = Buffer.from( [ 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ].reverse() ); -var INF_POSITIVE_BUFFER = Buffer.from( +const INF_POSITIVE_BUFFER = Buffer.from( [ 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ].reverse() @@ -70,7 +71,7 @@ describe('Decimal128', function () { }); it('fromString NaN input', function (done) { - var result = Decimal128.fromString('NaN'); + let result = Decimal128.fromString('NaN'); expect(NAN).to.deep.equal(result.bytes); result = Decimal128.fromString('+NaN'); expect(NAN).to.deep.equal(result.bytes); @@ -92,7 +93,7 @@ describe('Decimal128', function () { }); it('fromString infinity input', function (done) { - var result = Decimal128.fromString('Infinity'); + let result = Decimal128.fromString('Infinity'); expect(INF_POSITIVE_BUFFER).to.deep.equal(result.bytes); result = Decimal128.fromString('+Infinity'); expect(INF_POSITIVE_BUFFER).to.deep.equal(result.bytes); @@ -107,8 +108,8 @@ describe('Decimal128', function () { it('fromString simple', function (done) { // Create decimal from string value 1 - var result = Decimal128.fromString('1'); - var bytes = Buffer.from( + let result = Decimal128.fromString('1'); + let bytes = Buffer.from( [ 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 @@ -210,8 +211,8 @@ describe('Decimal128', function () { it('fromString scientific format', function (done) { // Create decimal from string value 10e0 - var result = Decimal128.fromString('10e0'); - var bytes = Buffer.from( + let result = Decimal128.fromString('10e0'); + let bytes = Buffer.from( [ 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a @@ -283,8 +284,8 @@ describe('Decimal128', function () { it('fromString large format', function (done) { // Create decimal from string value 12345689012345789012345 - var result = Decimal128.fromString('12345689012345789012345'); - var bytes = Buffer.from( + let result = Decimal128.fromString('12345689012345789012345'); + let bytes = Buffer.from( [ 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x02, 0x9d, 0x42, 0xda, 0x3a, 0x76, 0xf9, 0xe0, 0xd9, 0x79 @@ -337,8 +338,8 @@ describe('Decimal128', function () { it('fromString exponent normalization', function (done) { // Create decimal from string value 1000000000000000000000000000000000000000 - result = Decimal128.fromString('1000000000000000000000000000000000000000'); - bytes = Buffer.from( + let result = Decimal128.fromString('1000000000000000000000000000000000000000'); + let bytes = Buffer.from( [ 0x30, 0x4c, 0x31, 0x4d, 0xc6, 0x44, 0x8d, 0x93, 0x38, 0xc1, 0x5b, 0x0a, 0x00, 0x00, 0x00, 0x00 @@ -366,7 +367,7 @@ describe('Decimal128', function () { ); expect(bytes).to.deep.equal(result.bytes); - var str = + const str = '100000000000000000000000000000000000000000000000000000000000000000000' + '000000000000000000000000000000000000000000000000000000000000000000000' + '000000000000000000000000000000000000000000000000000000000000000000000' + @@ -385,8 +386,8 @@ describe('Decimal128', function () { // Create decimal from string value str - var result = Decimal128.fromString(str); - var bytes = Buffer.from( + result = Decimal128.fromString(str); + bytes = Buffer.from( [ 0x37, 0xcc, 0x31, 0x4d, 0xc6, 0x44, 0x8d, 0x93, 0x38, 0xc1, 0x5b, 0x0a, 0x00, 0x00, 0x00, 0x00 @@ -424,8 +425,8 @@ describe('Decimal128', function () { it('fromString from string zeros', function (done) { // Create decimal from string value 0 - var result = Decimal128.fromString('0'); - var bytes = Buffer.from( + let result = Decimal128.fromString('0'); + let bytes = Buffer.from( [ 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 @@ -467,8 +468,8 @@ describe('Decimal128', function () { it('fromString from string round', function (done) { // Create decimal from string value 10E-6177 - var result = Decimal128.fromString('10E-6177'); - var bytes = Buffer.from( + let result = Decimal128.fromString('10E-6177'); + const bytes = Buffer.from( [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 @@ -809,7 +810,7 @@ describe('Decimal128', function () { }); it('toString infinity', function (done) { - var decimal = new Decimal128( + let decimal = new Decimal128( Buffer.from( [ 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -832,7 +833,7 @@ describe('Decimal128', function () { }); it('toString NaN', function (done) { - var decimal = new Decimal128( + let decimal = new Decimal128( Buffer.from( [ 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -885,7 +886,7 @@ describe('Decimal128', function () { }); it('toString regular', function (done) { - var decimal = new Decimal128( + let decimal = new Decimal128( Buffer.from( [ 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -988,7 +989,7 @@ describe('Decimal128', function () { }); it('toString scientific', function (done) { - var decimal = new Decimal128( + let decimal = new Decimal128( Buffer.from( [ 0x5f, 0xfe, 0x31, 0x4d, 0xc6, 0x44, 0x8d, 0x93, 0x38, 0xc1, 0x5b, 0x0a, 0x00, 0x00, 0x00, @@ -1111,7 +1112,7 @@ describe('Decimal128', function () { }); it('toString zeros', function (done) { - var decimal = new Decimal128( + let decimal = new Decimal128( Buffer.from( [ 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -1145,10 +1146,10 @@ describe('Decimal128', function () { it('Serialize and Deserialize tests', function (done) { // Test all methods around a simple serialization at object top level - var doc = { value: Decimal128.fromString('1') }; - var buffer = BSON.serialize(doc); - var size = BSON.calculateObjectSize(doc); - var back = BSON.deserialize(buffer); + let doc = { value: Decimal128.fromString('1') }; + let buffer = BSON.serialize(doc); + let size = BSON.calculateObjectSize(doc); + let back = BSON.deserialize(buffer); expect(buffer.length).to.equal(size); expect(doc).to.deep.equal(back); @@ -1193,6 +1194,7 @@ describe('Decimal128', function () { it('throws correct error for invalid constructor argument type', () => { const constructorArgErrMsg = 'Decimal128 must take a Buffer or string'; + // ts-ignore expect(() => new Decimal128(-0)).to.throw(constructorArgErrMsg); expect(() => new Decimal128(-1)).to.throw(constructorArgErrMsg); expect(() => new Decimal128(10)).to.throw(constructorArgErrMsg); @@ -1235,5 +1237,75 @@ describe('Decimal128', function () { }); } ); + + describe('fromStringWithRounding', function () { + context('Corpus tests', function () { + // Filter for only Decimal128 tests + for (const { description, valid, _filename, parseErrors } of corpus.filter(s => + /Decimal128/.test(s.description) + )) { + const scenarioName = `${description} (${_filename})`; + describe(scenarioName, function () { + if (valid) { + // We only care about the extended json inputs because the bson inputs will not test the + // fromString or fromStringWithRounding code paths + const inputs = [ + 'canonical_extjson', + 'degenerate_extjson', + 'relaxed_extjson', + 'converted_extjson' + ]; + for (const validTest of valid) { + context(`Valid Test: ${validTest.description}`, function () { + for (const input of inputs) { + if (validTest[input]) { + describe(`with ${input} input`, function () { + it('has same output as fromString', function () { + const extJSONString: string = JSON.parse(validTest[input]).d + .$numberDecimal; + expect(Decimal128.fromStringWithRounding(extJSONString)).to.deep.equal( + Decimal128.fromString(extJSONString) + ); + }); + }); + } + } + }); + } + } + if (parseErrors) { + // Filter out the inexact rounding tests + for (const parseErrorTest of parseErrors.filter( + p => !/Inexact/.test(p.description) + )) { + context(`ParseError - ${parseErrorTest.description}`, function () { + it('emits the same error as fromString', function () { + const errorOrNull = (f: () => void) => { + try { + f(); + } catch (err) { + return err; + } + return null; + }; + const fromStringError = errorOrNull(() => + Decimal128.fromString(parseErrorTest.string) + ); + const fromStringWithRoundingError = errorOrNull(() => + Decimal128.fromStringWithRounding(parseErrorTest.string) + ); + + expect(fromStringError).to.be.instanceOf(Error); + expect(fromStringWithRoundingError).to.be.instanceOf(Error); + + expect(fromStringWithRoundingError).to.deep.equal(fromStringError); + }); + }); + } + } + }); + } + }); + }); }); }); From ec3fef6b17d138e16fc6cbda87d12c7ecd1bcba5 Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 30 Aug 2023 16:33:15 -0400 Subject: [PATCH 05/12] test(NODE-5594): add tests to check rounding behaviour --- test/node/decimal128.test.ts | 39 ++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/test/node/decimal128.test.ts b/test/node/decimal128.test.ts index d05fd6c9..95ae7021 100644 --- a/test/node/decimal128.test.ts +++ b/test/node/decimal128.test.ts @@ -468,7 +468,7 @@ describe('Decimal128', function () { it('fromString from string round', function (done) { // Create decimal from string value 10E-6177 - let result = Decimal128.fromString('10E-6177'); + const result = Decimal128.fromString('10E-6177'); const bytes = Buffer.from( [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -478,9 +478,6 @@ describe('Decimal128', function () { expect(bytes).to.deep.equal(result.bytes); - result = Decimal128.fromString('37.499999999999999196428571428571375'); - expect(result.toString()).to.deep.equal('37.49999999999999919642857142857138'); - // // Create decimal from string value 15E-6177 // result = Decimal128.fromString('15E-6177'); // bytes = Buffer.from( @@ -1306,6 +1303,40 @@ describe('Decimal128', function () { }); } }); + + context('when the input has more than 34 significant digits', function () { + it('does not throw an error', function () { + expect(() => + Decimal128.fromStringWithRounding('37.499999999999999196428571428571375') + ).to.not.throw(); + }); + context('when the digit to round is >= 5', function () { + it('rounds up correctly', function () { + const result = Decimal128.fromStringWithRounding( + '37.499999999999999196428571428571375' + ); + expect(result.toString()).to.deep.equal('37.49999999999999919642857142857138'); + }); + }); + context('when the digit to round is < 5', function () { + it('rounds down correctly', function () { + const result = Decimal128.fromStringWithRounding( + '37.499999999999999196428571428571374' + ); + expect(result.toString()).to.deep.equal('37.49999999999999919642857142857137'); + }); + }); + + context('when the digit to round is 9', function () { + it('rounds up and carries correctly', function () { + const result = Decimal128.fromStringWithRounding( + '37.4999999999999999196428571428571399' + ); + expect(result.toString()).to.deep.equal('37.49999999999999991964285714285714'); + }); + }); + }); }); }); }); + From 0dbbc69d9b2632cfba49f611c524e4300f3e23dd Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 30 Aug 2023 16:39:12 -0400 Subject: [PATCH 06/12] test(NODE-5594): fix web tests --- test/node/decimal128.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/node/decimal128.test.ts b/test/node/decimal128.test.ts index 95ae7021..4ab7d725 100644 --- a/test/node/decimal128.test.ts +++ b/test/node/decimal128.test.ts @@ -3,6 +3,7 @@ import { expect } from 'chai'; import * as corpus from './tools/bson_corpus_test_loader'; const Decimal128 = BSON.Decimal128; +const BSONError = BSON.BSONError; const NAN = Buffer.from( [ @@ -1292,8 +1293,8 @@ describe('Decimal128', function () { Decimal128.fromStringWithRounding(parseErrorTest.string) ); - expect(fromStringError).to.be.instanceOf(Error); - expect(fromStringWithRoundingError).to.be.instanceOf(Error); + expect(fromStringError).to.be.instanceOf(BSONError); + expect(fromStringWithRoundingError).to.be.instanceOf(BSONError); expect(fromStringWithRoundingError).to.deep.equal(fromStringError); }); @@ -1339,4 +1340,3 @@ describe('Decimal128', function () { }); }); }); - From 926c7a67c9dc7d2bff9a074629aa0945569f623e Mon Sep 17 00:00:00 2001 From: Warren James Date: Wed, 30 Aug 2023 16:57:30 -0400 Subject: [PATCH 07/12] refactor(NODE-5594): convert Decimal128.fromString and Decimal128.fromStringWithRounding to wrapper methods --- src/decimal128.ts | 547 +++++++++++----------------------------------- 1 file changed, 131 insertions(+), 416 deletions(-) diff --git a/src/decimal128.ts b/src/decimal128.ts index b940f898..9314ca33 100644 --- a/src/decimal128.ts +++ b/src/decimal128.ts @@ -158,6 +158,20 @@ export class Decimal128 extends BSONValue { * @param representation - a numeric string representation. */ static fromString(representation: string): Decimal128 { + return Decimal128.fromStringInternal(representation, { allowRounding: false }); + } + + /** + * Create a Decimal128 instance from a string representation, allowing for rounding to 34 + * significant digits + * + * @param representation - a numeric string representation. + */ + static fromStringWithRounding(representation: string): Decimal128 { + return Decimal128.fromStringInternal(representation, { allowRounding: true }); + } + + private static fromStringInternal(representation: string, options: { allowRounding: boolean }) { // Parse state tracking let isNegative = false; let sawSign = false; @@ -351,445 +365,146 @@ export class Decimal128 extends BSONValue { exponent = exponent - 1; } - while (exponent < EXPONENT_MIN || nDigitsStored < nDigits) { - // Shift last digit. can only do this if < significant digits than # stored. - if (lastDigit === 0) { - if (significantDigits === 0) { + if (options.allowRounding) { + while (exponent < EXPONENT_MIN || nDigitsStored < nDigits) { + // Shift last digit. can only do this if < significant digits than # stored. + if (lastDigit === 0 && significantDigits < nDigitsStored) { exponent = EXPONENT_MIN; + significantDigits = 0; break; } - invalidErr(representation, 'exponent underflow'); - } - - if (nDigitsStored < nDigits) { - if ( - representation[nDigits - 1 + Number(sawSign) + Number(sawRadix)] !== '0' && - significantDigits !== 0 - ) { - invalidErr(representation, 'inexact rounding'); - } - // adjust to match digits not stored - nDigits = nDigits - 1; - } else { - if (digits[lastDigit] !== 0) { - invalidErr(representation, 'inexact rounding'); + if (nDigitsStored < nDigits) { + // adjust to match digits not stored + nDigits = nDigits - 1; + } else { + // adjust to round + lastDigit = lastDigit - 1; } - // adjust to round - lastDigit = lastDigit - 1; - } - - if (exponent < EXPONENT_MAX) { - exponent = exponent + 1; - } else { - invalidErr(representation, 'overflow'); - } - } - - // Round - // We've normalized the exponent, but might still need to round. - if (lastDigit + 1 < significantDigits) { - // If we have seen a radix point, 'string' is 1 longer than we have - // documented with ndigits_read, so inc the position of the first nonzero - // digit and the position that digits are read to. - if (sawRadix) { - firstNonZero = firstNonZero + 1; - } - // if saw sign, we need to increment again to account for - or + sign at start. - if (sawSign) { - firstNonZero = firstNonZero + 1; - } - - const roundDigit = parseInt(representation[firstNonZero + lastDigit + 1], 10); - - if (roundDigit !== 0) { - invalidErr(representation, 'inexact rounding'); - } - } - - // Encode significand - // The high 17 digits of the significand - significandHigh = Long.fromNumber(0); - // The low 17 digits of the significand - significandLow = Long.fromNumber(0); - - // read a zero - if (significantDigits === 0) { - significandHigh = Long.fromNumber(0); - significandLow = Long.fromNumber(0); - } else if (lastDigit < 17) { - let dIdx = 0; - significandLow = Long.fromNumber(digits[dIdx++]); - significandHigh = new Long(0, 0); - - for (; dIdx <= lastDigit; dIdx++) { - significandLow = significandLow.multiply(Long.fromNumber(10)); - significandLow = significandLow.add(Long.fromNumber(digits[dIdx])); - } - } else { - let dIdx = 0; - significandHigh = Long.fromNumber(digits[dIdx++]); - - for (; dIdx <= lastDigit - 17; dIdx++) { - significandHigh = significandHigh.multiply(Long.fromNumber(10)); - significandHigh = significandHigh.add(Long.fromNumber(digits[dIdx])); - } - - significandLow = Long.fromNumber(digits[dIdx++]); - - for (; dIdx <= lastDigit; dIdx++) { - significandLow = significandLow.multiply(Long.fromNumber(10)); - significandLow = significandLow.add(Long.fromNumber(digits[dIdx])); - } - } - - const significand = multiply64x2(significandHigh, Long.fromString('100000000000000000')); - significand.low = significand.low.add(significandLow); - - if (lessThan(significand.low, significandLow)) { - significand.high = significand.high.add(Long.fromNumber(1)); - } - - // Biased exponent - biasedExponent = exponent + EXPONENT_BIAS; - const dec = { low: Long.fromNumber(0), high: Long.fromNumber(0) }; - - // Encode combination, exponent, and significand. - if ( - significand.high.shiftRightUnsigned(49).and(Long.fromNumber(1)).equals(Long.fromNumber(1)) - ) { - // Encode '11' into bits 1 to 3 - dec.high = dec.high.or(Long.fromNumber(0x3).shiftLeft(61)); - dec.high = dec.high.or( - Long.fromNumber(biasedExponent).and(Long.fromNumber(0x3fff).shiftLeft(47)) - ); - dec.high = dec.high.or(significand.high.and(Long.fromNumber(0x7fffffffffff))); - } else { - dec.high = dec.high.or(Long.fromNumber(biasedExponent & 0x3fff).shiftLeft(49)); - dec.high = dec.high.or(significand.high.and(Long.fromNumber(0x1ffffffffffff))); - } - - dec.low = significand.low; - - // Encode sign - if (isNegative) { - dec.high = dec.high.or(Long.fromString('9223372036854775808')); - } - - // Encode into a buffer - const buffer = ByteUtils.allocate(16); - index = 0; - - // Encode the low 64 bits of the decimal - // Encode low bits - buffer[index++] = dec.low.low & 0xff; - buffer[index++] = (dec.low.low >> 8) & 0xff; - buffer[index++] = (dec.low.low >> 16) & 0xff; - buffer[index++] = (dec.low.low >> 24) & 0xff; - // Encode high bits - buffer[index++] = dec.low.high & 0xff; - buffer[index++] = (dec.low.high >> 8) & 0xff; - buffer[index++] = (dec.low.high >> 16) & 0xff; - buffer[index++] = (dec.low.high >> 24) & 0xff; - - // Encode the high 64 bits of the decimal - // Encode low bits - buffer[index++] = dec.high.low & 0xff; - buffer[index++] = (dec.high.low >> 8) & 0xff; - buffer[index++] = (dec.high.low >> 16) & 0xff; - buffer[index++] = (dec.high.low >> 24) & 0xff; - // Encode high bits - buffer[index++] = dec.high.high & 0xff; - buffer[index++] = (dec.high.high >> 8) & 0xff; - buffer[index++] = (dec.high.high >> 16) & 0xff; - buffer[index++] = (dec.high.high >> 24) & 0xff; - - // Return the new Decimal128 - return new Decimal128(buffer); - } - - static fromStringWithRounding(representation: string): Decimal128 { - // Parse state tracking - let isNegative = false; - let sawSign = false; - let sawRadix = false; - let foundNonZero = false; - - // Total number of significant digits (no leading or trailing zero) - let significantDigits = 0; - // Total number of significand digits read - let nDigitsRead = 0; - // Total number of digits (no leading zeros) - let nDigits = 0; - // The number of the digits after radix - let radixPosition = 0; - // The index of the first non-zero in *str* - let firstNonZero = 0; - - // Digits Array - const digits = [0]; - // The number of digits in digits - let nDigitsStored = 0; - // Insertion pointer for digits - let digitsInsert = 0; - // The index of the first non-zero digit - let firstDigit = 0; - // The index of the last digit - let lastDigit = 0; - - // Exponent - let exponent = 0; - // loop index over array - let i = 0; - // The high 17 digits of the significand - let significandHigh = new Long(0, 0); - // The low 17 digits of the significand - let significandLow = new Long(0, 0); - // The biased exponent - let biasedExponent = 0; - - // Read index - let index = 0; - - // Naively prevent against REDOS attacks. - // TODO: implementing a custom parsing for this, or refactoring the regex would yield - // further gains. - if (representation.length >= 7000) { - throw new BSONError('' + representation + ' not a valid Decimal128 string'); - } - - // Results - const stringMatch = representation.match(PARSE_STRING_REGEXP); - const infMatch = representation.match(PARSE_INF_REGEXP); - const nanMatch = representation.match(PARSE_NAN_REGEXP); - - // Validate the string - if ((!stringMatch && !infMatch && !nanMatch) || representation.length === 0) { - throw new BSONError('' + representation + ' not a valid Decimal128 string'); - } - - if (stringMatch) { - // full_match = stringMatch[0] - // sign = stringMatch[1] - - const unsignedNumber = stringMatch[2]; - // stringMatch[3] is undefined if a whole number (ex "1", 12") - // but defined if a number w/ decimal in it (ex "1.0, 12.2") - - const e = stringMatch[4]; - const expSign = stringMatch[5]; - const expNumber = stringMatch[6]; - // they provided e, but didn't give an exponent number. for ex "1e" - if (e && expNumber === undefined) invalidErr(representation, 'missing exponent power'); - - // they provided e, but didn't give a number before it. for ex "e1" - if (e && unsignedNumber === undefined) invalidErr(representation, 'missing exponent base'); - - if (e === undefined && (expSign || expNumber)) { - invalidErr(representation, 'missing e before exponent'); - } - } - - // Get the negative or positive sign - if (representation[index] === '+' || representation[index] === '-') { - sawSign = true; - isNegative = representation[index++] === '-'; - } - - // Check if user passed Infinity or NaN - if (!isDigit(representation[index]) && representation[index] !== '.') { - if (representation[index] === 'i' || representation[index] === 'I') { - return new Decimal128(isNegative ? INF_NEGATIVE_BUFFER : INF_POSITIVE_BUFFER); - } else if (representation[index] === 'N') { - return new Decimal128(NAN_BUFFER); - } - } - - // Read all the digits - while (isDigit(representation[index]) || representation[index] === '.') { - if (representation[index] === '.') { - if (sawRadix) invalidErr(representation, 'contains multiple periods'); - - sawRadix = true; - index = index + 1; - continue; - } - - if (nDigitsStored < MAX_DIGITS) { - if (representation[index] !== '0' || foundNonZero) { - if (!foundNonZero) { - firstNonZero = nDigitsRead; + if (exponent < EXPONENT_MAX) { + exponent = exponent + 1; + } else { + // Check if we have a zero then just hard clamp, otherwise fail + const digitsString = digits.join(''); + if (digitsString.match(/^0+$/)) { + exponent = EXPONENT_MAX; + break; } - - foundNonZero = true; - - // Only store 34 digits - digits[digitsInsert++] = parseInt(representation[index], 10); - nDigitsStored = nDigitsStored + 1; + invalidErr(representation, 'overflow'); } } - if (foundNonZero) nDigits = nDigits + 1; - if (sawRadix) radixPosition = radixPosition + 1; - - nDigitsRead = nDigitsRead + 1; - index = index + 1; - } - - if (sawRadix && !nDigitsRead) - throw new BSONError('' + representation + ' not a valid Decimal128 string'); + // Round + // We've normalized the exponent, but might still need to round. + if (lastDigit + 1 < significantDigits) { + let endOfString = nDigitsRead; - // Read exponent if exists - if (representation[index] === 'e' || representation[index] === 'E') { - // Read exponent digits - const match = representation.substr(++index).match(EXPONENT_REGEX); - - // No digits read - if (!match || !match[2]) return new Decimal128(NAN_BUFFER); - - // Get exponent - exponent = parseInt(match[0], 10); - - // Adjust the index - index = index + match[0].length; - } - - // Return not a number - if (representation[index]) return new Decimal128(NAN_BUFFER); + // If we have seen a radix point, 'string' is 1 longer than we have + // documented with ndigits_read, so inc the position of the first nonzero + // digit and the position that digits are read to. + if (sawRadix) { + firstNonZero = firstNonZero + 1; + endOfString = endOfString + 1; + } + // if negative, we need to increment again to account for - sign at start. + if (sawSign) { + firstNonZero = firstNonZero + 1; + endOfString = endOfString + 1; + } - // Done reading input - // Find first non-zero digit in digits - firstDigit = 0; + const roundDigit = parseInt(representation[firstNonZero + lastDigit + 1], 10); + let roundBit = 0; + + if (roundDigit >= 5) { + roundBit = 1; + if (roundDigit === 5) { + roundBit = digits[lastDigit] % 2 === 1 ? 1 : 0; + for (let i = firstNonZero + lastDigit + 2; i < endOfString; i++) { + if (parseInt(representation[i], 10)) { + roundBit = 1; + break; + } + } + } + } - if (!nDigitsStored) { - firstDigit = 0; - lastDigit = 0; - digits[0] = 0; - nDigits = 1; - nDigitsStored = 1; - significantDigits = 0; - } else { - lastDigit = nDigitsStored - 1; - significantDigits = nDigits; - if (significantDigits !== 1) { - while (digits[firstNonZero + significantDigits - 1] === 0) { - significantDigits = significantDigits - 1; + if (roundBit) { + let dIdx = lastDigit; + + for (; dIdx >= 0; dIdx--) { + if (++digits[dIdx] > 9) { + digits[dIdx] = 0; + + // overflowed most significant digit + if (dIdx === 0) { + if (exponent < EXPONENT_MAX) { + exponent = exponent + 1; + digits[dIdx] = 1; + } else { + return new Decimal128(isNegative ? INF_NEGATIVE_BUFFER : INF_POSITIVE_BUFFER); + } + } + } else { + break; + } + } } } - } - - // Normalization of exponent - // Correct exponent based on radix position, and shift significand as needed - // to represent user input - - // Overflow prevention - if (exponent <= radixPosition && radixPosition - exponent > 1 << 14) { - exponent = EXPONENT_MIN; } else { - exponent = exponent - radixPosition; - } - - // Attempt to normalize the exponent - while (exponent > EXPONENT_MAX) { - // Shift exponent to significand and decrease - lastDigit = lastDigit + 1; + while (exponent < EXPONENT_MIN || nDigitsStored < nDigits) { + // Shift last digit. can only do this if < significant digits than # stored. + if (lastDigit === 0) { + if (significantDigits === 0) { + exponent = EXPONENT_MIN; + break; + } - if (lastDigit - firstDigit > MAX_DIGITS) { - // Check if we have a zero then just hard clamp, otherwise fail - const digitsString = digits.join(''); - if (digitsString.match(/^0+$/)) { - exponent = EXPONENT_MAX; - break; + invalidErr(representation, 'exponent underflow'); } - invalidErr(representation, 'overflow'); - } - exponent = exponent - 1; - } - - while (exponent < EXPONENT_MIN || nDigitsStored < nDigits) { - // Shift last digit. can only do this if < significant digits than # stored. - if (lastDigit === 0 && significantDigits < nDigitsStored) { - exponent = EXPONENT_MIN; - significantDigits = 0; - break; - } - - if (nDigitsStored < nDigits) { - // adjust to match digits not stored - nDigits = nDigits - 1; - } else { - // adjust to round - lastDigit = lastDigit - 1; - } - - if (exponent < EXPONENT_MAX) { - exponent = exponent + 1; - } else { - // Check if we have a zero then just hard clamp, otherwise fail - const digitsString = digits.join(''); - if (digitsString.match(/^0+$/)) { - exponent = EXPONENT_MAX; - break; + if (nDigitsStored < nDigits) { + if ( + representation[nDigits - 1 + Number(sawSign) + Number(sawRadix)] !== '0' && + significantDigits !== 0 + ) { + invalidErr(representation, 'inexact rounding'); + } + // adjust to match digits not stored + nDigits = nDigits - 1; + } else { + if (digits[lastDigit] !== 0) { + invalidErr(representation, 'inexact rounding'); + } + // adjust to round + lastDigit = lastDigit - 1; } - invalidErr(representation, 'overflow'); - } - } - // Round - // We've normalized the exponent, but might still need to round. - if (lastDigit - firstDigit + 1 < significantDigits) { - let endOfString = nDigitsRead; - - // If we have seen a radix point, 'string' is 1 longer than we have - // documented with ndigits_read, so inc the position of the first nonzero - // digit and the position that digits are read to. - if (sawRadix) { - firstNonZero = firstNonZero + 1; - endOfString = endOfString + 1; - } - // if negative, we need to increment again to account for - sign at start. - if (sawSign) { - firstNonZero = firstNonZero + 1; - endOfString = endOfString + 1; - } - - const roundDigit = parseInt(representation[firstNonZero + lastDigit + 1], 10); - let roundBit = 0; - - if (roundDigit >= 5) { - roundBit = 1; - if (roundDigit === 5) { - roundBit = digits[lastDigit] % 2 === 1 ? 1 : 0; - for (i = firstNonZero + lastDigit + 2; i < endOfString; i++) { - if (parseInt(representation[i], 10)) { - roundBit = 1; - break; - } - } + if (exponent < EXPONENT_MAX) { + exponent = exponent + 1; + } else { + invalidErr(representation, 'overflow'); } } - if (roundBit) { - let dIdx = lastDigit; + // Round + // We've normalized the exponent, but might still need to round. + if (lastDigit + 1 < significantDigits) { + // If we have seen a radix point, 'string' is 1 longer than we have + // documented with ndigits_read, so inc the position of the first nonzero + // digit and the position that digits are read to. + if (sawRadix) { + firstNonZero = firstNonZero + 1; + } + // if saw sign, we need to increment again to account for - or + sign at start. + if (sawSign) { + firstNonZero = firstNonZero + 1; + } - for (; dIdx >= 0; dIdx--) { - if (++digits[dIdx] > 9) { - digits[dIdx] = 0; + const roundDigit = parseInt(representation[firstNonZero + lastDigit + 1], 10); - // overflowed most significant digit - if (dIdx === 0) { - if (exponent < EXPONENT_MAX) { - exponent = exponent + 1; - digits[dIdx] = 1; - } else { - return new Decimal128(isNegative ? INF_NEGATIVE_BUFFER : INF_POSITIVE_BUFFER); - } - } - } else { - break; - } + if (roundDigit !== 0) { + invalidErr(representation, 'inexact rounding'); } } } @@ -804,8 +519,8 @@ export class Decimal128 extends BSONValue { if (significantDigits === 0) { significandHigh = Long.fromNumber(0); significandLow = Long.fromNumber(0); - } else if (lastDigit - firstDigit < 17) { - let dIdx = firstDigit; + } else if (lastDigit < 17) { + let dIdx = 0; significandLow = Long.fromNumber(digits[dIdx++]); significandHigh = new Long(0, 0); @@ -814,7 +529,7 @@ export class Decimal128 extends BSONValue { significandLow = significandLow.add(Long.fromNumber(digits[dIdx])); } } else { - let dIdx = firstDigit; + let dIdx = 0; significandHigh = Long.fromNumber(digits[dIdx++]); for (; dIdx <= lastDigit - 17; dIdx++) { From 89893b409de0a76d956f543774af4970135b2dee Mon Sep 17 00:00:00 2001 From: Warren James Date: Thu, 31 Aug 2023 14:05:01 -0400 Subject: [PATCH 08/12] docs(NODE-5594): Add example to fromStringWithRounding API docs --- src/decimal128.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/decimal128.ts b/src/decimal128.ts index 9314ca33..aca82512 100644 --- a/src/decimal128.ts +++ b/src/decimal128.ts @@ -165,6 +165,18 @@ export class Decimal128 extends BSONValue { * Create a Decimal128 instance from a string representation, allowing for rounding to 34 * significant digits * + * @example Example of a number that will be rounded + * ```ts + * > let d = Decimal128.fromString('37.499999999999999196428571428571375') + * Uncaught: + * BSONError: "37.499999999999999196428571428571375" is not a valid Decimal128 string - inexact rounding + * at invalidErr (/home/wajames/js-bson/lib/bson.cjs:1402:11) + * at Decimal128.fromStringInternal (/home/wajames/js-bson/lib/bson.cjs:1633:25) + * at Decimal128.fromString (/home/wajames/js-bson/lib/bson.cjs:1424:27) + * + * > d = Decimal128.fromStringWithRounding('37.499999999999999196428571428571375') + * new Decimal128("37.49999999999999919642857142857138") + * ``` * @param representation - a numeric string representation. */ static fromStringWithRounding(representation: string): Decimal128 { From bc246b465a7d4936e73081dc681842a264428637 Mon Sep 17 00:00:00 2001 From: Warren James Date: Fri, 1 Sep 2023 10:44:07 -0400 Subject: [PATCH 09/12] style(NODE-5594): rename internal method --- src/decimal128.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/decimal128.ts b/src/decimal128.ts index aca82512..b00c9d70 100644 --- a/src/decimal128.ts +++ b/src/decimal128.ts @@ -158,7 +158,7 @@ export class Decimal128 extends BSONValue { * @param representation - a numeric string representation. */ static fromString(representation: string): Decimal128 { - return Decimal128.fromStringInternal(representation, { allowRounding: false }); + return Decimal128._fromString(representation, { allowRounding: false }); } /** @@ -180,10 +180,10 @@ export class Decimal128 extends BSONValue { * @param representation - a numeric string representation. */ static fromStringWithRounding(representation: string): Decimal128 { - return Decimal128.fromStringInternal(representation, { allowRounding: true }); + return Decimal128._fromString(representation, { allowRounding: true }); } - private static fromStringInternal(representation: string, options: { allowRounding: boolean }) { + private static _fromString(representation: string, options: { allowRounding: boolean }) { // Parse state tracking let isNegative = false; let sawSign = false; From ad65791b42840c57d7cda7594487e264140e35cd Mon Sep 17 00:00:00 2001 From: Warren James Date: Fri, 1 Sep 2023 10:44:42 -0400 Subject: [PATCH 10/12] test(NODE-5594): restructure decimal128 tests --- test/node/decimal128.test.ts | 405 +++++++++++++++++------------------ 1 file changed, 202 insertions(+), 203 deletions(-) diff --git a/test/node/decimal128.test.ts b/test/node/decimal128.test.ts index 4ab7d725..0b29a3f2 100644 --- a/test/node/decimal128.test.ts +++ b/test/node/decimal128.test.ts @@ -71,216 +71,215 @@ describe('Decimal128', function () { done(); }); - it('fromString NaN input', function (done) { - let result = Decimal128.fromString('NaN'); - expect(NAN).to.deep.equal(result.bytes); - result = Decimal128.fromString('+NaN'); - expect(NAN).to.deep.equal(result.bytes); - result = Decimal128.fromString('-NaN'); - expect(NAN).to.deep.equal(result.bytes); - result = Decimal128.fromString('-nan'); - expect(NAN).to.deep.equal(result.bytes); - result = Decimal128.fromString('+nan'); - expect(NAN).to.deep.equal(result.bytes); - result = Decimal128.fromString('nan'); - expect(NAN).to.deep.equal(result.bytes); - result = Decimal128.fromString('Nan'); - expect(NAN).to.deep.equal(result.bytes); - result = Decimal128.fromString('+Nan'); - expect(NAN).to.deep.equal(result.bytes); - result = Decimal128.fromString('-Nan'); - expect(NAN).to.deep.equal(result.bytes); - done(); - }); - - it('fromString infinity input', function (done) { - let result = Decimal128.fromString('Infinity'); - expect(INF_POSITIVE_BUFFER).to.deep.equal(result.bytes); - result = Decimal128.fromString('+Infinity'); - expect(INF_POSITIVE_BUFFER).to.deep.equal(result.bytes); - result = Decimal128.fromString('+Inf'); - expect(INF_POSITIVE_BUFFER).to.deep.equal(result.bytes); - result = Decimal128.fromString('-Inf'); - expect(INF_NEGATIVE_BUFFER).to.deep.equal(result.bytes); - result = Decimal128.fromString('-Infinity'); - expect(INF_NEGATIVE_BUFFER).to.deep.equal(result.bytes); - done(); + context('fromString NaN input', function () { + const inputs = ['NaN', '+NaN', '-NaN', '-nan', '+nan', 'nan', 'Nan', '+Nan', '-Nan']; + for (const input of inputs) { + it(`returns NAN when input is "${input}"`, function () { + const result = Decimal128.fromString(input); + expect(NAN).to.deep.equal(result.bytes); + }); + } }); - it('fromString simple', function (done) { - // Create decimal from string value 1 - let result = Decimal128.fromString('1'); - let bytes = Buffer.from( - [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); + context('fromString inifity input', function () { + const positiveInputs = ['Infinity', '+Infinity', '+Inf']; + const negativeInputs = ['-Inf', '-Infinity']; - // Create decimal from string value 0 - result = Decimal128.fromString('0'); - bytes = Buffer.from( - [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value -0 - result = Decimal128.fromString('-0'); - bytes = Buffer.from( - [ - 0xb0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value -1 - result = Decimal128.fromString('-1'); - bytes = Buffer.from( - [ - 0xb0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 12345678901234567 - result = Decimal128.fromString('12345678901234567'); - bytes = Buffer.from( - [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xdc, 0x54, 0x5d, 0x6b, 0x4b, - 0x87 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 989898983458 - result = Decimal128.fromString('989898983458'); - bytes = Buffer.from( - [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe6, 0x7a, 0x93, 0xc8, - 0x22 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value -12345678901234567 - result = Decimal128.fromString('-12345678901234567'); - bytes = Buffer.from( - [ - 0xb0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xdc, 0x54, 0x5d, 0x6b, 0x4b, - 0x87 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 0.12345 - result = Decimal128.fromString('0.12345'); - bytes = Buffer.from( - [ - 0x30, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, - 0x39 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 0.0012345 - result = Decimal128.fromString('0.0012345'); - bytes = Buffer.from( - [ - 0x30, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, - 0x39 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); + for (const input of positiveInputs) { + it(`returns positive infinity when input is "${input}"`, function () { + const result = Decimal128.fromString(input); + expect(INF_POSITIVE_BUFFER).to.deep.equal(result.bytes); + }); + } - // Create decimal from string value 00012345678901234567 - result = Decimal128.fromString('00012345678901234567'); - bytes = Buffer.from( - [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xdc, 0x54, 0x5d, 0x6b, 0x4b, - 0x87 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - done(); + for (const input of negativeInputs) { + it(`returns negative infinity when input is "${input}"`, function () { + const result = Decimal128.fromString(input); + expect(INF_NEGATIVE_BUFFER).to.deep.equal(result.bytes); + }); + } }); - it('fromString scientific format', function (done) { - // Create decimal from string value 10e0 - let result = Decimal128.fromString('10e0'); - let bytes = Buffer.from( - [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x0a - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 1e1 - result = Decimal128.fromString('1e1'); - bytes = Buffer.from( - [ - 0x30, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 10e-1 - result = Decimal128.fromString('10e-1'); - bytes = Buffer.from( - [ - 0x30, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x0a - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 12345678901234567e6111 - result = Decimal128.fromString('12345678901234567e6111'); - bytes = Buffer.from( - [ - 0x5f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xdc, 0x54, 0x5d, 0x6b, 0x4b, - 0x87 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 1e-6176 - result = Decimal128.fromString('1e-6176'); - bytes = Buffer.from( - [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value "-100E-10 - result = Decimal128.fromString('-100E-10'); - bytes = Buffer.from( - [ - 0xb0, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x64 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); + context('fromString simple', function () { + const tests: { input: string; result: Buffer }[] = [ + { + input: '1', + result: Buffer.from( + [ + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01 + ].reverse() + ) + }, + { + input: '0', + result: Buffer.from( + [ + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 + ].reverse() + ) + }, + { + input: '-0', + result: Buffer.from( + [ + 0xb0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 + ].reverse() + ) + }, + { + input: '-1', + result: Buffer.from( + [ + 0xb0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01 + ].reverse() + ) + }, + { + input: '12345678901234567', + result: Buffer.from( + [ + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xdc, 0x54, 0x5d, 0x6b, + 0x4b, 0x87 + ].reverse() + ) + }, + { + input: '989898983458', + result: Buffer.from( + [ + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe6, 0x7a, 0x93, + 0xc8, 0x22 + ].reverse() + ) + }, + { + input: '-12345678901234567', + result: Buffer.from( + [ + 0xb0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xdc, 0x54, 0x5d, 0x6b, + 0x4b, 0x87 + ].reverse() + ) + }, + { + input: '0.12345', + result: Buffer.from( + [ + 0x30, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x30, 0x39 + ].reverse() + ) + }, + { + input: '0.0012345', + result: Buffer.from( + [ + 0x30, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x30, 0x39 + ].reverse() + ) + }, + { + input: '00012345678901234567', + result: Buffer.from( + [ + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xdc, 0x54, 0x5d, 0x6b, + 0x4b, 0x87 + ].reverse() + ) + } + ]; + + for (const test of tests) { + it(`returns Decimal128 with bytes content: '[${test.result.join(',')}]' with input "${ + test.input + }"`, function () { + const result = Decimal128.fromString(test.input); + expect(result.bytes).to.deep.equal(test.result); + }); + } + }); - // Create decimal from string value 10.50E8 - result = Decimal128.fromString('10.50E8'); - bytes = Buffer.from( - [ - 0x30, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, - 0x1a - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - done(); + context('fromString scientific format', function () { + const tests: { input: string; result: Buffer }[] = [ + { + input: '10e0', + result: Buffer.from( + [ + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0a + ].reverse() + ) + }, + + { + input: '1e1', + result: Buffer.from( + [ + 0x30, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01 + ].reverse() + ) + }, + { + input: '10e-1', + result: Buffer.from( + [ + 0x30, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0a + ].reverse() + ) + }, + { + input: '12345678901234567e6111', + result: Buffer.from( + [ + 0x5f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xdc, 0x54, 0x5d, 0x6b, + 0x4b, 0x87 + ].reverse() + ) + }, + { + input: '1e-6176', + result: Buffer.from( + [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01 + ].reverse() + ) + }, + { + input: '-100E-10', + result: Buffer.from( + [ + 0xb0, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x64 + ].reverse() + ) + }, + { + input: '10.50E8', + result: Buffer.from( + [ + 0x30, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x1a + ].reverse() + ) + } + ]; + + for (const test of tests) { + it(`returns Decimal128 with bytes content: '[${test.result.join(',')}]' with input "${ + test.input + }"`, function () { + const result = Decimal128.fromString(test.input); + expect(result.bytes).to.deep.equal(test.result); + }); + } }); it('fromString large format', function (done) { From 47f8956ec0a2b73af3ee49c1f2a18ceae530c028 Mon Sep 17 00:00:00 2001 From: Warren James Date: Fri, 1 Sep 2023 11:25:09 -0400 Subject: [PATCH 11/12] test(NODE-5594): restructure tests --- test/node/decimal128.test.ts | 2328 +++++++++++++++++----------------- 1 file changed, 1147 insertions(+), 1181 deletions(-) diff --git a/test/node/decimal128.test.ts b/test/node/decimal128.test.ts index 0b29a3f2..b925475e 100644 --- a/test/node/decimal128.test.ts +++ b/test/node/decimal128.test.ts @@ -21,1127 +21,1123 @@ const INF_POSITIVE_BUFFER = Buffer.from( ].reverse() ); +function generateFromStringTests(tests: { input: string; result: Buffer }[]) { + for (const test of tests) { + it(`returns Decimal128 with byte content: '[${test.result.join(',')}]' with input "${ + test.input + }"`, function () { + const result = Decimal128.fromString(test.input); + expect(result.bytes).to.deep.equal(test.result); + }); + } +} describe('Decimal128', function () { /** * @ignore */ - it('fromString invalid input', function (done) { - expect(function () { - Decimal128.fromString('E02'); - }).to.throw; - expect(function () { - Decimal128.fromString('E+02'); - }).to.throw; - expect(function () { - Decimal128.fromString('e+02'); - }).to.throw; - expect(function () { - Decimal128.fromString('.'); - }).to.throw; - expect(function () { - Decimal128.fromString('.e'); - }).to.throw; - expect(function () { - Decimal128.fromString(''); - }).to.throw; - expect(function () { - Decimal128.fromString('invalid'); - }).to.throw; - expect(function () { - Decimal128.fromString('in'); - }).to.throw; - expect(function () { - Decimal128.fromString('i'); - }).to.throw; - expect(function () { - Decimal128.fromString('..1'); - }).to.throw; - expect(function () { - Decimal128.fromString('1abcede'); - }).to.throw; - expect(function () { - Decimal128.fromString('1.24abc'); - }).to.throw; - expect(function () { - Decimal128.fromString('1.24abcE+02'); - }).to.throw; - expect(function () { - Decimal128.fromString('1.24E+02abc2d'); - }).to.throw; - done(); - }); + context('fromString', function () { + context('invalid input', function () { + const inputs = [ + 'E02', + 'E+02', + 'e+02', + '.', + '.e', + '', + 'invalid', + 'in', + 'i', + '..1', + '1abcede', + '1.24abc', + '1.24abcE+02', + '1.24E+02abc2d' + ]; + for (const input of inputs) { + it(`throws with input: "${input}"`, function () { + expect(() => Decimal128.fromString(input)).to.throw( + BSONError, + /not a valid Decimal128 string/ + ); + }); + } + }); - context('fromString NaN input', function () { - const inputs = ['NaN', '+NaN', '-NaN', '-nan', '+nan', 'nan', 'Nan', '+Nan', '-Nan']; - for (const input of inputs) { - it(`returns NAN when input is "${input}"`, function () { - const result = Decimal128.fromString(input); - expect(NAN).to.deep.equal(result.bytes); - }); - } - }); + context('NaN input', function () { + const inputs = ['NaN', '+NaN', '-NaN', '-nan', '+nan', 'nan', 'Nan', '+Nan', '-Nan']; + for (const input of inputs) { + it(`returns NAN when input is "${input}"`, function () { + const result = Decimal128.fromString(input); + expect(NAN).to.deep.equal(result.bytes); + }); + } + }); - context('fromString inifity input', function () { - const positiveInputs = ['Infinity', '+Infinity', '+Inf']; - const negativeInputs = ['-Inf', '-Infinity']; + context('positive infinity input', function () { + const inputs = ['Infinity', '+Infinity', '+Inf']; + for (const input of inputs) { + it(`returns positive infinity when input is "${input}"`, function () { + const result = Decimal128.fromString(input); + expect(INF_POSITIVE_BUFFER).to.deep.equal(result.bytes); + }); + } + }); - for (const input of positiveInputs) { - it(`returns positive infinity when input is "${input}"`, function () { - const result = Decimal128.fromString(input); - expect(INF_POSITIVE_BUFFER).to.deep.equal(result.bytes); - }); - } + context('negative infinity input', function () { + const inputs = ['-Inf', '-Infinity']; + for (const input of inputs) { + it(`returns negative infinity when input is "${input}"`, function () { + const result = Decimal128.fromString(input); + expect(INF_NEGATIVE_BUFFER).to.deep.equal(result.bytes); + }); + } + }); + + context('simple input', function () { + const tests: { input: string; result: Buffer }[] = [ + { + input: '1', + result: Buffer.from( + [ + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01 + ].reverse() + ) + }, + { + input: '0', + result: Buffer.from( + [ + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 + ].reverse() + ) + }, + { + input: '-0', + result: Buffer.from( + [ + 0xb0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 + ].reverse() + ) + }, + { + input: '-1', + result: Buffer.from( + [ + 0xb0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01 + ].reverse() + ) + }, + { + input: '12345678901234567', + result: Buffer.from( + [ + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xdc, 0x54, 0x5d, 0x6b, + 0x4b, 0x87 + ].reverse() + ) + }, + { + input: '989898983458', + result: Buffer.from( + [ + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe6, 0x7a, 0x93, + 0xc8, 0x22 + ].reverse() + ) + }, + { + input: '-12345678901234567', + result: Buffer.from( + [ + 0xb0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xdc, 0x54, 0x5d, 0x6b, + 0x4b, 0x87 + ].reverse() + ) + }, + { + input: '0.12345', + result: Buffer.from( + [ + 0x30, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x30, 0x39 + ].reverse() + ) + }, + { + input: '0.0012345', + result: Buffer.from( + [ + 0x30, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x30, 0x39 + ].reverse() + ) + }, + { + input: '00012345678901234567', + result: Buffer.from( + [ + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xdc, 0x54, 0x5d, 0x6b, + 0x4b, 0x87 + ].reverse() + ) + } + ]; + + generateFromStringTests(tests); + }); + + context('scientific format', function () { + const tests: { input: string; result: Buffer }[] = [ + { + input: '10e0', + result: Buffer.from( + [ + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0a + ].reverse() + ) + }, + + { + input: '1e1', + result: Buffer.from( + [ + 0x30, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01 + ].reverse() + ) + }, + { + input: '10e-1', + result: Buffer.from( + [ + 0x30, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0a + ].reverse() + ) + }, + { + input: '12345678901234567e6111', + result: Buffer.from( + [ + 0x5f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xdc, 0x54, 0x5d, 0x6b, + 0x4b, 0x87 + ].reverse() + ) + }, + { + input: '1e-6176', + result: Buffer.from( + [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01 + ].reverse() + ) + }, + { + input: '-100E-10', + result: Buffer.from( + [ + 0xb0, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x64 + ].reverse() + ) + }, + { + input: '10.50E8', + result: Buffer.from( + [ + 0x30, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x1a + ].reverse() + ) + } + ]; + + generateFromStringTests(tests); + }); + + context('large format', function () { + const tests: { input: string; result: Buffer }[] = [ + { + input: '12345689012345789012345', + result: Buffer.from( + [ + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x02, 0x9d, 0x42, 0xda, 0x3a, 0x76, 0xf9, 0xe0, + 0xd9, 0x79 + ].reverse() + ) + }, + { + input: '1234567890123456789012345678901234', + result: Buffer.from( + [ + 0x30, 0x40, 0x3c, 0xde, 0x6f, 0xff, 0x97, 0x32, 0xde, 0x82, 0x5c, 0xd0, 0x7e, 0x96, + 0xaf, 0xf2 + ].reverse() + ) + }, + { + input: '9.999999999999999999999999999999999E+6144', + result: Buffer.from( + [ + 0x5f, 0xff, 0xed, 0x09, 0xbe, 0xad, 0x87, 0xc0, 0x37, 0x8d, 0x8e, 0x63, 0xff, 0xff, + 0xff, 0xff + ].reverse() + ) + }, + { + input: '9.999999999999999999999999999999999E-6143', + result: Buffer.from( + [ + 0x00, 0x01, 0xed, 0x09, 0xbe, 0xad, 0x87, 0xc0, 0x37, 0x8d, 0x8e, 0x63, 0xff, 0xff, + 0xff, 0xff + ].reverse() + ) + }, + { + input: '5.192296858534827628530496329220095E+33', + result: Buffer.from( + [ + 0x30, 0x40, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff + ].reverse() + ) + } + ]; + + generateFromStringTests(tests); + }); + + context('exponent normalization', function () { + const tests: { input: string; result: Buffer }[] = [ + { + input: '1000000000000000000000000000000000000000', + result: Buffer.from( + [ + 0x30, 0x4c, 0x31, 0x4d, 0xc6, 0x44, 0x8d, 0x93, 0x38, 0xc1, 0x5b, 0x0a, 0x00, 0x00, + 0x00, 0x00 + ].reverse() + ) + }, + { + input: '10000000000000000000000000000000000', + result: Buffer.from( + [ + 0x30, 0x42, 0x31, 0x4d, 0xc6, 0x44, 0x8d, 0x93, 0x38, 0xc1, 0x5b, 0x0a, 0x00, 0x00, + 0x00, 0x00 + ].reverse() + ) + }, + { + input: '1000000000000000000000000000000000', + result: Buffer.from( + [ + 0x30, 0x40, 0x31, 0x4d, 0xc6, 0x44, 0x8d, 0x93, 0x38, 0xc1, 0x5b, 0x0a, 0x00, 0x00, + 0x00, 0x00 + ].reverse() + ) + }, + { + input: + '100000000000000000000000000000000000000000000000000000000000000000000' + + '000000000000000000000000000000000000000000000000000000000000000000000' + + '000000000000000000000000000000000000000000000000000000000000000000000' + + '000000000000000000000000000000000000000000000000000000000000000000000' + + '000000000000000000000000000000000000000000000000000000000000000000000' + + '000000000000000000000000000000000000000000000000000000000000000000000' + + '000000000000000000000000000000000000000000000000000000000000000000000' + + '000000000000000000000000000000000000000000000000000000000000000000000' + + '000000000000000000000000000000000000000000000000000000000000000000000' + + '000000000000000000000000000000000000000000000000000000000000000000000' + + '000000000000000000000000000000000000000000000000000000000000000000000' + + '000000000000000000000000000000000000000000000000000000000000000000000' + + '000000000000000000000000000000000000000000000000000000000000000000000' + + '000000000000000000000000000000000000000000000000000000000000000000000' + + '0000000000000000000000000000000000', + result: Buffer.from( + [ + 0x37, 0xcc, 0x31, 0x4d, 0xc6, 0x44, 0x8d, 0x93, 0x38, 0xc1, 0x5b, 0x0a, 0x00, 0x00, + 0x00, 0x00 + ].reverse() + ) + } + ]; + + generateFromStringTests(tests); + // this should throw error according to spec. + // Create decimal from string value 1E-6177 + + // var result = Decimal128.fromString('1E-6177'); + // var bytes = Buffer.from( + // [ + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00 + // ].reverse() + // ); + // expect(bytes).to.deep.equal(result.bytes); + }); + + context('from string zeros', function () { + const tests: { input: string; result: Buffer }[] = [ + { + input: '0', + result: Buffer.from( + [ + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 + ].reverse() + ) + }, + { + input: '0e-611', + result: Buffer.from( + [ + 0x2b, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 + ].reverse() + ) + }, + { + input: '0e+6000', + result: Buffer.from( + [ + 0x5f, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 + ].reverse() + ) + }, + { + input: '-0e-1', + result: Buffer.from( + [ + 0xb0, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 + ].reverse() + ) + } + ]; - for (const input of negativeInputs) { - it(`returns negative infinity when input is "${input}"`, function () { - const result = Decimal128.fromString(input); - expect(INF_NEGATIVE_BUFFER).to.deep.equal(result.bytes); + generateFromStringTests(tests); + }); + + it('from string round', function (done) { + // Create decimal from string value 10E-6177 + const result = Decimal128.fromString('10E-6177'); + const bytes = Buffer.from( + [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01 + ].reverse() + ); + + expect(bytes).to.deep.equal(result.bytes); + + // // Create decimal from string value 15E-6177 + // result = Decimal128.fromString('15E-6177'); + // bytes = Buffer.from( + // [ + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x02 + // ].reverse() + // ); + // expect(bytes).to.deep.equal(result.bytes); + + // // var array = new Array(6179); + // // for(var i = 0; i < array.length; i++) array[i] = '0'; + // // array[1] = '.'; + // // array[6177] = '1'; + // // array[6178] = '5'; + // // // Create decimal from string value array + // // result = Decimal128.fromString(array.join('')); + // // bytes = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + // // , 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02].reverse()); + // // expect(bytes).to.deep.equal(result.bytes); + + // // Create decimal from string value 251E-6178 + // result = Decimal128.fromString('251E-6178'); + // bytes = Buffer.from( + // [ + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x03 + // ].reverse() + // ); + // expect(bytes).to.deep.equal(result.bytes); + + // // Create decimal from string value 250E-6178 + // result = Decimal128.fromString('250E-6178'); + // bytes = Buffer.from( + // [ + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x02 + // ].reverse() + // ); + // expect(bytes).to.deep.equal(result.bytes); + + // // Create decimal from string value 10000000000000000000000000000000006 + // result = Decimal128.fromString('10000000000000000000000000000000006'); + // bytes = Buffer.from( + // [ + // 0x30, + // 0x42, + // 0x31, + // 0x4d, + // 0xc6, + // 0x44, + // 0x8d, + // 0x93, + // 0x38, + // 0xc1, + // 0x5b, + // 0x0a, + // 0x00, + // 0x00, + // 0x00, + // 0x01 + // ].reverse() + // ); + // expect(bytes).to.deep.equal(result.bytes); + + // // Create decimal from string value 10000000000000000000000000000000003 + // result = Decimal128.fromString('10000000000000000000000000000000003'); + // bytes = Buffer.from( + // [ + // 0x30, + // 0x42, + // 0x31, + // 0x4d, + // 0xc6, + // 0x44, + // 0x8d, + // 0x93, + // 0x38, + // 0xc1, + // 0x5b, + // 0x0a, + // 0x00, + // 0x00, + // 0x00, + // 0x00 + // ].reverse() + // ); + // expect(bytes).to.deep.equal(result.bytes); + + // // Create decimal from string value 10000000000000000000000000000000005 + // result = Decimal128.fromString('10000000000000000000000000000000005'); + // bytes = Buffer.from( + // [ + // 0x30, + // 0x42, + // 0x31, + // 0x4d, + // 0xc6, + // 0x44, + // 0x8d, + // 0x93, + // 0x38, + // 0xc1, + // 0x5b, + // 0x0a, + // 0x00, + // 0x00, + // 0x00, + // 0x00 + // ].reverse() + // ); + // expect(bytes).to.deep.equal(result.bytes); + + // // Create decimal from string value 100000000000000000000000000000000051 + // result = Decimal128.fromString('100000000000000000000000000000000051'); + // bytes = Buffer.from( + // [ + // 0x30, + // 0x44, + // 0x31, + // 0x4d, + // 0xc6, + // 0x44, + // 0x8d, + // 0x93, + // 0x38, + // 0xc1, + // 0x5b, + // 0x0a, + // 0x00, + // 0x00, + // 0x00, + // 0x01 + // ].reverse() + // ); + // expect(bytes).to.deep.equal(result.bytes); + + // // Create decimal from string value 10000000000000000000000000000000006E6111 + // result = Decimal128.fromString('10000000000000000000000000000000006E6111'); + // bytes = Buffer.from( + // [ + // 0x78, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00 + // ].reverse() + // ); + // expect(bytes).to.deep.equal(result.bytes); + + // // Create decimal from string value 12980742146337069071326240823050239 + // result = Decimal128.fromString('12980742146337069071326240823050239'); + // bytes = Buffer.from( + // [ + // 0x30, + // 0x42, + // 0x40, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00 + // ].reverse() + // ); + // expect(bytes).to.deep.equal(result.bytes); + + // // Create decimal from string value 99999999999999999999999999999999999 + // result = Decimal128.fromString('99999999999999999999999999999999999'); + // bytes = Buffer.from( + // [ + // 0x30, + // 0x44, + // 0x31, + // 0x4d, + // 0xc6, + // 0x44, + // 0x8d, + // 0x93, + // 0x38, + // 0xc1, + // 0x5b, + // 0x0a, + // 0x00, + // 0x00, + // 0x00, + // 0x00 + // ].reverse() + // ); + // expect(bytes).to.deep.equal(result.bytes); + + // // Create decimal from string value 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 + // result = Decimal128.fromString( + // '9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999' + // ); + // bytes = Buffer.from( + // [ + // 0x30, + // 0xc6, + // 0x31, + // 0x4d, + // 0xc6, + // 0x44, + // 0x8d, + // 0x93, + // 0x38, + // 0xc1, + // 0x5b, + // 0x0a, + // 0x00, + // 0x00, + // 0x00, + // 0x00 + // ].reverse() + // ); + // expect(bytes).to.deep.equal(result.bytes); + + // // Create decimal from string value 9999999999999999999999999999999999E6111 + // result = Decimal128.fromString('9999999999999999999999999999999999E6111'); + // bytes = Buffer.from( + // [ + // 0x5f, + // 0xff, + // 0xed, + // 0x09, + // 0xbe, + // 0xad, + // 0x87, + // 0xc0, + // 0x37, + // 0x8d, + // 0x8e, + // 0x63, + // 0xff, + // 0xff, + // 0xff, + // 0xff + // ].reverse() + // ); + // expect(bytes).to.deep.equal(result.bytes); + + // // Create decimal from string value 99999999999999999999999999999999999E6144 + // result = Decimal128.fromString('99999999999999999999999999999999999E6144'); + // bytes = Buffer.from( + // [ + // 0x78, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00, + // 0x00 + // ].reverse() + // ); + // expect(bytes).to.deep.equal(result.bytes); + + done(); + }); + + context("when input has leading '+' and has more than 34 significant digits", function () { + it('throws BSON error on inexact rounding', function () { + expect(() => + Decimal128.fromString('+100000000000000000000000000000000000000000000001') + ).to.throw(BSON.BSONError, /inexact rounding/); }); - } + }); + + context( + 'when input has 1 significant digits, 34 total digits and an exponent greater than exponent_max', + function () { + it('throws BSON error reporting overflow', function () { + expect(() => Decimal128.fromString('1000000000000000000000000000000000e6112')).to.throw( + BSON.BSONError, + /overflow/ + ); + }); + } + ); }); - context('fromString simple', function () { - const tests: { input: string; result: Buffer }[] = [ - { - input: '1', - result: Buffer.from( + describe('toString', function () { + it('toString infinity', function (done) { + let decimal = new Decimal128( + Buffer.from( [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01 + 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 ].reverse() ) - }, - { - input: '0', - result: Buffer.from( + ); + expect('Infinity').to.equal(decimal.toString()); + + decimal = new Decimal128( + Buffer.from( [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ].reverse() ) - }, - { - input: '-0', - result: Buffer.from( + ); + expect('-Infinity').to.equal(decimal.toString()); + done(); + }); + + it('toString NaN', function (done) { + let decimal = new Decimal128( + Buffer.from( [ - 0xb0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ].reverse() ) - }, - { - input: '-1', - result: Buffer.from( + ); + expect('NaN').to.equal(decimal.toString()); + + decimal = new Decimal128( + Buffer.from( [ - 0xb0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01 + 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 ].reverse() ) - }, - { - input: '12345678901234567', - result: Buffer.from( + ); + expect('NaN').to.equal(decimal.toString()); + + decimal = new Decimal128( + Buffer.from( [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xdc, 0x54, 0x5d, 0x6b, - 0x4b, 0x87 + 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 ].reverse() ) - }, - { - input: '989898983458', - result: Buffer.from( + ); + expect('NaN').to.equal(decimal.toString()); + + decimal = new Decimal128( + Buffer.from( [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe6, 0x7a, 0x93, - 0xc8, 0x22 + 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 ].reverse() ) - }, - { - input: '-12345678901234567', - result: Buffer.from( + ); + expect('NaN').to.equal(decimal.toString()); + + decimal = new Decimal128( + Buffer.from( [ - 0xb0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xdc, 0x54, 0x5d, 0x6b, - 0x4b, 0x87 + 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x12 ].reverse() ) - }, - { - input: '0.12345', - result: Buffer.from( + ); + expect('NaN').to.equal(decimal.toString()); + done(); + }); + + it('toString regular', function (done) { + let decimal = new Decimal128( + Buffer.from( [ - 0x30, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x30, 0x39 + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01 ].reverse() ) - }, - { - input: '0.0012345', - result: Buffer.from( + ); + expect('1').to.equal(decimal.toString()); + + decimal = new Decimal128( + Buffer.from( [ - 0x30, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x30, 0x39 + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 ].reverse() ) - }, - { - input: '00012345678901234567', - result: Buffer.from( + ); + expect('0').to.equal(decimal.toString()); + + decimal = new Decimal128( + Buffer.from( [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xdc, 0x54, 0x5d, 0x6b, - 0x4b, 0x87 + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x02 ].reverse() ) - } - ]; - - for (const test of tests) { - it(`returns Decimal128 with bytes content: '[${test.result.join(',')}]' with input "${ - test.input - }"`, function () { - const result = Decimal128.fromString(test.input); - expect(result.bytes).to.deep.equal(test.result); - }); - } - }); + ); + expect('2').to.equal(decimal.toString()); - context('fromString scientific format', function () { - const tests: { input: string; result: Buffer }[] = [ - { - input: '10e0', - result: Buffer.from( + decimal = new Decimal128( + Buffer.from( [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x0a + 0xb0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01 ].reverse() ) - }, + ); + expect('-1').to.equal(decimal.toString()); - { - input: '1e1', - result: Buffer.from( + decimal = new Decimal128( + Buffer.from( [ - 0x30, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01 + 0xb0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 ].reverse() ) - }, - { - input: '10e-1', - result: Buffer.from( + ); + expect('-0').to.equal(decimal.toString()); + + decimal = new Decimal128( + Buffer.from( [ 0x30, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x0a + 0x00, 0x01 ].reverse() ) - }, - { - input: '12345678901234567e6111', - result: Buffer.from( + ); + expect('0.1').to.equal(decimal.toString()); + + decimal = new Decimal128( + Buffer.from( [ - 0x5f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xdc, 0x54, 0x5d, 0x6b, - 0x4b, 0x87 + 0x30, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0xd2 ].reverse() ) - }, - { - input: '1e-6176', - result: Buffer.from( + ); + expect('0.001234').to.equal(decimal.toString()); + + decimal = new Decimal128( + Buffer.from( [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01 + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0xbe, 0x99, + 0x1a, 0x14 ].reverse() ) - }, - { - input: '-100E-10', - result: Buffer.from( + ); + expect('123456789012').to.equal(decimal.toString()); + + decimal = new Decimal128( + Buffer.from( [ - 0xb0, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x64 + 0x30, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x5a, + 0xef, 0x40 ].reverse() ) - }, - { - input: '10.50E8', - result: Buffer.from( + ); + expect('0.00123400000').to.equal(decimal.toString()); + + decimal = new Decimal128( + Buffer.from( [ - 0x30, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x04, 0x1a + 0x2f, 0xfc, 0x3c, 0xde, 0x6f, 0xff, 0x97, 0x32, 0xde, 0x82, 0x5c, 0xd0, 0x7e, 0x96, + 0xaf, 0xf2 ].reverse() ) - } - ]; - - for (const test of tests) { - it(`returns Decimal128 with bytes content: '[${test.result.join(',')}]' with input "${ - test.input - }"`, function () { - const result = Decimal128.fromString(test.input); - expect(result.bytes).to.deep.equal(test.result); - }); - } - }); - - it('fromString large format', function (done) { - // Create decimal from string value 12345689012345789012345 - let result = Decimal128.fromString('12345689012345789012345'); - let bytes = Buffer.from( - [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x02, 0x9d, 0x42, 0xda, 0x3a, 0x76, 0xf9, 0xe0, 0xd9, - 0x79 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 1234567890123456789012345678901234 - result = Decimal128.fromString('1234567890123456789012345678901234'); - bytes = Buffer.from( - [ - 0x30, 0x40, 0x3c, 0xde, 0x6f, 0xff, 0x97, 0x32, 0xde, 0x82, 0x5c, 0xd0, 0x7e, 0x96, 0xaf, - 0xf2 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 9.999999999999999999999999999999999E+6144 - result = Decimal128.fromString('9.999999999999999999999999999999999E+6144'); - bytes = Buffer.from( - [ - 0x5f, 0xff, 0xed, 0x09, 0xbe, 0xad, 0x87, 0xc0, 0x37, 0x8d, 0x8e, 0x63, 0xff, 0xff, 0xff, - 0xff - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 9.999999999999999999999999999999999E-6143 - result = Decimal128.fromString('9.999999999999999999999999999999999E-6143'); - bytes = Buffer.from( - [ - 0x00, 0x01, 0xed, 0x09, 0xbe, 0xad, 0x87, 0xc0, 0x37, 0x8d, 0x8e, 0x63, 0xff, 0xff, 0xff, - 0xff - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 5.192296858534827628530496329220095E+33 - result = Decimal128.fromString('5.192296858534827628530496329220095E+33'); - bytes = Buffer.from( - [ - 0x30, 0x40, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - done(); - }); - - it('fromString exponent normalization', function (done) { - // Create decimal from string value 1000000000000000000000000000000000000000 - - let result = Decimal128.fromString('1000000000000000000000000000000000000000'); - let bytes = Buffer.from( - [ - 0x30, 0x4c, 0x31, 0x4d, 0xc6, 0x44, 0x8d, 0x93, 0x38, 0xc1, 0x5b, 0x0a, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 10000000000000000000000000000000000 - result = Decimal128.fromString('10000000000000000000000000000000000'); - bytes = Buffer.from( - [ - 0x30, 0x42, 0x31, 0x4d, 0xc6, 0x44, 0x8d, 0x93, 0x38, 0xc1, 0x5b, 0x0a, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 1000000000000000000000000000000000 - result = Decimal128.fromString('1000000000000000000000000000000000'); - bytes = Buffer.from( - [ - 0x30, 0x40, 0x31, 0x4d, 0xc6, 0x44, 0x8d, 0x93, 0x38, 0xc1, 0x5b, 0x0a, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - const str = - '100000000000000000000000000000000000000000000000000000000000000000000' + - '000000000000000000000000000000000000000000000000000000000000000000000' + - '000000000000000000000000000000000000000000000000000000000000000000000' + - '000000000000000000000000000000000000000000000000000000000000000000000' + - '000000000000000000000000000000000000000000000000000000000000000000000' + - '000000000000000000000000000000000000000000000000000000000000000000000' + - '000000000000000000000000000000000000000000000000000000000000000000000' + - '000000000000000000000000000000000000000000000000000000000000000000000' + - '000000000000000000000000000000000000000000000000000000000000000000000' + - '000000000000000000000000000000000000000000000000000000000000000000000' + - '000000000000000000000000000000000000000000000000000000000000000000000' + - '000000000000000000000000000000000000000000000000000000000000000000000' + - '000000000000000000000000000000000000000000000000000000000000000000000' + - '000000000000000000000000000000000000000000000000000000000000000000000' + - '0000000000000000000000000000000000'; - - // Create decimal from string value str - - result = Decimal128.fromString(str); - bytes = Buffer.from( - [ - 0x37, 0xcc, 0x31, 0x4d, 0xc6, 0x44, 0x8d, 0x93, 0x38, 0xc1, 0x5b, 0x0a, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // this should throw error according to spec. - // Create decimal from string value 1E-6177 - - // var result = Decimal128.fromString('1E-6177'); - // var bytes = Buffer.from( - // [ - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00 - // ].reverse() - // ); - // expect(bytes).to.deep.equal(result.bytes); - done(); - }); - - it('fromString from string zeros', function (done) { - // Create decimal from string value 0 - let result = Decimal128.fromString('0'); - let bytes = Buffer.from( - [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 0e-611 - result = Decimal128.fromString('0e-611'); - bytes = Buffer.from( - [ - 0x2b, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 0e+6000 - result = Decimal128.fromString('0e+6000'); - bytes = Buffer.from( - [ - 0x5f, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - - // Create decimal from string value 1E-6177 - result = Decimal128.fromString('-0e-1'); - bytes = Buffer.from( - [ - 0xb0, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ); - expect(bytes).to.deep.equal(result.bytes); - done(); - }); - - it('fromString from string round', function (done) { - // Create decimal from string value 10E-6177 - const result = Decimal128.fromString('10E-6177'); - const bytes = Buffer.from( - [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01 - ].reverse() - ); - - expect(bytes).to.deep.equal(result.bytes); - - // // Create decimal from string value 15E-6177 - // result = Decimal128.fromString('15E-6177'); - // bytes = Buffer.from( - // [ - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x02 - // ].reverse() - // ); - // expect(bytes).to.deep.equal(result.bytes); - - // // var array = new Array(6179); - // // for(var i = 0; i < array.length; i++) array[i] = '0'; - // // array[1] = '.'; - // // array[6177] = '1'; - // // array[6178] = '5'; - // // // Create decimal from string value array - // // result = Decimal128.fromString(array.join('')); - // // bytes = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - // // , 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02].reverse()); - // // expect(bytes).to.deep.equal(result.bytes); - - // // Create decimal from string value 251E-6178 - // result = Decimal128.fromString('251E-6178'); - // bytes = Buffer.from( - // [ - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x03 - // ].reverse() - // ); - // expect(bytes).to.deep.equal(result.bytes); - - // // Create decimal from string value 250E-6178 - // result = Decimal128.fromString('250E-6178'); - // bytes = Buffer.from( - // [ - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x02 - // ].reverse() - // ); - // expect(bytes).to.deep.equal(result.bytes); - - // // Create decimal from string value 10000000000000000000000000000000006 - // result = Decimal128.fromString('10000000000000000000000000000000006'); - // bytes = Buffer.from( - // [ - // 0x30, - // 0x42, - // 0x31, - // 0x4d, - // 0xc6, - // 0x44, - // 0x8d, - // 0x93, - // 0x38, - // 0xc1, - // 0x5b, - // 0x0a, - // 0x00, - // 0x00, - // 0x00, - // 0x01 - // ].reverse() - // ); - // expect(bytes).to.deep.equal(result.bytes); - - // // Create decimal from string value 10000000000000000000000000000000003 - // result = Decimal128.fromString('10000000000000000000000000000000003'); - // bytes = Buffer.from( - // [ - // 0x30, - // 0x42, - // 0x31, - // 0x4d, - // 0xc6, - // 0x44, - // 0x8d, - // 0x93, - // 0x38, - // 0xc1, - // 0x5b, - // 0x0a, - // 0x00, - // 0x00, - // 0x00, - // 0x00 - // ].reverse() - // ); - // expect(bytes).to.deep.equal(result.bytes); - - // // Create decimal from string value 10000000000000000000000000000000005 - // result = Decimal128.fromString('10000000000000000000000000000000005'); - // bytes = Buffer.from( - // [ - // 0x30, - // 0x42, - // 0x31, - // 0x4d, - // 0xc6, - // 0x44, - // 0x8d, - // 0x93, - // 0x38, - // 0xc1, - // 0x5b, - // 0x0a, - // 0x00, - // 0x00, - // 0x00, - // 0x00 - // ].reverse() - // ); - // expect(bytes).to.deep.equal(result.bytes); - - // // Create decimal from string value 100000000000000000000000000000000051 - // result = Decimal128.fromString('100000000000000000000000000000000051'); - // bytes = Buffer.from( - // [ - // 0x30, - // 0x44, - // 0x31, - // 0x4d, - // 0xc6, - // 0x44, - // 0x8d, - // 0x93, - // 0x38, - // 0xc1, - // 0x5b, - // 0x0a, - // 0x00, - // 0x00, - // 0x00, - // 0x01 - // ].reverse() - // ); - // expect(bytes).to.deep.equal(result.bytes); - - // // Create decimal from string value 10000000000000000000000000000000006E6111 - // result = Decimal128.fromString('10000000000000000000000000000000006E6111'); - // bytes = Buffer.from( - // [ - // 0x78, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00 - // ].reverse() - // ); - // expect(bytes).to.deep.equal(result.bytes); - - // // Create decimal from string value 12980742146337069071326240823050239 - // result = Decimal128.fromString('12980742146337069071326240823050239'); - // bytes = Buffer.from( - // [ - // 0x30, - // 0x42, - // 0x40, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00 - // ].reverse() - // ); - // expect(bytes).to.deep.equal(result.bytes); - - // // Create decimal from string value 99999999999999999999999999999999999 - // result = Decimal128.fromString('99999999999999999999999999999999999'); - // bytes = Buffer.from( - // [ - // 0x30, - // 0x44, - // 0x31, - // 0x4d, - // 0xc6, - // 0x44, - // 0x8d, - // 0x93, - // 0x38, - // 0xc1, - // 0x5b, - // 0x0a, - // 0x00, - // 0x00, - // 0x00, - // 0x00 - // ].reverse() - // ); - // expect(bytes).to.deep.equal(result.bytes); - - // // Create decimal from string value 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 - // result = Decimal128.fromString( - // '9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999' - // ); - // bytes = Buffer.from( - // [ - // 0x30, - // 0xc6, - // 0x31, - // 0x4d, - // 0xc6, - // 0x44, - // 0x8d, - // 0x93, - // 0x38, - // 0xc1, - // 0x5b, - // 0x0a, - // 0x00, - // 0x00, - // 0x00, - // 0x00 - // ].reverse() - // ); - // expect(bytes).to.deep.equal(result.bytes); - - // // Create decimal from string value 9999999999999999999999999999999999E6111 - // result = Decimal128.fromString('9999999999999999999999999999999999E6111'); - // bytes = Buffer.from( - // [ - // 0x5f, - // 0xff, - // 0xed, - // 0x09, - // 0xbe, - // 0xad, - // 0x87, - // 0xc0, - // 0x37, - // 0x8d, - // 0x8e, - // 0x63, - // 0xff, - // 0xff, - // 0xff, - // 0xff - // ].reverse() - // ); - // expect(bytes).to.deep.equal(result.bytes); - - // // Create decimal from string value 99999999999999999999999999999999999E6144 - // result = Decimal128.fromString('99999999999999999999999999999999999E6144'); - // bytes = Buffer.from( - // [ - // 0x78, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00, - // 0x00 - // ].reverse() - // ); - // expect(bytes).to.deep.equal(result.bytes); - - done(); - }); - - it('toString infinity', function (done) { - let decimal = new Decimal128( - Buffer.from( - [ - 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ) - ); - expect('Infinity').to.equal(decimal.toString()); - - decimal = new Decimal128( - Buffer.from( - [ - 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ) - ); - expect('-Infinity').to.equal(decimal.toString()); - done(); - }); - - it('toString NaN', function (done) { - let decimal = new Decimal128( - Buffer.from( - [ - 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ) - ); - expect('NaN').to.equal(decimal.toString()); - - decimal = new Decimal128( - Buffer.from( - [ - 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ) - ); - expect('NaN').to.equal(decimal.toString()); - - decimal = new Decimal128( - Buffer.from( - [ - 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ) - ); - expect('NaN').to.equal(decimal.toString()); - - decimal = new Decimal128( - Buffer.from( - [ - 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ) - ); - expect('NaN').to.equal(decimal.toString()); - - decimal = new Decimal128( - Buffer.from( - [ - 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x12 - ].reverse() - ) - ); - expect('NaN').to.equal(decimal.toString()); - done(); - }); - - it('toString regular', function (done) { - let decimal = new Decimal128( - Buffer.from( - [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01 - ].reverse() - ) - ); - expect('1').to.equal(decimal.toString()); - - decimal = new Decimal128( - Buffer.from( - [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ) - ); - expect('0').to.equal(decimal.toString()); - - decimal = new Decimal128( - Buffer.from( - [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x02 - ].reverse() - ) - ); - expect('2').to.equal(decimal.toString()); - - decimal = new Decimal128( - Buffer.from( - [ - 0xb0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01 - ].reverse() - ) - ); - expect('-1').to.equal(decimal.toString()); - - decimal = new Decimal128( - Buffer.from( - [ - 0xb0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ) - ); - expect('-0').to.equal(decimal.toString()); - - decimal = new Decimal128( - Buffer.from( - [ - 0x30, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01 - ].reverse() - ) - ); - expect('0.1').to.equal(decimal.toString()); - - decimal = new Decimal128( - Buffer.from( - [ - 0x30, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, - 0xd2 - ].reverse() - ) - ); - expect('0.001234').to.equal(decimal.toString()); - - decimal = new Decimal128( - Buffer.from( - [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0xbe, 0x99, 0x1a, - 0x14 - ].reverse() - ) - ); - expect('123456789012').to.equal(decimal.toString()); - - decimal = new Decimal128( - Buffer.from( - [ - 0x30, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x5a, 0xef, - 0x40 - ].reverse() - ) - ); - expect('0.00123400000').to.equal(decimal.toString()); - - decimal = new Decimal128( - Buffer.from( - [ - 0x2f, 0xfc, 0x3c, 0xde, 0x6f, 0xff, 0x97, 0x32, 0xde, 0x82, 0x5c, 0xd0, 0x7e, 0x96, 0xaf, - 0xf2 - ].reverse() - ) - ); - expect('0.1234567890123456789012345678901234').to.equal(decimal.toString()); - done(); - }); + ); + expect('0.1234567890123456789012345678901234').to.equal(decimal.toString()); + done(); + }); - it('toString scientific', function (done) { - let decimal = new Decimal128( - Buffer.from( - [ - 0x5f, 0xfe, 0x31, 0x4d, 0xc6, 0x44, 0x8d, 0x93, 0x38, 0xc1, 0x5b, 0x0a, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ) - ); - expect('1.000000000000000000000000000000000E+6144').to.equal(decimal.toString()); + it('toString scientific', function (done) { + let decimal = new Decimal128( + Buffer.from( + [ + 0x5f, 0xfe, 0x31, 0x4d, 0xc6, 0x44, 0x8d, 0x93, 0x38, 0xc1, 0x5b, 0x0a, 0x00, 0x00, + 0x00, 0x00 + ].reverse() + ) + ); + expect('1.000000000000000000000000000000000E+6144').to.equal(decimal.toString()); - decimal = new Decimal128( - Buffer.from( - [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01 - ].reverse() - ) - ); - expect('1E-6176').to.equal(decimal.toString()); + decimal = new Decimal128( + Buffer.from( + [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01 + ].reverse() + ) + ); + expect('1E-6176').to.equal(decimal.toString()); - decimal = new Decimal128( - Buffer.from( - [ - 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01 - ].reverse() - ) - ); - expect('-1E-6176').to.equal(decimal.toString()); + decimal = new Decimal128( + Buffer.from( + [ + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01 + ].reverse() + ) + ); + expect('-1E-6176').to.equal(decimal.toString()); - decimal = new Decimal128( - Buffer.from( - [ - 0x31, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x18, 0x4d, 0xb6, 0x3e, - 0xb1 - ].reverse() - ) - ); - expect('9.999987654321E+112').to.equal(decimal.toString()); + decimal = new Decimal128( + Buffer.from( + [ + 0x31, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x18, 0x4d, 0xb6, + 0x3e, 0xb1 + ].reverse() + ) + ); + expect('9.999987654321E+112').to.equal(decimal.toString()); - decimal = new Decimal128( - Buffer.from( - [ - 0x5f, 0xff, 0xed, 0x09, 0xbe, 0xad, 0x87, 0xc0, 0x37, 0x8d, 0x8e, 0x63, 0xff, 0xff, 0xff, - 0xff - ].reverse() - ) - ); - expect('9.999999999999999999999999999999999E+6144').to.equal(decimal.toString()); + decimal = new Decimal128( + Buffer.from( + [ + 0x5f, 0xff, 0xed, 0x09, 0xbe, 0xad, 0x87, 0xc0, 0x37, 0x8d, 0x8e, 0x63, 0xff, 0xff, + 0xff, 0xff + ].reverse() + ) + ); + expect('9.999999999999999999999999999999999E+6144').to.equal(decimal.toString()); - decimal = new Decimal128( - Buffer.from( - [ - 0x00, 0x01, 0xed, 0x09, 0xbe, 0xad, 0x87, 0xc0, 0x37, 0x8d, 0x8e, 0x63, 0xff, 0xff, 0xff, - 0xff - ].reverse() - ) - ); - expect('9.999999999999999999999999999999999E-6143').to.equal(decimal.toString()); + decimal = new Decimal128( + Buffer.from( + [ + 0x00, 0x01, 0xed, 0x09, 0xbe, 0xad, 0x87, 0xc0, 0x37, 0x8d, 0x8e, 0x63, 0xff, 0xff, + 0xff, 0xff + ].reverse() + ) + ); + expect('9.999999999999999999999999999999999E-6143').to.equal(decimal.toString()); - decimal = new Decimal128( - Buffer.from( - [ - 0x30, 0x40, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff - ].reverse() - ) - ); - expect('5192296858534827628530496329220095').to.equal(decimal.toString()); + decimal = new Decimal128( + Buffer.from( + [ + 0x30, 0x40, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff + ].reverse() + ) + ); + expect('5192296858534827628530496329220095').to.equal(decimal.toString()); - decimal = new Decimal128( - Buffer.from( - [ - 0x30, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, - 0x1a - ].reverse() - ) - ); - expect('1.050E+9').to.equal(decimal.toString()); + decimal = new Decimal128( + Buffer.from( + [ + 0x30, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x1a + ].reverse() + ) + ); + expect('1.050E+9').to.equal(decimal.toString()); - decimal = new Decimal128( - Buffer.from( - [ - 0x30, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, - 0x1a - ].reverse() - ) - ); - expect('1.050E+4').to.equal(decimal.toString()); + decimal = new Decimal128( + Buffer.from( + [ + 0x30, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x1a + ].reverse() + ) + ); + expect('1.050E+4').to.equal(decimal.toString()); - decimal = new Decimal128( - Buffer.from( - [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x69 - ].reverse() - ) - ); - expect('105').to.equal(decimal.toString()); + decimal = new Decimal128( + Buffer.from( + [ + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x69 + ].reverse() + ) + ); + expect('105').to.equal(decimal.toString()); - decimal = new Decimal128( - Buffer.from( - [ - 0x30, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x69 - ].reverse() - ) - ); - expect('1.05E+3').to.equal(decimal.toString()); + decimal = new Decimal128( + Buffer.from( + [ + 0x30, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x69 + ].reverse() + ) + ); + expect('1.05E+3').to.equal(decimal.toString()); - decimal = new Decimal128( - Buffer.from( - [ - 0x30, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01 - ].reverse() - ) - ); - expect('1E+3').to.equal(decimal.toString()); - done(); - }); + decimal = new Decimal128( + Buffer.from( + [ + 0x30, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01 + ].reverse() + ) + ); + expect('1E+3').to.equal(decimal.toString()); + done(); + }); - it('toString zeros', function (done) { - let decimal = new Decimal128( - Buffer.from( - [ - 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ) - ); - expect('0').to.equal(decimal.toString()); + it('toString zeros', function (done) { + let decimal = new Decimal128( + Buffer.from( + [ + 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 + ].reverse() + ) + ); + expect('0').to.equal(decimal.toString()); - decimal = new Decimal128( - Buffer.from( - [ - 0x32, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ) - ); - expect('0E+300').to.equal(decimal.toString()); + decimal = new Decimal128( + Buffer.from( + [ + 0x32, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 + ].reverse() + ) + ); + expect('0E+300').to.equal(decimal.toString()); - decimal = new Decimal128( - Buffer.from( - [ - 0x2b, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00 - ].reverse() - ) - ); - expect('0E-600').to.equal(decimal.toString()); - done(); + decimal = new Decimal128( + Buffer.from( + [ + 0x2b, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 + ].reverse() + ) + ); + expect('0E-600').to.equal(decimal.toString()); + done(); + }); }); - it('Serialize and Deserialize tests', function (done) { + it('Serialize and Deserialize tests', function () { // Test all methods around a simple serialization at object top level let doc = { value: Decimal128.fromString('1') }; let buffer = BSON.serialize(doc); @@ -1172,168 +1168,138 @@ describe('Decimal128', function () { expect(buffer.length).to.equal(size); expect(doc).to.deep.equal(back); expect('1').to.equal(doc.value.a.toString()); - done(); - }); - - it('accepts strings in the constructor', () => { - expect(new Decimal128('0').toString()).to.equal('0'); - expect(new Decimal128('00').toString()).to.equal('0'); - expect(new Decimal128('0.5').toString()).to.equal('0.5'); - expect(new Decimal128('-0.5').toString()).to.equal('-0.5'); - expect(new Decimal128('-0.0097').toString()).to.equal('-0.0097'); - expect(new Decimal128('-0.0011').toString()).to.equal('-0.0011'); - expect(new Decimal128('-0.00110').toString()).to.equal('-0.00110'); - expect(new Decimal128('0.0011').toString()).to.equal('0.0011'); - expect(new Decimal128('0.00110').toString()).to.equal('0.00110'); - expect(new Decimal128('-1e400').toString()).to.equal('-1E+400'); }); - it('throws correct error for invalid constructor argument type', () => { - const constructorArgErrMsg = 'Decimal128 must take a Buffer or string'; + describe('constructor', function () { + it('accepts strings in the constructor', () => { + expect(new Decimal128('0').toString()).to.equal('0'); + expect(new Decimal128('00').toString()).to.equal('0'); + expect(new Decimal128('0.5').toString()).to.equal('0.5'); + expect(new Decimal128('-0.5').toString()).to.equal('-0.5'); + expect(new Decimal128('-0.0097').toString()).to.equal('-0.0097'); + expect(new Decimal128('-0.0011').toString()).to.equal('-0.0011'); + expect(new Decimal128('-0.00110').toString()).to.equal('-0.00110'); + expect(new Decimal128('0.0011').toString()).to.equal('0.0011'); + expect(new Decimal128('0.00110').toString()).to.equal('0.00110'); + expect(new Decimal128('-1e400').toString()).to.equal('-1E+400'); + }); - // ts-ignore - expect(() => new Decimal128(-0)).to.throw(constructorArgErrMsg); - expect(() => new Decimal128(-1)).to.throw(constructorArgErrMsg); - expect(() => new Decimal128(10)).to.throw(constructorArgErrMsg); - expect(() => new Decimal128(1111111111111111)).to.throw(constructorArgErrMsg); - }); + it('throws correct error for invalid constructor argument type', () => { + const constructorArgErrMsg = 'Decimal128 must take a Buffer or string'; - it('throws correct error for an invalid Buffer constructor argument', () => { - const byteLengthErrMsg = 'Decimal128 must take a Buffer of 16 bytes'; + // ts-ignore + expect(() => new Decimal128(-0)).to.throw(constructorArgErrMsg); + expect(() => new Decimal128(-1)).to.throw(constructorArgErrMsg); + expect(() => new Decimal128(10)).to.throw(constructorArgErrMsg); + expect(() => new Decimal128(1111111111111111)).to.throw(constructorArgErrMsg); + }); - expect(() => new Decimal128(new Uint8Array(0))).to.throw(byteLengthErrMsg); - expect(() => new Decimal128(Buffer.alloc(0))).to.throw(byteLengthErrMsg); - expect(() => new Decimal128(new Uint8Array(3))).to.throw(byteLengthErrMsg); - expect(() => new Decimal128(Buffer.alloc(3))).to.throw(byteLengthErrMsg); - expect(() => new Decimal128(new Uint8Array(17))).to.throw(byteLengthErrMsg); - expect(() => new Decimal128(Buffer.alloc(17))).to.throw(byteLengthErrMsg); - }); + it('throws correct error for an invalid Buffer constructor argument', () => { + const byteLengthErrMsg = 'Decimal128 must take a Buffer of 16 bytes'; - it('does not throw error for an empty Buffer of correct length constructor argument', () => { - expect(() => new Decimal128(Buffer.alloc(16))).to.not.throw(); - expect(() => new Decimal128(new Uint8Array(16))).to.not.throw(); - }); - - context('fromString()', function () { - context("when input has leading '+' and has more than 34 significant digits", function () { - it('throws BSON error on inexact rounding', function () { - expect(() => - Decimal128.fromString('+100000000000000000000000000000000000000000000001') - ).to.throw(BSON.BSONError, /inexact rounding/); - }); + expect(() => new Decimal128(new Uint8Array(0))).to.throw(byteLengthErrMsg); + expect(() => new Decimal128(Buffer.alloc(0))).to.throw(byteLengthErrMsg); + expect(() => new Decimal128(new Uint8Array(3))).to.throw(byteLengthErrMsg); + expect(() => new Decimal128(Buffer.alloc(3))).to.throw(byteLengthErrMsg); + expect(() => new Decimal128(new Uint8Array(17))).to.throw(byteLengthErrMsg); + expect(() => new Decimal128(Buffer.alloc(17))).to.throw(byteLengthErrMsg); }); - context( - 'when input has 1 significant digits, 34 total digits and an exponent greater than exponent_max', - function () { - it('throws BSON error reporting overflow', function () { - expect(() => Decimal128.fromString('1000000000000000000000000000000000e6112')).to.throw( - BSON.BSONError, - /overflow/ - ); - }); - } - ); + it('does not throw error for an empty Buffer of correct length constructor argument', () => { + expect(() => new Decimal128(Buffer.alloc(16))).to.not.throw(); + expect(() => new Decimal128(new Uint8Array(16))).to.not.throw(); + }); + }); - describe('fromStringWithRounding', function () { - context('Corpus tests', function () { - // Filter for only Decimal128 tests - for (const { description, valid, _filename, parseErrors } of corpus.filter(s => - /Decimal128/.test(s.description) - )) { - const scenarioName = `${description} (${_filename})`; - describe(scenarioName, function () { - if (valid) { - // We only care about the extended json inputs because the bson inputs will not test the - // fromString or fromStringWithRounding code paths - const inputs = [ - 'canonical_extjson', - 'degenerate_extjson', - 'relaxed_extjson', - 'converted_extjson' - ]; - for (const validTest of valid) { - context(`Valid Test: ${validTest.description}`, function () { - for (const input of inputs) { - if (validTest[input]) { - describe(`with ${input} input`, function () { - it('has same output as fromString', function () { - const extJSONString: string = JSON.parse(validTest[input]).d - .$numberDecimal; - expect(Decimal128.fromStringWithRounding(extJSONString)).to.deep.equal( - Decimal128.fromString(extJSONString) - ); - }); + describe('fromStringWithRounding', function () { + context('Corpus tests', function () { + // Filter for only Decimal128 tests + for (const { description, valid, _filename, parseErrors } of corpus.filter(s => + /Decimal128/.test(s.description) + )) { + const scenarioName = `${description} (${_filename})`; + describe(scenarioName, function () { + if (valid) { + // We only care about the extended json inputs because the bson inputs will not test the + // fromString or fromStringWithRounding code paths + const inputs = [ + 'canonical_extjson', + 'degenerate_extjson', + 'relaxed_extjson', + 'converted_extjson' + ]; + for (const validTest of valid) { + context(`Valid Test: ${validTest.description}`, function () { + for (const input of inputs) { + if (validTest[input]) { + describe(`with ${input} input`, function () { + it('has same output as fromString', function () { + const extJSONString: string = JSON.parse(validTest[input]).d.$numberDecimal; + expect(Decimal128.fromStringWithRounding(extJSONString)).to.deep.equal( + Decimal128.fromString(extJSONString) + ); }); - } + }); } - }); - } + } + }); } - if (parseErrors) { - // Filter out the inexact rounding tests - for (const parseErrorTest of parseErrors.filter( - p => !/Inexact/.test(p.description) - )) { - context(`ParseError - ${parseErrorTest.description}`, function () { - it('emits the same error as fromString', function () { - const errorOrNull = (f: () => void) => { - try { - f(); - } catch (err) { - return err; - } - return null; - }; - const fromStringError = errorOrNull(() => - Decimal128.fromString(parseErrorTest.string) - ); - const fromStringWithRoundingError = errorOrNull(() => - Decimal128.fromStringWithRounding(parseErrorTest.string) - ); - - expect(fromStringError).to.be.instanceOf(BSONError); - expect(fromStringWithRoundingError).to.be.instanceOf(BSONError); - - expect(fromStringWithRoundingError).to.deep.equal(fromStringError); - }); + } + if (parseErrors) { + // Filter out the inexact rounding tests + for (const parseErrorTest of parseErrors.filter(p => !/Inexact/.test(p.description))) { + context(`ParseError - ${parseErrorTest.description}`, function () { + it('emits the same error as fromString', function () { + const errorOrNull = (f: () => void) => { + try { + f(); + } catch (err) { + return err; + } + return null; + }; + const fromStringError = errorOrNull(() => + Decimal128.fromString(parseErrorTest.string) + ); + const fromStringWithRoundingError = errorOrNull(() => + Decimal128.fromStringWithRounding(parseErrorTest.string) + ); + + expect(fromStringError).to.be.instanceOf(BSONError); + expect(fromStringWithRoundingError).to.be.instanceOf(BSONError); + + expect(fromStringWithRoundingError).to.deep.equal(fromStringError); }); - } + }); } - }); - } - }); - - context('when the input has more than 34 significant digits', function () { - it('does not throw an error', function () { - expect(() => - Decimal128.fromStringWithRounding('37.499999999999999196428571428571375') - ).to.not.throw(); + } }); - context('when the digit to round is >= 5', function () { - it('rounds up correctly', function () { - const result = Decimal128.fromStringWithRounding( - '37.499999999999999196428571428571375' - ); - expect(result.toString()).to.deep.equal('37.49999999999999919642857142857138'); - }); + } + }); + + context('when the input has more than 34 significant digits', function () { + it('does not throw an error', function () { + expect(() => + Decimal128.fromStringWithRounding('37.499999999999999196428571428571375') + ).to.not.throw(); + }); + context('when the digit to round is >= 5', function () { + it('rounds up correctly', function () { + const result = Decimal128.fromStringWithRounding('37.499999999999999196428571428571375'); + expect(result.toString()).to.deep.equal('37.49999999999999919642857142857138'); }); - context('when the digit to round is < 5', function () { - it('rounds down correctly', function () { - const result = Decimal128.fromStringWithRounding( - '37.499999999999999196428571428571374' - ); - expect(result.toString()).to.deep.equal('37.49999999999999919642857142857137'); - }); + }); + context('when the digit to round is < 5', function () { + it('rounds down correctly', function () { + const result = Decimal128.fromStringWithRounding('37.499999999999999196428571428571374'); + expect(result.toString()).to.deep.equal('37.49999999999999919642857142857137'); }); + }); - context('when the digit to round is 9', function () { - it('rounds up and carries correctly', function () { - const result = Decimal128.fromStringWithRounding( - '37.4999999999999999196428571428571399' - ); - expect(result.toString()).to.deep.equal('37.49999999999999991964285714285714'); - }); + context('when the digit to round is 9', function () { + it('rounds up and carries correctly', function () { + const result = Decimal128.fromStringWithRounding('37.4999999999999999196428571428571399'); + expect(result.toString()).to.deep.equal('37.49999999999999991964285714285714'); }); }); }); From a360f1cadfe09866fcafe29a037f9f7a234e1f40 Mon Sep 17 00:00:00 2001 From: Warren James Date: Fri, 1 Sep 2023 11:55:47 -0400 Subject: [PATCH 12/12] test(NODE-5594): remove callback --- test/node/decimal128.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/node/decimal128.test.ts b/test/node/decimal128.test.ts index b925475e..1527fbb2 100644 --- a/test/node/decimal128.test.ts +++ b/test/node/decimal128.test.ts @@ -439,7 +439,7 @@ describe('Decimal128', function () { generateFromStringTests(tests); }); - it('from string round', function (done) { + it('from string round', function () { // Create decimal from string value 10E-6177 const result = Decimal128.fromString('10E-6177'); const bytes = Buffer.from( @@ -775,8 +775,6 @@ describe('Decimal128', function () { // ].reverse() // ); // expect(bytes).to.deep.equal(result.bytes); - - done(); }); context("when input has leading '+' and has more than 34 significant digits", function () {