Skip to content

Commit

Permalink
feat(NODE-5594): add Decimal128.fromStringWithRounding() static method (
Browse files Browse the repository at this point in the history
#617)

Co-authored-by: hconn-riparian <[email protected]>
  • Loading branch information
W-A-James and hconn-riparian committed Sep 12, 2023
1 parent 63fb316 commit 725062b
Show file tree
Hide file tree
Showing 3 changed files with 1,458 additions and 1,276 deletions.
193 changes: 153 additions & 40 deletions src/decimal128.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,32 @@ export class Decimal128 extends BSONValue {
* @param representation - a numeric string representation.
*/
static fromString(representation: string): Decimal128 {
return Decimal128._fromString(representation, { allowRounding: false });
}

/**
* 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 {
return Decimal128._fromString(representation, { allowRounding: true });
}

private static _fromString(representation: string, options: { allowRounding: boolean }) {
// Parse state tracking
let isNegative = false;
let sawSign = false;
Expand Down Expand Up @@ -351,59 +377,147 @@ 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) {
// 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;
}
invalidErr(representation, 'overflow');
}
}

if (nDigitsStored < nDigits) {
if (
representation[nDigits - 1 + Number(sawSign) + Number(sawRadix)] !== '0' &&
significantDigits !== 0
) {
invalidErr(representation, 'inexact rounding');
// Round
// We've normalized the exponent, but might still need to round.
if (lastDigit + 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;
}
// adjust to match digits not stored
nDigits = nDigits - 1;
} else {
if (digits[lastDigit] !== 0) {
invalidErr(representation, 'inexact rounding');
// if negative, we need to increment again to account for - sign at start.
if (sawSign) {
firstNonZero = firstNonZero + 1;
endOfString = endOfString + 1;
}
// adjust to round
lastDigit = lastDigit - 1;
}

if (exponent < EXPONENT_MAX) {
exponent = exponent + 1;
} else {
invalidErr(representation, 'overflow');
}
}
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;
}
}
}
}

// 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 (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;
}
}
}
}
// if saw sign, we need to increment again to account for - or + sign at start.
if (sawSign) {
firstNonZero = firstNonZero + 1;
} else {
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');
}
}

const roundDigit = parseInt(representation[firstNonZero + lastDigit + 1], 10);
// 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');
if (roundDigit !== 0) {
invalidErr(representation, 'inexact rounding');
}
}
}

Expand Down Expand Up @@ -507,7 +621,6 @@ export class Decimal128 extends BSONValue {
// 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,
Expand Down
Loading

0 comments on commit 725062b

Please sign in to comment.