Skip to content

feat(NODE-5579): add Decimal128.fromStringWithRounding() static method #621

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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