Skip to content

Commit b7647c8

Browse files
aljazerzentrxcllnt
authored andcommitted
apacheGH-35067: [JavaScript] toString for signed BigNums (apache#35067)
### What changes are included in this PR? Closes apache#22932 Basically, signed negative numbers were displayed as very large positive numbers. ### Are these changes tested? Yes, I've also added tests for `bn.ts` (BigNum) that was not tested before. ### Are there any user-facing changes? Negative numbers stored in BigNum will now be displayed as negative numbers instead of very large positive numbers. Note that this change also affects decimals, because they are stored in BigNum as signed numbers. Decimals still don't convert to string correctly, because inserting the decimal dot is not implemented. Ref #28804. * Closes: apache#35067 Lead-authored-by: Aljaž Mur Eržen <[email protected]> Co-authored-by: Aljaž Mur Eržen <[email protected]> Co-authored-by: Paul Taylor <[email protected]> Signed-off-by: Dominik Moritz <[email protected]>
1 parent 9a2d7bd commit b7647c8

File tree

3 files changed

+138
-11
lines changed

3 files changed

+138
-11
lines changed

js/gulp/test-task.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const testFiles = [
4141
`test/unit/`,
4242
// `test/unit/bit-tests.ts`,
4343
// `test/unit/int-tests.ts`,
44+
// `test/unit/bn-tests.ts`,
4445
// `test/unit/math-tests.ts`,
4546
// `test/unit/table-tests.ts`,
4647
// `test/unit/generated-data-tests.ts`,

js/src/util/bn.ts

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,17 @@ function BigNum(this: any, x: any, ...xs: any) {
3636
}
3737

3838
BigNum.prototype[isArrowBigNumSymbol] = true;
39-
BigNum.prototype.toJSON = function <T extends BN<BigNumArray>>(this: T) { return `"${bignumToString(this)}"`; };
40-
BigNum.prototype.valueOf = function <T extends BN<BigNumArray>>(this: T) { return bignumToNumber(this); };
41-
BigNum.prototype.toString = function <T extends BN<BigNumArray>>(this: T) { return bignumToString(this); };
39+
BigNum.prototype.toJSON = function <T extends BN<BigNumArray>>(this: T) { return `"${bigNumToString(this)}"`; };
40+
BigNum.prototype.valueOf = function <T extends BN<BigNumArray>>(this: T) { return bigNumToNumber(this); };
41+
BigNum.prototype.toString = function <T extends BN<BigNumArray>>(this: T) { return bigNumToString(this); };
4242
BigNum.prototype[Symbol.toPrimitive] = function <T extends BN<BigNumArray>>(this: T, hint: 'string' | 'number' | 'default' = 'default') {
4343
switch (hint) {
44-
case 'number': return bignumToNumber(this);
45-
case 'string': return bignumToString(this);
46-
case 'default': return bignumToBigInt(this);
44+
case 'number': return bigNumToNumber(this);
45+
case 'string': return bigNumToString(this);
46+
case 'default': return bigNumToBigInt(this);
4747
}
4848
// @ts-ignore
49-
return bignumToString(this);
49+
return bigNumToString(this);
5050
};
5151

5252
/** @ignore */
@@ -70,7 +70,7 @@ Object.assign(UnsignedBigNum.prototype, BigNum.prototype, { 'constructor': Unsig
7070
Object.assign(DecimalBigNum.prototype, BigNum.prototype, { 'constructor': DecimalBigNum, 'signed': true, 'TypedArray': Uint32Array, 'BigIntArray': BigUint64Array });
7171

7272
/** @ignore */
73-
function bignumToNumber<T extends BN<BigNumArray>>(bn: T) {
73+
function bigNumToNumber<T extends BN<BigNumArray>>(bn: T) {
7474
const { buffer, byteOffset, length, 'signed': signed } = bn;
7575
const words = new BigUint64Array(buffer, byteOffset, length);
7676
const negative = signed && words[words.length - 1] & (BigInt(1) << BigInt(63));
@@ -90,12 +90,52 @@ function bignumToNumber<T extends BN<BigNumArray>>(bn: T) {
9090
}
9191

9292
/** @ignore */
93-
export const bignumToString: { <T extends BN<BigNumArray>>(a: T): string } = (<T extends BN<BigNumArray>>(a: T) => a.byteLength === 8 ? `${new a['BigIntArray'](a.buffer, a.byteOffset, 1)[0]}` : decimalToString(a));
93+
export const bigNumToString: { <T extends BN<BigNumArray>>(a: T): string } = (<T extends BN<BigNumArray>>(a: T) => {
94+
// use BigInt native implementation
95+
if (a.byteLength === 8) {
96+
const bigIntArray = new a['BigIntArray'](a.buffer, a.byteOffset, 1);
97+
return `${bigIntArray[0]}`;
98+
}
99+
100+
// unsigned numbers
101+
if (!a['signed']) {
102+
return unsignedBigNumToString(a);
103+
}
104+
105+
let array = new Uint16Array(a.buffer, a.byteOffset, a.byteLength / 2);
106+
107+
// detect positive numbers
108+
const highOrderWord = new Int16Array([array[array.length - 1]])[0];
109+
if (highOrderWord >= 0) {
110+
return unsignedBigNumToString(a);
111+
}
112+
113+
// flip the negative value
114+
array = array.slice();
115+
let carry = 1;
116+
for (let i = 0; i < array.length; i++) {
117+
const elem = array[i];
118+
const updated = ~elem + carry;
119+
array[i] = updated;
120+
carry &= elem === 0 ? 1 : 0;
121+
}
122+
123+
const negated = unsignedBigNumToString(<any>array);
124+
return `-${negated}`;
125+
});
126+
94127
/** @ignore */
95-
export const bignumToBigInt: { <T extends BN<BigNumArray>>(a: T): bigint } = (<T extends BN<BigNumArray>>(a: T) => a.byteLength === 8 ? new a['BigIntArray'](a.buffer, a.byteOffset, 1)[0] : <any>decimalToString(a));
128+
export const bigNumToBigInt: { <T extends BN<BigNumArray>>(a: T): bigint } = (<T extends BN<BigNumArray>>(a: T) => {
129+
if (a.byteLength === 8) {
130+
const bigIntArray = new a['BigIntArray'](a.buffer, a.byteOffset, 1);
131+
return bigIntArray[0];
132+
} else {
133+
return <any>bigNumToString(a);
134+
}
135+
});
96136

97137
/** @ignore */
98-
function decimalToString<T extends BN<BigNumArray>>(a: T) {
138+
function unsignedBigNumToString<T extends BN<BigNumArray>>(a: T) {
99139
let digits = '';
100140
const base64 = new Uint32Array(2);
101141
let base32 = new Uint16Array(a.buffer, a.byteOffset, a.byteLength / 2);

js/test/unit/bn-tests.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
import * as Arrow from 'apache-arrow';
19+
const { BN } = Arrow.util;
20+
21+
describe(`BN`, () => {
22+
test(`to detect signed numbers, unsigned numbers and decimals`, () => {
23+
// SignedBigNum
24+
const i = new BN(new Int32Array([5, 0]));
25+
expect(i.signed).toBe(true);
26+
27+
// UnsignedBigNum
28+
const u = new BN(new Uint32Array([5, 0]));
29+
expect(u.signed).toBe(false);
30+
31+
// DecimalBigNum
32+
const d = new BN(new Uint16Array([1, 2, 3, 4, 5, 6, 7, 8]));
33+
expect(d.signed).toBe(true);
34+
});
35+
36+
test(`toString for signed numbers`, () => {
37+
const i1 = new BN(new Int32Array([5, 33]), true);
38+
expect(i1.toString()).toBe('141733920773');
39+
40+
const i2 = new BN(new Int32Array([0xFFFFFFFF, 0xFFFFFFFF]), true);
41+
expect(i2.toString()).toBe('-1');
42+
43+
const i3 = new BN(new Int32Array([0x11111111, 0x11111111, 0x11111111]), true);
44+
expect(i3.toString()).toBe('5281877500950955839569596689');
45+
46+
const i4 = new BN(new Int16Array([0xFFFF, 0xFFFF]), true);
47+
expect(i4.toString()).toBe('-1');
48+
49+
const i5 = new BN(new Int32Array([0xFFFFFFFE, 0xFFFFFFFF, 0xFFFFFFFF]), true);
50+
expect(i5.toString()).toBe('-2');
51+
});
52+
53+
test(`toString for unsigned numbers`, () => {
54+
const u1 = new BN(new Uint32Array([5, 33]), false);
55+
expect(u1.toString()).toBe('141733920773');
56+
57+
const u2 = new BN(new Uint32Array([0xFFFFFFFF, 0xFFFFFFFF]), false);
58+
expect(u2.toString()).toBe('18446744073709551615');
59+
60+
const u3 = new BN(new Uint32Array([0x11111111, 0x11111111, 0x11111111]), false);
61+
expect(u3.toString()).toBe('5281877500950955839569596689');
62+
63+
const u4 = new BN(new Uint16Array([0xFFFF, 0xFFFF]), false);
64+
expect(u4.toString()).toBe('4294967295');
65+
});
66+
67+
test(`toString for decimals`, () => {
68+
const toDecimal = (val: Uint32Array) => {
69+
const builder = Arrow.makeBuilder({
70+
type: new Arrow.Decimal(128, 0),
71+
nullValues: []
72+
});
73+
builder.append(val);
74+
return <Arrow.Type.Decimal><any>builder.finish().toVector().get(0);
75+
};
76+
77+
const d2 = toDecimal(new Uint32Array([0xFFFFFFFE, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF]));
78+
expect(d2.toString()).toBe('-2');
79+
80+
const d3 = toDecimal(new Uint32Array([0x00000000, 0x00000000, 0x00000000, 0x80000000]));
81+
expect(d3.toString()).toBe('-170141183460469231731687303715884105728');
82+
83+
const d4 = toDecimal(new Uint32Array([0x9D91E773, 0x4BB90CED, 0xAB2354CC, 0x54278E9B]));
84+
expect(d4.toString()).toBe('111860543658909349380118287427608635251');
85+
});
86+
});

0 commit comments

Comments
 (0)