-
Notifications
You must be signed in to change notification settings - Fork 256
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
fix(NODE-5546): decimal 128 fromString performs inexact rounding #613
Changes from 3 commits
097190b
a806d53
1177d20
36a098c
301a7d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -160,6 +160,7 @@ export class Decimal128 extends BSONValue { | |
static fromString(representation: string): Decimal128 { | ||
// Parse state tracking | ||
let isNegative = false; | ||
let sawSign = false; | ||
let sawRadix = false; | ||
let foundNonZero = false; | ||
|
||
|
@@ -187,8 +188,6 @@ export class Decimal128 extends BSONValue { | |
|
||
// 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 | ||
|
@@ -241,6 +240,7 @@ export class Decimal128 extends BSONValue { | |
|
||
// Get the negative or positive sign | ||
if (representation[index] === '+' || representation[index] === '-') { | ||
sawSign = true; | ||
isNegative = representation[index++] === '-'; | ||
} | ||
|
||
|
@@ -263,7 +263,7 @@ export class Decimal128 extends BSONValue { | |
continue; | ||
} | ||
|
||
if (nDigitsStored < 34) { | ||
if (nDigitsStored < MAX_DIGITS) { | ||
if (representation[index] !== '0' || foundNonZero) { | ||
if (!foundNonZero) { | ||
firstNonZero = nDigitsRead; | ||
|
@@ -320,7 +320,11 @@ export class Decimal128 extends BSONValue { | |
lastDigit = nDigitsStored - 1; | ||
significantDigits = nDigits; | ||
if (significantDigits !== 1) { | ||
while (digits[firstNonZero + significantDigits - 1] === 0) { | ||
while ( | ||
representation[ | ||
firstNonZero + significantDigits - 1 + Number(sawSign) + Number(sawRadix) | ||
] === '0' | ||
) { | ||
significantDigits = significantDigits - 1; | ||
} | ||
} | ||
|
@@ -331,7 +335,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)) { | ||
nbbeeken marked this conversation as resolved.
Show resolved
Hide resolved
|
||
exponent = EXPONENT_MIN; | ||
} else { | ||
exponent = exponent - radixPosition; | ||
|
@@ -342,10 +346,9 @@ export class Decimal128 extends BSONValue { | |
// Shift exponent to significand and decrease | ||
lastDigit = lastDigit + 1; | ||
|
||
if (lastDigit - firstDigit > MAX_DIGITS) { | ||
if (lastDigit - firstDigit >= MAX_DIGITS) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check this with a test case There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at it, With regards to changing to the gte sign, the cases that trigger this branch are clamped zeros with large positive increments (see spec tests in decimal128-1.json), but any number with a sufficiently large exponent can enter this block. If we consider a number like
since we skip the iteration when With the gte sign in place, we get the following result, which is what we expect
it seems like we should have spec test that test corner cases like this. |
||
// Check if we have a zero then just hard clamp, otherwise fail | ||
const digitsString = digits.join(''); | ||
if (digitsString.match(/^0+$/)) { | ||
if (significantDigits === 0) { | ||
exponent = EXPONENT_MAX; | ||
break; | ||
} | ||
|
@@ -357,85 +360,57 @@ 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 && significantDigits < nDigitsStored) { | ||
exponent = EXPONENT_MIN; | ||
significantDigits = 0; | ||
break; | ||
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 not allowed'); | ||
nbbeeken marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
// adjust to match digits not stored | ||
nDigits = nDigits - 1; | ||
} else { | ||
if (digits[lastDigit] !== 0) { | ||
invalidErr(representation, 'inexact rounding not allowed'); | ||
} | ||
// 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'); | ||
} | ||
} | ||
|
||
// 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 (isNegative) { | ||
// if saw sign, we need to increment again to account for - or + sign at start. | ||
if (sawSign) { | ||
nbbeeken marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 (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); | ||
} | ||
} | ||
} | ||
} | ||
if (roundDigit !== 0) { | ||
invalidErr(representation, 'inexact rounding not allowed'); | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎵 I saw the sign
And it opened up my eyes, I saw the sign
Life is demanding without understanding 🎵