diff --git a/src/scalar/date.ts b/src/scalar/date.ts new file mode 100644 index 0000000..d55df73 --- /dev/null +++ b/src/scalar/date.ts @@ -0,0 +1,68 @@ +import { DataType, Date_ as ArrowDate, DateUnit } from '@apache-arrow/esnext-esm'; +import { DateTime } from 'luxon'; + +import { Scalar } from './scalar.js'; +import { isInvalid, NULL_VALUE } from './util.js'; + +export class Date implements Scalar { + private _valid = false; + private _value: DateTime = DateTime.fromMillis(0); + private _unit: DateUnit; + + public constructor(unit: DateUnit, v?: unknown) { + this._unit = unit; + this.value = v; + return this; + } + + public get dataType(): DataType { + return new ArrowDate(this._unit); + } + + public get valid(): boolean { + return this._valid; + } + + public get value(): DateTime { + return this._value; + } + + public set value(value: unknown) { + if (isInvalid(value)) { + this._valid = false; + return; + } + + if (value instanceof Date && value.dataType === this.dataType) { + this._valid = value.valid; + this._value = value.value; + return; + } + + let dateValue: DateTime | null = null; + + if (typeof value === 'string') { + dateValue = DateTime.fromISO(value, { setZone: true }); + + if (!dateValue.isValid) { + dateValue = DateTime.fromFormat(value, 'yyyy-MM-dd', { zone: 'utc' }); + } + } + + if (dateValue && dateValue.isValid) { + this._value = dateValue; + this._valid = true; + return; + } + + throw new Error(`Unable to set '${value}' as Date`); + } + + public toString(): string { + if (this._valid) { + return this._value.toISO()!; + } + + return NULL_VALUE; + } +} diff --git a/src/scalar/float32.ts b/src/scalar/float32.ts new file mode 100644 index 0000000..0c31ed3 --- /dev/null +++ b/src/scalar/float32.ts @@ -0,0 +1,76 @@ +import { DataType, Float32 as ArrowFloat32 } from '@apache-arrow/esnext-esm'; + +import { Scalar } from './scalar.js'; +import { isInvalid, NULL_VALUE } from './util.js'; + +export class Float32 implements Scalar { + private _valid = false; + private _value: number = 0; + + public constructor(v?: unknown) { + this.value = v; + return this; + } + + public get dataType(): DataType { + return new ArrowFloat32(); + } + + public get valid(): boolean { + return this._valid; + } + + public get value(): number { + return this._value; + } + + public set value(value: unknown) { + if (isInvalid(value)) { + this._valid = false; + return; + } + + if (value instanceof Float32) { + this._valid = value.valid; + this._value = value.value; + return; + } + + if (typeof value === 'number') { + if (!this.validFloat32(value)) { + throw new TypeError(`Value '${value}' cannot be safely converted to Float32`); + } + + this._value = value; + this._valid = true; + return; + } + + const floatValue = Number.parseFloat(String(value)); + if (!Number.isNaN(floatValue)) { + if (!this.validFloat32(floatValue)) { + throw new TypeError(`Value '${value}' cannot be safely converted to Float32`); + } + + this._value = floatValue; + this._valid = true; + return; + } + + throw new Error(`Unable to set '${value}' as Float32`); + } + + public toString() { + if (this._valid) { + return String(this._value); + } + + return NULL_VALUE; + } + + validFloat32(n: number) { + const float32 = new Float32Array(1); + float32[0] = n; + return float32[0] === n; + } +} diff --git a/src/scalar/int16.ts b/src/scalar/int16.ts new file mode 100644 index 0000000..ecb37a7 --- /dev/null +++ b/src/scalar/int16.ts @@ -0,0 +1,74 @@ +import { DataType, Int16 as ArrowInt16 } from '@apache-arrow/esnext-esm'; +import { bigIntToNumber } from '@apache-arrow/esnext-esm/util/bigint.js'; + +import { Scalar } from './scalar.js'; +import { isInvalid, NULL_VALUE } from './util.js'; + +export class Int16 implements Scalar { + private _valid = false; + private _value: bigint = BigInt(0); + + public constructor(v?: unknown) { + this.value = v; + return this; + } + + public get dataType(): DataType { + return new ArrowInt16(); + } + + public get valid(): boolean { + return this._valid; + } + + public get value(): bigint { + return this._value; + } + + public set value(value: unknown) { + if (isInvalid(value)) { + this._valid = false; + return; + } + + if (value instanceof Int16) { + this._valid = value.valid; + this._value = value.value; + return; + } + + if (typeof value === 'bigint') { + if (!this.validInt16(value)) { + throw new TypeError(`Value '${value}' cannot be safely converted to Int16`); + } + this._value = value; + this._valid = true; + return; + } + + if (typeof value === 'number') { + const v = BigInt(value); + if (!this.validInt16(v)) { + throw new TypeError(`Value '${value}' cannot be safely converted to Int16`); + } + this._value = v; + this._valid = true; + return; + } + + throw new Error(`Unable to set '${value}' as Int16`); + } + + public toString() { + if (this._valid) { + return String(this._value); + } + + return NULL_VALUE; + } + + validInt16(n: bigint) { + const number_ = bigIntToNumber(n); + return Number.isSafeInteger(number_) && number_ >= -32_768 && number_ <= 32_767; + } +} diff --git a/src/scalar/int32.ts b/src/scalar/int32.ts new file mode 100644 index 0000000..c998303 --- /dev/null +++ b/src/scalar/int32.ts @@ -0,0 +1,74 @@ +import { DataType, Int32 as ArrowInt32 } from '@apache-arrow/esnext-esm'; +import { bigIntToNumber } from '@apache-arrow/esnext-esm/util/bigint.js'; + +import { Scalar } from './scalar.js'; +import { isInvalid, NULL_VALUE } from './util.js'; + +export class Int32 implements Scalar { + private _valid = false; + private _value: bigint = BigInt(0); + + public constructor(v?: unknown) { + this.value = v; + return this; + } + + public get dataType(): DataType { + return new ArrowInt32(); + } + + public get valid(): boolean { + return this._valid; + } + + public get value(): bigint { + return this._value; + } + + public set value(value: unknown) { + if (isInvalid(value)) { + this._valid = false; + return; + } + + if (value instanceof Int32) { + this._valid = value.valid; + this._value = value.value; + return; + } + + if (typeof value === 'bigint') { + if (!this.validInt32(value)) { + throw new TypeError(`Value '${value}' cannot be safely converted to Int32`); + } + this._value = value; + this._valid = true; + return; + } + + if (typeof value === 'number') { + const v = BigInt(value); + if (!this.validInt32(v)) { + throw new TypeError(`Value '${value}' cannot be safely converted to Int32`); + } + this._value = v; + this._valid = true; + return; + } + + throw new Error(`Unable to set '${value}' as Int32`); + } + + public toString() { + if (this._valid) { + return String(this._value); + } + + return NULL_VALUE; + } + + validInt32(n: bigint) { + const number_ = bigIntToNumber(n); + return Number.isSafeInteger(number_) && number_ >= -2_147_483_648 && number_ <= 2_147_483_647; + } +} diff --git a/src/scalar/int64.ts b/src/scalar/int64.ts index fea0d8b..0f35041 100644 --- a/src/scalar/int64.ts +++ b/src/scalar/int64.ts @@ -1,4 +1,5 @@ import { DataType, Int64 as ArrowInt64 } from '@apache-arrow/esnext-esm'; +import { bigIntToNumber } from '@apache-arrow/esnext-esm/util/bigint.js'; import { Scalar } from './scalar.js'; import { isInvalid, NULL_VALUE } from './util.js'; @@ -37,16 +38,20 @@ export class Int64 implements Scalar { } if (typeof value === 'bigint') { + if (!this.validInt64(value)) { + throw new TypeError(`Value '${value}' cannot be safely converted to Int64`); + } this._value = value; this._valid = true; return; } if (typeof value === 'number') { - if (!Number.isSafeInteger(value)) { + const v = BigInt(value); + if (!this.validInt64(v)) { throw new TypeError(`Value '${value}' cannot be safely converted to Int64`); } - this._value = BigInt(value); + this._value = v; this._valid = true; return; } @@ -61,4 +66,10 @@ export class Int64 implements Scalar { return NULL_VALUE; } + + validInt64(n: bigint) { + const MIN_INT64 = BigInt('-9223372036854775808'); // -2^63 + const MAX_INT64 = BigInt('9223372036854775807'); // 2^63 - 1 + return Number.isSafeInteger(bigIntToNumber(n)) && n >= MIN_INT64 && n <= MAX_INT64; + } } diff --git a/src/scalar/list.test.ts b/src/scalar/list.test.ts index 214f601..92d4703 100644 --- a/src/scalar/list.test.ts +++ b/src/scalar/list.test.ts @@ -4,39 +4,39 @@ import { Int64 } from './int64.js'; import { List } from './list.js'; test('list', (t) => { - const l = new List(Int64); - t.deepEqual(l, new List(Int64)); + const l = new List(new Int64()); + t.deepEqual(l, new List(new Int64())); l.value = [new Int64(17), new Int64(19), new Int64(null)]; t.is(l.length, 3); }); test('list equality', (t) => { - const l1 = new List(Int64); + const l1 = new List(new Int64()); l1.value = [new Int64(17), new Int64(19), new Int64(null)]; - const l2 = new List(Int64); + const l2 = new List(new Int64()); l2.value = [new Int64(17), new Int64(19), new Int64(null)]; t.deepEqual(l1, l2); }); test('list inequality', (t) => { - const l1 = new List(Int64); + const l1 = new List(new Int64()); l1.value = new Int64(4); - const l2 = new List(Int64); + const l2 = new List(new Int64()); l2.value = new Int64(7); t.notDeepEqual(l1, l2); }); test('list equality when invalid', (t) => { - const l1 = new List(Int64); - l1.value = new Int64(null); + const l1 = new List(new Int64()); + l1.value = new Int64(); - const l2 = new List(Int64); - l2.value = new Int64(null); + const l2 = new List(new Int64()); + l2.value = new Int64(); t.deepEqual(l1, l2); }); diff --git a/src/scalar/list.ts b/src/scalar/list.ts index d64525f..78fa613 100644 --- a/src/scalar/list.ts +++ b/src/scalar/list.ts @@ -6,17 +6,24 @@ import { isInvalid, NULL_VALUE } from './util.js'; type TVector> = T[]; export class List> implements Scalar> { - private _type: new (value?: unknown) => T; + private _childScalarInstance: T; private _valid = false; private _value: TVector = []; - constructor(scalarType: new (value?: unknown) => T, initialValue?: unknown) { - this._type = scalarType; - if (!isInvalid(initialValue)) this.value = initialValue; + constructor(childScalarInstance: T, initialValue?: TVector) { + this._childScalarInstance = childScalarInstance; + + if (!isInvalid(initialValue)) { + this._value = initialValue!.map((value) => { + const instance = Object.create(this._childScalarInstance); + instance.value = value; + return instance; + }); + } } get dataType(): DataType { - return new ArrowList(this._type.prototype.dataType); + return new ArrowList(this._childScalarInstance.dataType.ArrayType); } set value(inputValue: unknown) { @@ -30,23 +37,22 @@ export class List> implements Scalar> { const temporaryVector: TVector = []; if (inputArray.length > 0) { - const firstItemScalar = new this._type(); - firstItemScalar.value = inputArray[0]; - const firstItemType = Object.getPrototypeOf(firstItemScalar).constructor; + this._childScalarInstance.value = inputArray[0]; + const firstItemType = Object.getPrototypeOf(this._childScalarInstance).constructor; for (const item of inputArray) { - const scalar = new this._type(); - scalar.value = item; - - if (Object.getPrototypeOf(scalar).constructor !== firstItemType) { + try { + this._childScalarInstance.value = item; + } catch { throw new Error( - `Type mismatch: All items should be of the same type as the first item. Expected type ${ - firstItemType.name - }, but got ${Object.getPrototypeOf(scalar).constructor.name}`, + `Type mismatch: All items should be of the same type as the first item. Expected type ${firstItemType.name}`, ); } - temporaryVector.push(scalar); + // Here, instead of creating a new scalar, we clone the existing instance with the current value + const scalarClone = Object.create(this._childScalarInstance); + scalarClone.value = item; + temporaryVector.push(scalarClone); } this._value = temporaryVector; diff --git a/src/scalar/scalar.ts b/src/scalar/scalar.ts index bd8ad82..13495ad 100644 --- a/src/scalar/scalar.ts +++ b/src/scalar/scalar.ts @@ -1,10 +1,18 @@ -import { DataType } from '@apache-arrow/esnext-esm'; +import { DataType, Precision } from '@apache-arrow/esnext-esm'; import { Bool } from './bool.js'; +import { Date } from './date.js'; +import { Float32 } from './float32.js'; import { Float64 } from './float64.js'; +import { Int16 } from './int16.js'; +import { Int32 } from './int32.js'; import { Int64 } from './int64.js'; +import { List } from './list.js'; import { Text } from './text.js'; import { Timestamp } from './timestamp.js'; +import { Uint16 } from './uint16.js'; +import { Uint32 } from './uint32.js'; +import { Uint64 } from './uint64.js'; export type Stringable = { toString: () => string }; @@ -22,15 +30,61 @@ export const newScalar = (dataType: DataType): Scalar => { if (DataType.isBool(dataType)) { return new Bool(); } + if (DataType.isInt(dataType)) { - return new Int64(); + if (dataType.isSigned) { + switch (dataType.bitWidth) { + case 16: { + return new Int16(); + } + case 32: { + return new Int32(); + } + default: { + return new Int64(); + } + } + } + + switch (dataType.bitWidth) { + case 16: { + return new Uint16(); + } + case 32: { + return new Uint32(); + } + default: { + return new Uint64(); + } + } } + if (DataType.isFloat(dataType)) { - return new Float64(); + switch (dataType.precision) { + // case Precision.HALF: { + // TODO + // } + case Precision.SINGLE: { + return new Float32(); + } + default: { + return new Float64(); + } + } } + if (DataType.isTimestamp(dataType)) { return new Timestamp(); } + if (DataType.isList(dataType)) { + const childScalar = newScalar(dataType.valueType); + return new List(childScalar); + } + + if (DataType.isDate(dataType)) { + return new Date(dataType.unit); + } + return new Text(); }; diff --git a/src/scalar/uint16.ts b/src/scalar/uint16.ts new file mode 100644 index 0000000..454b919 --- /dev/null +++ b/src/scalar/uint16.ts @@ -0,0 +1,74 @@ +import { DataType, Uint16 as ArrowUint16 } from '@apache-arrow/esnext-esm'; +import { bigIntToNumber } from '@apache-arrow/esnext-esm/util/bigint.js'; + +import { Scalar } from './scalar.js'; +import { isInvalid, NULL_VALUE } from './util.js'; + +export class Uint16 implements Scalar { + private _valid = false; + private _value: bigint = BigInt(0); + + public constructor(v?: unknown) { + this.value = v; + return this; + } + + public get dataType(): DataType { + return new ArrowUint16(); + } + + public get valid(): boolean { + return this._valid; + } + + public get value(): bigint { + return this._value; + } + + public set value(value: unknown) { + if (isInvalid(value)) { + this._valid = false; + return; + } + + if (value instanceof Uint16) { + this._valid = value.valid; + this._value = value.value; + return; + } + + if (typeof value === 'bigint') { + if (!this.validUint16(value)) { + throw new TypeError(`Value '${value}' cannot be safely converted to Uint16`); + } + this._value = value; + this._valid = true; + return; + } + + if (typeof value === 'number') { + const v = BigInt(value); + if (!this.validUint16(v)) { + throw new TypeError(`Value '${value}' cannot be safely converted to Uint16`); + } + this._value = v; + this._valid = true; + return; + } + + throw new Error(`Unable to set '${value}' as Uint16`); + } + + public toString() { + if (this._valid) { + return String(this._value); + } + + return NULL_VALUE; + } + + validUint16(n: bigint) { + const number_ = bigIntToNumber(n); + return Number.isSafeInteger(number_) && number_ >= 0 && number_ <= 65_535; + } +} diff --git a/src/scalar/uint32.ts b/src/scalar/uint32.ts new file mode 100644 index 0000000..cfa2335 --- /dev/null +++ b/src/scalar/uint32.ts @@ -0,0 +1,74 @@ +import { DataType, Uint32 as ArrowUint32 } from '@apache-arrow/esnext-esm'; +import { bigIntToNumber } from '@apache-arrow/esnext-esm/util/bigint.js'; + +import { Scalar } from './scalar.js'; +import { isInvalid, NULL_VALUE } from './util.js'; + +export class Uint32 implements Scalar { + private _valid = false; + private _value: bigint = BigInt(0); + + public constructor(v?: unknown) { + this.value = v; + return this; + } + + public get dataType(): DataType { + return new ArrowUint32(); + } + + public get valid(): boolean { + return this._valid; + } + + public get value(): bigint { + return this._value; + } + + public set value(value: unknown) { + if (isInvalid(value)) { + this._valid = false; + return; + } + + if (value instanceof Uint32) { + this._valid = value.valid; + this._value = value.value; + return; + } + + if (typeof value === 'bigint') { + if (!this.validUint32(value)) { + throw new TypeError(`Value '${value}' cannot be safely converted to Uint32`); + } + this._value = value; + this._valid = true; + return; + } + + if (typeof value === 'number') { + const v = BigInt(value); + if (!this.validUint32(v)) { + throw new TypeError(`Value '${value}' cannot be safely converted to Uint32`); + } + this._value = v; + this._valid = true; + return; + } + + throw new Error(`Unable to set '${value}' as Uint32`); + } + + public toString() { + if (this._valid) { + return String(this._value); + } + + return NULL_VALUE; + } + + validUint32(n: bigint) { + const number_ = bigIntToNumber(n); + return Number.isSafeInteger(number_) && number_ >= 0 && number_ <= 4_294_967_295; + } +} diff --git a/src/scalar/uint64.ts b/src/scalar/uint64.ts new file mode 100644 index 0000000..0ec9b3d --- /dev/null +++ b/src/scalar/uint64.ts @@ -0,0 +1,73 @@ +import { DataType, Uint64 as ArrowUint64 } from '@apache-arrow/esnext-esm'; +import { bigIntToNumber } from '@apache-arrow/esnext-esm/util/bigint.js'; + +import { Scalar } from './scalar.js'; +import { isInvalid, NULL_VALUE } from './util.js'; + +export class Uint64 implements Scalar { + private _valid = false; + private _value: bigint = BigInt(0); + + public constructor(v?: unknown) { + this.value = v; + return this; + } + + public get dataType(): DataType { + return new ArrowUint64(); + } + + public get valid(): boolean { + return this._valid; + } + + public get value(): bigint { + return this._value; + } + + public set value(value: unknown) { + if (isInvalid(value)) { + this._valid = false; + return; + } + + if (value instanceof Uint64) { + this._valid = value.valid; + this._value = value.value; + return; + } + + if (typeof value === 'bigint') { + if (!this.validUint64(value)) { + throw new TypeError(`Value '${value}' cannot be safely converted to Uint64`); + } + this._value = value; + this._valid = true; + return; + } + + if (typeof value === 'number') { + const v = BigInt(value); + if (!this.validUint64(v)) { + throw new TypeError(`Value '${value}' cannot be safely converted to Uint64`); + } + this._value = v; + this._valid = true; + return; + } + + throw new Error(`Unable to set '${value}' as Uint64`); + } + + public toString() { + if (this._valid) { + return String(this._value); + } + + return NULL_VALUE; + } + + validUint64(n: bigint) { + return Number.isSafeInteger(bigIntToNumber(n)) && n >= 0 && n <= 18_446_744_073_709_551_615n; + } +}