Skip to content

Commit 29ee345

Browse files
committed
feat: Add 32- and 64-bit decimal support.
1 parent a6c8817 commit 29ee345

File tree

16 files changed

+179
-31
lines changed

16 files changed

+179
-31
lines changed

docs/api/data-types.md

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Each of the methods below returns a `DataType` instance as a standard JavaScript
5050
* [binary](#binary)
5151
* [utf8](#utf8)
5252
* [bool](#bool)
53-
* [decimal](#decimal)
53+
* [decimal](#decimal), [decimal32](#decimal32), [decimal64](#decimal63), [decimal128](#decimal128), [decimal256](#decimal256)
5454
* [date](#date), [dateDay](#dateDay), [dateMillisecond](#dateMillisecond)
5555
* [time](#time), [timeSecond](#timeSecond), [timeMillisecond](#timeMillisecond), [timeMicrosecond](#timeMicrosecond), [timeNanosecond](#timeNanosecond)
5656
* [timestamp](#timestamp)
@@ -239,21 +239,53 @@ bool()
239239
<hr/><a id="decimal" href="#decimal">#</a>
240240
<b>decimal</b>(<i>precision</i>, <i>scale</i>[, <i>bitWidth</i>])
241241

242-
Create an Decimal data type instance for exact decimal values, represented as a 128 or 256-bit integer value in two's complement. Decimals are fixed point numbers with a set *precision* (total number of decimal digits) and *scale* (number of fractional digits). For example, the number `35.42` can be represented as `3542` with *precision* ≥ 4 and *scale* = 2.
242+
Create an Decimal data type instance for exact decimal values, represented as a 32, 64, 128, or 256-bit integer value in two's complement. Decimals are fixed point numbers with a set *precision* (total number of decimal digits) and *scale* (number of fractional digits). For example, the number `35.42` can be represented as `3542` with *precision* ≥ 4 and *scale* = 2.
243243

244-
By default, Flechette converts decimals to 64-bit floating point numbers upon extraction (e.g., mapping `3542` back to `35.42`). While useful for many downstream applications, this conversion may be lossy and introduce inaccuracies. Pass the `useDecimalBigInt` extraction option (e.g., to [`tableFromIPC`](/flechette/api/#tableFromIPC) or [`tableFromArrays`](/flechette/api/#tableFromArrays)) to instead extract decimal data as `BigInt` values.
244+
By default, Flechette converts decimals to 64-bit floating point numbers upon extraction (e.g., mapping `3542` back to `35.42`). While useful for many downstream applications, this conversion may be lossy and introduce inaccuracies. Pass the `useDecimalBigInt` extraction option (e.g., to [`tableFromIPC`](/flechette/api/#tableFromIPC) or [`tableFromArrays`](/flechette/api/#tableFromArrays)) to instead extract decimal data as `BigInt` values (64-bit or larger decimals) or integer `number` values (32-bit decimals).
245245

246246
* *precision* (`number`): The total number of decimal digits that can be represented.
247247
* *scale* (`number`): The number of fractional digits, beyond the decimal point.
248-
* *bitWidth* (`number`): The decimal bit width, one of `128` (default) or `256`.
248+
* *bitWidth* (`number`): The decimal bit width, one of `32`, `64`, `128` (default) or `256`.
249249

250250
```js
251-
import { utf8 } from '@uwdata/flechette';
251+
import { decimal } from '@uwdata/flechette';
252252
// decimal with 18 total digits, including 3 fractional digits
253253
// { typeId: 7, precision: 18, scale: 3, bitWidth: 128, ... }
254254
decimal(18, 3)
255255
```
256256

257+
<hr/><a id="decimal32" href="#decimal32">#</a>
258+
<b>decimal32</b>(<i>precision</i>, <i>scale</i>)
259+
260+
Create a Decimal data type instance that uses 32 bits per decimal. 32-bit decimals are stored within an `Int32Array`.
261+
262+
* *precision* (`number`): The total number of decimal digits that can be represented.
263+
* *scale* (`number`): The number of fractional digits, beyond the decimal point.
264+
265+
<hr/><a id="decimal64" href="#decimal64">#</a>
266+
<b>decimal64</b>(<i>precision</i>, <i>scale</i>)
267+
268+
Create a Decimal data type instance that uses 64 bits per decimal. 64-bit decimals are stored within a `Uint64Array`.
269+
270+
* *precision* (`number`): The total number of decimal digits that can be represented.
271+
* *scale* (`number`): The number of fractional digits, beyond the decimal point.
272+
273+
<hr/><a id="decimal128" href="#decimal128">#</a>
274+
<b>decimal128</b>(<i>precision</i>, <i>scale</i>)
275+
276+
Create a Decimal data type instance that uses 128 bits per decimal. 128-bit decimals are stored within a `Uint64Array` with a stride of 2 (two array entries per decimal value).
277+
278+
* *precision* (`number`): The total number of decimal digits that can be represented.
279+
* *scale* (`number`): The number of fractional digits, beyond the decimal point.
280+
281+
<hr/><a id="decimal256" href="#decimal256">#</a>
282+
<b>decimal256</b>(<i>precision</i>, <i>scale</i>)
283+
284+
Create a Decimal data type instance that uses 256 bits per decimal. 256-bit decimals are stored within a `Uint64Array` with a stride of 4 (four array entries per decimal value).
285+
286+
* *precision* (`number`): The total number of decimal digits that can be represented.
287+
* *scale* (`number`): The number of fractional digits, beyond the decimal point.
288+
257289
### Date
258290

259291
<hr/><a id="date" href="#date">#</a>

src/batch-type.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BinaryBatch, BinaryViewBatch, BoolBatch, DateBatch, DateDayBatch, DateDayMillisecondBatch, DecimalBigIntBatch, DecimalNumberBatch, DenseUnionBatch, DictionaryBatch, DirectBatch, FixedBinaryBatch, FixedListBatch, Float16Batch, Int64Batch, IntervalDayTimeBatch, IntervalMonthDayNanoBatch, LargeBinaryBatch, LargeListBatch, LargeListViewBatch, LargeUtf8Batch, ListBatch, ListViewBatch, MapBatch, MapEntryBatch, NullBatch, RunEndEncodedBatch, SparseUnionBatch, StructBatch, StructProxyBatch, TimestampMicrosecondBatch, TimestampMillisecondBatch, TimestampNanosecondBatch, TimestampSecondBatch, Utf8Batch, Utf8ViewBatch } from './batch.js';
1+
import { BinaryBatch, BinaryViewBatch, BoolBatch, DateBatch, DateDayBatch, DateDayMillisecondBatch, Decimal32NumberBatch, DecimalBigIntBatch, DecimalNumberBatch, DenseUnionBatch, DictionaryBatch, DirectBatch, FixedBinaryBatch, FixedListBatch, Float16Batch, Int64Batch, IntervalDayTimeBatch, IntervalMonthDayNanoBatch, LargeBinaryBatch, LargeListBatch, LargeListViewBatch, LargeUtf8Batch, ListBatch, ListViewBatch, MapBatch, MapEntryBatch, NullBatch, RunEndEncodedBatch, SparseUnionBatch, StructBatch, StructProxyBatch, TimestampMicrosecondBatch, TimestampMillisecondBatch, TimestampNanosecondBatch, TimestampSecondBatch, Utf8Batch, Utf8ViewBatch } from './batch.js';
22
import { DateUnit, IntervalUnit, TimeUnit, Type } from './constants.js';
33
import { invalidDataType } from './data-types.js';
44

@@ -29,7 +29,9 @@ export function batchType(type, options = {}) {
2929
useDate && DateBatch
3030
);
3131
case Type.Decimal:
32-
return useDecimalBigInt ? DecimalBigIntBatch : DecimalNumberBatch;
32+
return bitWidth === 32
33+
? (useDecimalBigInt ? DirectBatch : Decimal32NumberBatch)
34+
: (useDecimalBigInt ? DecimalBigIntBatch : DecimalNumberBatch);
3335
case Type.Interval:
3436
return unit === IntervalUnit.DAY_TIME ? IntervalDayTimeBatch
3537
: unit === IntervalUnit.YEAR_MONTH ? DirectBatch

src/batch.js

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { bisect, float64Array } from './util/arrays.js';
2-
import { divide, fromDecimal128, fromDecimal256, toNumber } from './util/numbers.js';
2+
import { divide, fromDecimal128, fromDecimal256, fromDecimal64, toNumber } from './util/numbers.js';
33
import { decodeBit, readInt32, readInt64 } from './util/read.js';
44
import { decodeUtf8 } from './util/strings.js';
55
import { objectFactory, proxyFactory } from './util/struct.js';
@@ -213,7 +213,7 @@ export class NullBatch extends ArrayBatch {
213213

214214
/**
215215
* A batch that coerces BigInt values to 64-bit numbers.
216-
* * @extends {NumberBatch}
216+
* @extends {NumberBatch}
217217
*/
218218
export class Int64Batch extends NumberBatch {
219219
/**
@@ -259,7 +259,27 @@ export class BoolBatch extends ArrayBatch {
259259
}
260260

261261
/**
262-
* An abstract class for a batch of 128- or 256-bit decimal numbers,
262+
* A batch of 32-bit decimal numbers, returned as converted 64-bit floating
263+
* point numbers. Number coercion may be lossy if the decimal precision can
264+
* not be represented in a 64-bit floating point format.
265+
* @extends {NumberBatch}
266+
*/
267+
export class Decimal32NumberBatch extends NumberBatch {
268+
constructor(options) {
269+
super(options);
270+
const { scale } = /** @type {import('./types.js').DecimalType} */ (this.type);
271+
this.scale = 10 ** scale;
272+
}
273+
/**
274+
* @param {number} index The value index
275+
*/
276+
value(index) {
277+
return /** @type {number} */(this.values[index]) / this.scale;
278+
}
279+
}
280+
281+
/**
282+
* An abstract class for a batch of 64-, 128- or 256-bit decimal numbers,
263283
* accessed in strided BigUint64Arrays.
264284
* @template T
265285
* @extends {Batch<T>}
@@ -268,14 +288,16 @@ export class DecimalBatch extends Batch {
268288
constructor(options) {
269289
super(options);
270290
const { bitWidth, scale } = /** @type {import('./types.js').DecimalType} */ (this.type);
271-
this.decimal = bitWidth === 128 ? fromDecimal128 : fromDecimal256;
291+
this.decimal = bitWidth === 64 ? fromDecimal64
292+
: bitWidth === 128 ? fromDecimal128
293+
: fromDecimal256;
272294
this.scale = 10n ** BigInt(scale);
273295
}
274296
}
275297

276298
/**
277-
* A batch of 128- or 256-bit decimal numbers, returned as converted
278-
* 64-bit numbers. The number coercion may be lossy if the decimal
299+
* A batch of 64-, 128- or 256-bit decimal numbers, returned as converted
300+
* 64-bit floating point numbers. Number coercion may be lossy if the decimal
279301
* precision can not be represented in a 64-bit floating point format.
280302
* @extends {DecimalBatch<number>}
281303
*/
@@ -293,7 +315,7 @@ export class DecimalNumberBatch extends DecimalBatch {
293315
}
294316

295317
/**
296-
* A batch of 128- or 256-bit decimal numbers, returned as scaled
318+
* A batch of 64-, 128- or 256-bit decimal numbers, returned as scaled
297319
* bigint values, such that all fractional digits have been shifted
298320
* to integer places by the decimal type scale factor.
299321
* @extends {DecimalBatch<bigint>}

src/build/builder.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { batchType } from '../batch-type.js';
22
import { IntervalUnit, Type } from '../constants.js';
33
import { invalidDataType } from '../data-types.js';
44
import { isInt64ArrayType } from '../util/arrays.js';
5-
import { toBigInt, toDateDay, toFloat16, toTimestamp } from '../util/numbers.js';
5+
import { toBigInt, toDateDay, toDecimal32, toFloat16, toTimestamp } from '../util/numbers.js';
66
import { BinaryBuilder } from './builders/binary.js';
77
import { BoolBuilder } from './builders/bool.js';
88
import { DecimalBuilder } from './builders/decimal.js';
@@ -65,7 +65,9 @@ export function builder(type, ctx = builderContext()) {
6565
case Type.Bool:
6666
return new BoolBuilder(type, ctx);
6767
case Type.Decimal:
68-
return new DecimalBuilder(type, ctx);
68+
return type.bitWidth === 32
69+
? new TransformBuilder(type, ctx, toDecimal32(type.scale))
70+
: new DecimalBuilder(type, ctx);
6971
case Type.Date:
7072
return new TransformBuilder(type, ctx, type.unit ? toBigInt : toDateDay);
7173
case Type.Timestamp:

src/build/builders/values.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export class DirectBuilder extends ValidityBuilder {
2626
this.values.set(value, index);
2727
}
2828
}
29+
2930
done() {
3031
return {
3132
...super.done(),

src/data-types.js

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -221,26 +221,62 @@ export const utf8 = () => ({
221221
export const bool = () => basicType(Type.Bool);
222222

223223
/**
224-
* Return a Decimal data type instance. Decimal values are represented as 128
225-
* or 256 bit integers in two's complement. Decimals are fixed point numbers
226-
* with a set *precision* (total number of decimal digits) and *scale*
224+
* Return a Decimal data type instance. Decimal values are represented as 32,
225+
* 64, 128, or 256 bit integers in two's complement. Decimals are fixed point
226+
* numbers with a set *precision* (total number of decimal digits) and *scale*
227227
* (number of fractional digits). For example, the number `35.42` can be
228228
* represented as `3542` with *precision* ≥ 4 and *scale* = 2.
229229
* @param {number} precision The decimal precision: the total number of
230230
* decimal digits that can be represented.
231231
* @param {number} scale The number of fractional digits, beyond the
232232
* decimal point.
233-
* @param {128 | 256} [bitWidth] The decimal bit width.
234-
* One of 128 (default) or 256.
233+
* @param {32 | 64 | 128 | 256} [bitWidth] The decimal bit width.
234+
* One of 32, 64, 128 (default), or 256.
235235
* @returns {import('./types.js').DecimalType} The decimal data type.
236236
*/
237237
export const decimal = (precision, scale, bitWidth = 128) => ({
238238
typeId: Type.Decimal,
239239
precision,
240240
scale,
241-
bitWidth: checkOneOf(bitWidth, [128, 256]),
242-
values: uint64Array
241+
bitWidth: checkOneOf(bitWidth, [32, 64, 128, 256]),
242+
values: bitWidth === 32 ? int32Array : uint64Array
243243
});
244+
/**
245+
* Return an Decimal data type instance with a bit width of 32.
246+
* @param {number} precision The decimal precision: the total number of
247+
* decimal digits that can be represented.
248+
* @param {number} scale The number of fractional digits, beyond the
249+
* decimal point.
250+
* @returns {import('./types.js').DecimalType} The decimal data type.
251+
*/
252+
export const decimal32 = (precision, scale) => decimal(precision, scale, 32);
253+
/**
254+
* Return an Decimal data type instance with a bit width of 64.
255+
* @param {number} precision The decimal precision: the total number of
256+
* decimal digits that can be represented.
257+
* @param {number} scale The number of fractional digits, beyond the
258+
* decimal point.
259+
* @returns {import('./types.js').DecimalType} The decimal data type.
260+
*/
261+
export const decimal64 = (precision, scale) => decimal(precision, scale, 64);
262+
/**
263+
* Return an Decimal data type instance with a bit width of 128.
264+
* @param {number} precision The decimal precision: the total number of
265+
* decimal digits that can be represented.
266+
* @param {number} scale The number of fractional digits, beyond the
267+
* decimal point.
268+
* @returns {import('./types.js').DecimalType} The decimal data type.
269+
*/
270+
export const decimal128 = (precision, scale) => decimal(precision, scale, 128);
271+
/**
272+
* Return an Decimal data type instance with a bit width of 256.
273+
* @param {number} precision The decimal precision: the total number of
274+
* decimal digits that can be represented.
275+
* @param {number} scale The number of fractional digits, beyond the
276+
* decimal point.
277+
* @returns {import('./types.js').DecimalType} The decimal data type.
278+
*/
279+
export const decimal256 = (precision, scale) => decimal(precision, scale, 256);
244280

245281
/**
246282
* Return a Date data type instance. Date values are 32-bit or 64-bit signed

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export {
1717
binary,
1818
utf8,
1919
bool,
20-
decimal,
20+
decimal, decimal32, decimal64, decimal128, decimal256,
2121
date, dateDay, dateMillisecond,
2222
dictionary,
2323
time, timeSecond, timeMillisecond, timeMicrosecond, timeNanosecond,

src/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ export type DateTimeArrayConstructor =
7575
| Int32ArrayConstructor
7676
| BigInt64ArrayConstructor;
7777

78+
export type DecimalArrayConstructor =
79+
| Int32ArrayConstructor
80+
| BigUint64ArrayConstructor;
81+
7882
export type TypedArrayConstructor =
7983
| Uint8ArrayConstructor
8084
| Uint16ArrayConstructor
@@ -146,7 +150,7 @@ export type Utf8Type = { typeId: 5, offsets: Int32ArrayConstructor };
146150
export type BoolType = { typeId: 6 };
147151

148152
/** Fixed decimal number data type. */
149-
export type DecimalType = { typeId: 7, precision: number, scale: number, bitWidth: 128 | 256, values: BigUint64ArrayConstructor };
153+
export type DecimalType = { typeId: 7, precision: number, scale: number, bitWidth: 32 | 64 | 128 | 256, values: DecimalArrayConstructor };
150154

151155
/** Date data type. */
152156
export type DateType = { typeId: 8, unit: DateUnit_, values: DateTimeArrayConstructor };

src/util/numbers.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,17 @@ export function divide(num, div) {
9595
return Number(num / div) + Number(num % div) / Number(div);
9696
}
9797

98+
/**
99+
* Return a 32-bit decimal conversion method for the given decimal scale.
100+
* @param {number} scale The scale mapping fractional digits to integers.
101+
* @returns {(value: number) => number} A conversion method that maps
102+
* floating point numbers to 32-bit decimals.
103+
*/
104+
export function toDecimal32(scale) {
105+
const s = 10 ** scale;
106+
return (value) => Math.round(value * s) | 0;
107+
}
108+
98109
/**
99110
* Convert a floating point number or bigint to decimal bytes.
100111
* @param {number|bigint} value The number to encode. If a bigint, we assume
@@ -111,16 +122,29 @@ export function toDecimal(value, buf, offset, stride, scale) {
111122
: toBigInt(Math.trunc(value * scale));
112123
// assignment into uint64array performs needed truncation for us
113124
buf[offset] = v;
114-
buf[offset + 1] = (v >> 64n);
115-
if (stride > 2) {
116-
buf[offset + 2] = (v >> 128n);
117-
buf[offset + 3] = (v >> 192n);
125+
if (stride > 1) {
126+
buf[offset + 1] = (v >> 64n);
127+
if (stride > 2) {
128+
buf[offset + 2] = (v >> 128n);
129+
buf[offset + 3] = (v >> 192n);
130+
}
118131
}
119132
}
120133

121134
// helper method to extract uint64 values from bigints
122135
const asUint64 = v => BigInt.asUintN(64, v);
123136

137+
/**
138+
* Convert a 64-bit decimal value to a bigint.
139+
* @param {BigUint64Array} buf The uint64 array containing the decimal bytes.
140+
* @param {number} offset The starting index offset into the array.
141+
* @returns {bigint} The converted decimal as a bigint, such that all
142+
* fractional digits are scaled up to integers (for example, 1.12 -> 112).
143+
*/
144+
export function fromDecimal64(buf, offset) {
145+
return BigInt.asIntN(64, buf[offset]);
146+
}
147+
124148
/**
125149
* Convert a 128-bit decimal value to a bigint.
126150
* @param {BigUint64Array} buf The uint64 array containing the decimal bytes.

test/data/decimal128.arrows

336 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)