This is an automated email from the ASF dual-hosted git repository. ptaylor pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/main by this push: new f8f17a58ad GH-45250: [JS] Fix denominator precision loss and remove unnecessary safe integer check for fractional part (#45251) f8f17a58ad is described below commit f8f17a58adb6b5fe4fed6c13d7d3b2e64b4a9acd Author: Kent Yao <y...@apache.org> AuthorDate: Wed Jan 22 11:07:50 2025 +0800 GH-45250: [JS] Fix denominator precision loss and remove unnecessary safe integer check for fractional part (#45251) <!-- Thanks for opening a pull request! If this is your first pull request you can find detailed information on how to contribute here: * [New Contributor's Guide](https://arrow.apache.org/docs/dev/developers/guide/step_by_step/pr_lifecycle.html#reviews-and-merge-of-the-pull-request) * [Contributing Overview](https://arrow.apache.org/docs/dev/developers/overview.html) If this is not a [minor PR](https://github.com/apache/arrow/blob/main/CONTRIBUTING.md#Minor-Fixes). Could you open an issue for this pull request on GitHub? https://github.com/apache/arrow/issues/new/choose Opening GitHub issues ahead of time contributes to the [Openness](http://theapacheway.com/open/#:~:text=Openness%20allows%20new%20users%20the,must%20happen%20in%20the%20open.) of the Apache Arrow project. Then could you also rename the pull request title in the following format? GH-${GITHUB_ISSUE_ID}: [${COMPONENT}] ${SUMMARY} or MINOR: [${COMPONENT}] ${SUMMARY} --> ### Rationale for this change <!-- Why are you proposing this change? If this is already explained clearly in the issue then this section is not needed. Explaining clearly why changes are proposed helps reviewers understand your changes and offer better suggestions for fixes. --> ### What changes are included in this PR? This PR uses string constructor for bigint to calculate the power of 10 with scale as the exponent value of the expression. To Fix precision loss like ```typecript > BigInt(10) ** BigInt(25); 10000000000000000000000000n > BigInt(Math.pow(10, 25)) 10000000000000000905969664n ``` Also, we remove the unnecessary safe integer check for the fraction part. ### Are these changes tested? add some unit tests ### Are there any user-facing changes? no <!-- If there are user-facing changes then we may require documentation to be updated before approving the PR. --> <!-- If there are any breaking changes to public APIs, please uncomment the line below and explain which changes are breaking. --> <!-- **This PR includes breaking changes to public APIs.** --> <!-- Please uncomment the line below (and provide explanation) if the changes fix either (a) a security vulnerability, (b) a bug that caused incorrect or invalid data to be produced, or (c) a bug that causes a crash (even when the API contract is upheld). We use this to highlight fixes to issues that may affect users without their knowledge. For this reason, fixing bugs that cause errors don't count, since those are usually obvious. --> <!-- **This PR contains a "Critical Fix".** --> * GitHub Issue: #45250 --------- Co-authored-by: Paul Taylor <178183+trxcl...@users.noreply.github.com> --- js/src/util/bn.ts | 11 +++++++---- js/test/unit/bn-tests.ts | 13 ++++++++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/js/src/util/bn.ts b/js/src/util/bn.ts index 8f6dfe258f..8eed390924 100644 --- a/js/src/util/bn.ts +++ b/js/src/util/bn.ts @@ -91,11 +91,14 @@ export function bigNumToNumber<T extends BN<BigNumArray>>(bn: T, scale?: number) number |= word * (BigInt(1) << BigInt(64 * i++)); } } - if (typeof scale === 'number') { - const denominator = BigInt(Math.pow(10, scale)); + if (typeof scale === 'number' && scale > 0) { + const denominator = BigInt('1'.padEnd(scale + 1, '0')); const quotient = number / denominator; - const remainder = number % denominator; - return bigIntToNumber(quotient) + (bigIntToNumber(remainder) / bigIntToNumber(denominator)); + const remainder = negative? -(number % denominator) : number % denominator; + const integerPart = bigIntToNumber(quotient); + const fractionPart = `${remainder}`.padStart(scale, '0'); + const sign = negative && integerPart === 0 ? '-' : ''; + return +`${sign}${integerPart}.${fractionPart}`; } return bigIntToNumber(number); } diff --git a/js/test/unit/bn-tests.ts b/js/test/unit/bn-tests.ts index 2ea8f6055d..510b8c2cd6 100644 --- a/js/test/unit/bn-tests.ts +++ b/js/test/unit/bn-tests.ts @@ -16,7 +16,7 @@ // under the License. import * as Arrow from 'apache-arrow'; -const { BN } = Arrow.util; +const { BN, bigNumToNumber } = Arrow.util; describe(`BN`, () => { test(`to detect signed numbers, unsigned numbers and decimals`, () => { @@ -98,4 +98,15 @@ describe(`BN`, () => { // const n6 = new BN(new Uint32Array([0x00000000, 0x00000000, 0x00000000, 0x80000000]), false); // expect(n6.valueOf(1)).toBe(1.7014118346046923e+37); }); + + test(`bigNumToNumber`, () => { + const n1 = new BN(new Uint32Array([3, 2, 1, 0])); + expect(() => bigNumToNumber(n1)).toThrow('18446744082299486211'); + /* eslint-disable @typescript-eslint/no-loss-of-precision */ + expect(bigNumToNumber(n1, 10)).toBeCloseTo(1844674408.2299486); + expect(bigNumToNumber(n1, 15)).toBeCloseTo(18446.744082299486); + expect(bigNumToNumber(n1, 20)).toBeCloseTo(0.18446744082299486); + expect(bigNumToNumber(n1, 25)).toBeCloseTo(0.0000018446744082299486); + /* eslint-enable @typescript-eslint/no-loss-of-precision */ + }); });