Skip to content

Commit 6fee2d5

Browse files
feat(NODE-5594): add Decimal128.fromStringWithRounding() static method (#617)
Co-authored-by: hconn-riparian <[email protected]>
1 parent 79ff955 commit 6fee2d5

File tree

3 files changed

+1458
-1276
lines changed

3 files changed

+1458
-1276
lines changed

src/decimal128.ts

Lines changed: 153 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,32 @@ export class Decimal128 extends BSONValue {
158158
* @param representation - a numeric string representation.
159159
*/
160160
static fromString(representation: string): Decimal128 {
161+
return Decimal128._fromString(representation, { allowRounding: false });
162+
}
163+
164+
/**
165+
* Create a Decimal128 instance from a string representation, allowing for rounding to 34
166+
* significant digits
167+
*
168+
* @example Example of a number that will be rounded
169+
* ```ts
170+
* > let d = Decimal128.fromString('37.499999999999999196428571428571375')
171+
* Uncaught:
172+
* BSONError: "37.499999999999999196428571428571375" is not a valid Decimal128 string - inexact rounding
173+
* at invalidErr (/home/wajames/js-bson/lib/bson.cjs:1402:11)
174+
* at Decimal128.fromStringInternal (/home/wajames/js-bson/lib/bson.cjs:1633:25)
175+
* at Decimal128.fromString (/home/wajames/js-bson/lib/bson.cjs:1424:27)
176+
*
177+
* > d = Decimal128.fromStringWithRounding('37.499999999999999196428571428571375')
178+
* new Decimal128("37.49999999999999919642857142857138")
179+
* ```
180+
* @param representation - a numeric string representation.
181+
*/
182+
static fromStringWithRounding(representation: string): Decimal128 {
183+
return Decimal128._fromString(representation, { allowRounding: true });
184+
}
185+
186+
private static _fromString(representation: string, options: { allowRounding: boolean }) {
161187
// Parse state tracking
162188
let isNegative = false;
163189
let sawSign = false;
@@ -351,59 +377,147 @@ export class Decimal128 extends BSONValue {
351377
exponent = exponent - 1;
352378
}
353379

354-
while (exponent < EXPONENT_MIN || nDigitsStored < nDigits) {
355-
// Shift last digit. can only do this if < significant digits than # stored.
356-
if (lastDigit === 0) {
357-
if (significantDigits === 0) {
380+
if (options.allowRounding) {
381+
while (exponent < EXPONENT_MIN || nDigitsStored < nDigits) {
382+
// Shift last digit. can only do this if < significant digits than # stored.
383+
if (lastDigit === 0 && significantDigits < nDigitsStored) {
358384
exponent = EXPONENT_MIN;
385+
significantDigits = 0;
359386
break;
360387
}
361388

362-
invalidErr(representation, 'exponent underflow');
389+
if (nDigitsStored < nDigits) {
390+
// adjust to match digits not stored
391+
nDigits = nDigits - 1;
392+
} else {
393+
// adjust to round
394+
lastDigit = lastDigit - 1;
395+
}
396+
397+
if (exponent < EXPONENT_MAX) {
398+
exponent = exponent + 1;
399+
} else {
400+
// Check if we have a zero then just hard clamp, otherwise fail
401+
const digitsString = digits.join('');
402+
if (digitsString.match(/^0+$/)) {
403+
exponent = EXPONENT_MAX;
404+
break;
405+
}
406+
invalidErr(representation, 'overflow');
407+
}
363408
}
364409

365-
if (nDigitsStored < nDigits) {
366-
if (
367-
representation[nDigits - 1 + Number(sawSign) + Number(sawRadix)] !== '0' &&
368-
significantDigits !== 0
369-
) {
370-
invalidErr(representation, 'inexact rounding');
410+
// Round
411+
// We've normalized the exponent, but might still need to round.
412+
if (lastDigit + 1 < significantDigits) {
413+
let endOfString = nDigitsRead;
414+
415+
// If we have seen a radix point, 'string' is 1 longer than we have
416+
// documented with ndigits_read, so inc the position of the first nonzero
417+
// digit and the position that digits are read to.
418+
if (sawRadix) {
419+
firstNonZero = firstNonZero + 1;
420+
endOfString = endOfString + 1;
371421
}
372-
// adjust to match digits not stored
373-
nDigits = nDigits - 1;
374-
} else {
375-
if (digits[lastDigit] !== 0) {
376-
invalidErr(representation, 'inexact rounding');
422+
// if negative, we need to increment again to account for - sign at start.
423+
if (sawSign) {
424+
firstNonZero = firstNonZero + 1;
425+
endOfString = endOfString + 1;
377426
}
378-
// adjust to round
379-
lastDigit = lastDigit - 1;
380-
}
381427

382-
if (exponent < EXPONENT_MAX) {
383-
exponent = exponent + 1;
384-
} else {
385-
invalidErr(representation, 'overflow');
386-
}
387-
}
428+
const roundDigit = parseInt(representation[firstNonZero + lastDigit + 1], 10);
429+
let roundBit = 0;
430+
431+
if (roundDigit >= 5) {
432+
roundBit = 1;
433+
if (roundDigit === 5) {
434+
roundBit = digits[lastDigit] % 2 === 1 ? 1 : 0;
435+
for (let i = firstNonZero + lastDigit + 2; i < endOfString; i++) {
436+
if (parseInt(representation[i], 10)) {
437+
roundBit = 1;
438+
break;
439+
}
440+
}
441+
}
442+
}
388443

389-
// Round
390-
// We've normalized the exponent, but might still need to round.
391-
if (lastDigit + 1 < significantDigits) {
392-
// If we have seen a radix point, 'string' is 1 longer than we have
393-
// documented with ndigits_read, so inc the position of the first nonzero
394-
// digit and the position that digits are read to.
395-
if (sawRadix) {
396-
firstNonZero = firstNonZero + 1;
444+
if (roundBit) {
445+
let dIdx = lastDigit;
446+
447+
for (; dIdx >= 0; dIdx--) {
448+
if (++digits[dIdx] > 9) {
449+
digits[dIdx] = 0;
450+
451+
// overflowed most significant digit
452+
if (dIdx === 0) {
453+
if (exponent < EXPONENT_MAX) {
454+
exponent = exponent + 1;
455+
digits[dIdx] = 1;
456+
} else {
457+
return new Decimal128(isNegative ? INF_NEGATIVE_BUFFER : INF_POSITIVE_BUFFER);
458+
}
459+
}
460+
} else {
461+
break;
462+
}
463+
}
464+
}
397465
}
398-
// if saw sign, we need to increment again to account for - or + sign at start.
399-
if (sawSign) {
400-
firstNonZero = firstNonZero + 1;
466+
} else {
467+
while (exponent < EXPONENT_MIN || nDigitsStored < nDigits) {
468+
// Shift last digit. can only do this if < significant digits than # stored.
469+
if (lastDigit === 0) {
470+
if (significantDigits === 0) {
471+
exponent = EXPONENT_MIN;
472+
break;
473+
}
474+
475+
invalidErr(representation, 'exponent underflow');
476+
}
477+
478+
if (nDigitsStored < nDigits) {
479+
if (
480+
representation[nDigits - 1 + Number(sawSign) + Number(sawRadix)] !== '0' &&
481+
significantDigits !== 0
482+
) {
483+
invalidErr(representation, 'inexact rounding');
484+
}
485+
// adjust to match digits not stored
486+
nDigits = nDigits - 1;
487+
} else {
488+
if (digits[lastDigit] !== 0) {
489+
invalidErr(representation, 'inexact rounding');
490+
}
491+
// adjust to round
492+
lastDigit = lastDigit - 1;
493+
}
494+
495+
if (exponent < EXPONENT_MAX) {
496+
exponent = exponent + 1;
497+
} else {
498+
invalidErr(representation, 'overflow');
499+
}
401500
}
402501

403-
const roundDigit = parseInt(representation[firstNonZero + lastDigit + 1], 10);
502+
// Round
503+
// We've normalized the exponent, but might still need to round.
504+
if (lastDigit + 1 < significantDigits) {
505+
// If we have seen a radix point, 'string' is 1 longer than we have
506+
// documented with ndigits_read, so inc the position of the first nonzero
507+
// digit and the position that digits are read to.
508+
if (sawRadix) {
509+
firstNonZero = firstNonZero + 1;
510+
}
511+
// if saw sign, we need to increment again to account for - or + sign at start.
512+
if (sawSign) {
513+
firstNonZero = firstNonZero + 1;
514+
}
515+
516+
const roundDigit = parseInt(representation[firstNonZero + lastDigit + 1], 10);
404517

405-
if (roundDigit !== 0) {
406-
invalidErr(representation, 'inexact rounding');
518+
if (roundDigit !== 0) {
519+
invalidErr(representation, 'inexact rounding');
520+
}
407521
}
408522
}
409523

@@ -507,7 +621,6 @@ export class Decimal128 extends BSONValue {
507621
// Return the new Decimal128
508622
return new Decimal128(buffer);
509623
}
510-
511624
/** Create a string representation of the raw Decimal128 value */
512625
toString(): string {
513626
// Note: bits in this routine are referred to starting at 0,

0 commit comments

Comments
 (0)