From 47ee8dea5461401c81dc3e3cda098795b1d1f797 Mon Sep 17 00:00:00 2001 From: Jeff Raymakers Date: Thu, 31 Oct 2024 21:47:11 -0700 Subject: [PATCH] rest of toString tests and some fixes to datetime conversions --- .../conversion/dateTimeStringConversion.ts | 92 ++++++++++-- api/test/api.test.ts | 137 +++++++++++++++++- 2 files changed, 213 insertions(+), 16 deletions(-) diff --git a/api/src/conversion/dateTimeStringConversion.ts b/api/src/conversion/dateTimeStringConversion.ts index 00291a1..fdab733 100644 --- a/api/src/conversion/dateTimeStringConversion.ts +++ b/api/src/conversion/dateTimeStringConversion.ts @@ -1,15 +1,16 @@ const DAYS_IN_400_YEARS = 146097; // (((365 * 4 + 1) * 25) - 1) * 4 + 1 const MILLISECONDS_PER_DAY_NUM = 86400000; // 1000 * 60 * 60 * 24 -const MICROSECONDS_PER_SECOND = BigInt(1000000); -const MICROSECONDS_PER_MILLISECOND = BigInt(1000); -const NANOSECONDS_PER_MICROSECOND = BigInt(1000); -const SECONDS_PER_MINUTE = BigInt(60); -const MINUTES_PER_HOUR = BigInt(60); -const MICROSECONDS_PER_DAY = BigInt(86400000000); // 24 * 60 * 60 * 1000000 +const MICROSECONDS_PER_SECOND = 1000000n; +const MICROSECONDS_PER_MILLISECOND = 1000n; +const NANOSECONDS_PER_SECOND = 1000000000n +const SECONDS_PER_MINUTE = 60n; +const MINUTES_PER_HOUR = 60n; +const MICROSECONDS_PER_DAY = 86400000000n; // 24 * 60 * 60 * 1000000 +const NANOSECONDS_PER_DAY = 86400000000000n; // 24 * 60 * 60 * 1000000000 -const NEGATIVE_INFINITY_TIMESTAMP = BigInt('-9223372036854775807'); // -(2^63-1) -const POSITIVE_INFINITY_TIMESTAMP = BigInt('9223372036854775807'); // 2^63-1 +const NEGATIVE_INFINITY_TIMESTAMP = -9223372036854775807n; // -(2^63-1) +const POSITIVE_INFINITY_TIMESTAMP = 9223372036854775807n; // 2^63-1 export function getDuckDBDateStringFromYearMonthDay( year: number, @@ -64,6 +65,23 @@ export function getDuckDBTimeStringFromParts( }`; } +export function getDuckDBTimeStringFromPartsNS( + hoursPart: bigint, + minutesPart: bigint, + secondsPart: bigint, + nanosecondsPart: bigint, +): string { + const hoursStr = String(hoursPart).padStart(2, '0'); + const minutesStr = String(minutesPart).padStart(2, '0'); + const secondsStr = String(secondsPart).padStart(2, '0'); + const nanosecondsStr = String(nanosecondsPart) + .padStart(9, '0') + .replace(/0+$/, ''); + return `${hoursStr}:${minutesStr}:${secondsStr}${ + nanosecondsStr.length > 0 ? `.${nanosecondsStr}` : '' + }`; +} + export function getDuckDBTimeStringFromPositiveMicroseconds( positiveMicroseconds: bigint, ): string { @@ -81,6 +99,23 @@ export function getDuckDBTimeStringFromPositiveMicroseconds( ); } +export function getDuckDBTimeStringFromPositiveNanoseconds( + positiveNanoseconds: bigint, +): string { + const nanosecondsPart = positiveNanoseconds % NANOSECONDS_PER_SECOND; + const seconds = positiveNanoseconds / NANOSECONDS_PER_SECOND; + const secondsPart = seconds % SECONDS_PER_MINUTE; + const minutes = seconds / SECONDS_PER_MINUTE; + const minutesPart = minutes % MINUTES_PER_HOUR; + const hoursPart = minutes / MINUTES_PER_HOUR; + return getDuckDBTimeStringFromPartsNS( + hoursPart, + minutesPart, + secondsPart, + nanosecondsPart, + ); +} + export function getDuckDBTimeStringFromMicrosecondsInDay( microsecondsInDay: bigint, ): string { @@ -91,6 +126,16 @@ export function getDuckDBTimeStringFromMicrosecondsInDay( return getDuckDBTimeStringFromPositiveMicroseconds(positiveMicroseconds); } +export function getDuckDBTimeStringFromNanosecondsInDay( + nanosecondsInDay: bigint, +): string { + const positiveNanoseconds = + nanosecondsInDay < 0 + ? nanosecondsInDay + NANOSECONDS_PER_DAY + : nanosecondsInDay; + return getDuckDBTimeStringFromPositiveNanoseconds(positiveNanoseconds); +} + export function getDuckDBTimeStringFromMicroseconds( microseconds: bigint, ): string { @@ -114,6 +159,19 @@ export function getDuckDBTimestampStringFromDaysAndMicroseconds( return `${dateStr} ${timeStr}${timezoneStr}`; } +export function getDuckDBTimestampStringFromDaysAndNanoseconds( + days: bigint, + nanosecondsInDay: bigint, + timezone?: string | null, +): string { + // This conversion of BigInt to Number is safe, because the largest absolute value that `days` can has is 106751 + // which fits without loss of precision in a JS Number. (106751 = (2^63-1) / NANOSECONDS_PER_DAY) + const dateStr = getDuckDBDateStringFromDays(Number(days)); + const timeStr = getDuckDBTimeStringFromNanosecondsInDay(nanosecondsInDay); + const timezoneStr = timezone ? ` ${timezone}` : ''; + return `${dateStr} ${timeStr}${timezoneStr}`; +} + export function getDuckDBTimestampStringFromMicroseconds( microseconds: bigint, timezone?: string | null, @@ -163,18 +221,22 @@ export function getDuckDBTimestampStringFromNanoseconds( nanoseconds: bigint, timezone?: string | null, ): string { - // Note that this division causes loss of precision. This matches the behavior of the DuckDB. It's important that this - // precision loss happen before the negative correction in getTimestampStringFromMicroseconds, otherwise off-by-one - // errors can occur. - return getDuckDBTimestampStringFromMicroseconds( - nanoseconds / NANOSECONDS_PER_MICROSECOND, + let days = nanoseconds / NANOSECONDS_PER_DAY; + let nanosecondsPart = nanoseconds % NANOSECONDS_PER_DAY; + if (nanosecondsPart < 0) { + days--; + nanosecondsPart += NANOSECONDS_PER_DAY; + } + return getDuckDBTimestampStringFromDaysAndNanoseconds( + days, + nanosecondsPart, timezone, ); } // Assumes baseUnit can be pluralized by adding an 's'. function numberAndUnit(value: number, baseUnit: string): string { - return `${value} ${baseUnit}${value !== 1 ? 's' : ''}`; + return `${value} ${baseUnit}${Math.abs(value) !== 1 ? 's' : ''}`; } export function getDuckDBIntervalString( @@ -199,7 +261,7 @@ export function getDuckDBIntervalString( if (days !== 0) { parts.push(numberAndUnit(days, 'day')); } - if (microseconds !== BigInt(0)) { + if (microseconds !== 0n) { parts.push(getDuckDBTimeStringFromMicroseconds(microseconds)); } if (parts.length > 0) { diff --git a/api/test/api.test.ts b/api/test/api.test.ts index 0684efd..970bf99 100644 --- a/api/test/api.test.ts +++ b/api/test/api.test.ts @@ -106,9 +106,12 @@ import { listValue, mapValue, structValue, + timeTZValue, + timeValue, timestampTZValue, timestampValue, unionValue, + uuidValue, version } from '../src'; @@ -593,7 +596,7 @@ describe('api', () => { assertValues(chunk, 21, DuckDBDoubleVector, [DuckDBDoubleType.Min, DuckDBDoubleType.Max, null]); assertValues(chunk, 22, DuckDBDecimal2Vector, [ decimalNumber(4, 1, -9999), - decimalNumber(4, 1, 9999 as number), + decimalNumber(4, 1, 9999), null, ]); assertValues(chunk, 23, DuckDBDecimal4Vector, [ @@ -844,20 +847,152 @@ describe('api', () => { }); }); test('values toString', () => { + // array assert.equal(arrayValue([]).toString(), '[]'); assert.equal(arrayValue([1, 2, 3]).toString(), '[1, 2, 3]'); assert.equal(arrayValue(['a', 'b', 'c']).toString(), `['a', 'b', 'c']`); + // bit assert.equal(bitValue('').toString(), ''); assert.equal(bitValue('10101').toString(), '10101'); assert.equal(bitValue('0010001001011100010101011010111').toString(), '0010001001011100010101011010111'); + // blob assert.equal(DuckDBBlobValue.fromString('').toString(), ''); assert.equal(DuckDBBlobValue.fromString('thisisalongblob\x00withnullbytes').toString(), 'thisisalongblob\\x00withnullbytes'); assert.equal(DuckDBBlobValue.fromString('\x00\x00\x00a').toString(), '\\x00\\x00\\x00a'); + // date assert.equal(DuckDBDateValue.Epoch.toString(), '1970-01-01'); assert.equal(DuckDBDateValue.Max.toString(), '5881580-07-10'); assert.equal(DuckDBDateValue.Min.toString(), '5877642-06-25 (BC)'); + + // decimal + assert.equal(decimalNumber(4, 1, 0).toString(), '0.0'); + assert.equal(decimalNumber(4, 1, 9876).toString(), '987.6'); + assert.equal(decimalNumber(4, 1, -9876).toString(), '-987.6'); + + assert.equal(decimalNumber(9, 4, 0).toString(), '0.0000'); + assert.equal(decimalNumber(9, 4, 987654321).toString(), '98765.4321'); + assert.equal(decimalNumber(9, 4, -987654321).toString(), '-98765.4321'); + + assert.equal(decimalNumber(18, 6, 0).toString(), '0.000000'); + assert.equal(decimalBigint(18, 6, 987654321098765432n).toString(), '987654321098.765432'); + assert.equal(decimalBigint(18, 6, -987654321098765432n).toString(), '-987654321098.765432'); + + assert.equal(decimalNumber(38, 10, 0).toString(), '0.0000000000'); + assert.equal(decimalBigint(38, 10, 98765432109876543210987654321098765432n).toString(), '9876543210987654321098765432.1098765432'); + assert.equal(decimalBigint(38, 10, -98765432109876543210987654321098765432n).toString(), '-9876543210987654321098765432.1098765432'); + + // interval + assert.equal(intervalValue(0, 0, 0n).toString(), '00:00:00'); + + assert.equal(intervalValue( 1, 0, 0n).toString(), '1 month'); + assert.equal(intervalValue(-1, 0, 0n).toString(), '-1 month'); + assert.equal(intervalValue( 2, 0, 0n).toString(), '2 months'); + assert.equal(intervalValue(-2, 0, 0n).toString(), '-2 months'); + assert.equal(intervalValue( 12, 0, 0n).toString(), '1 year'); + assert.equal(intervalValue(-12, 0, 0n).toString(), '-1 year'); + assert.equal(intervalValue( 24, 0, 0n).toString(), '2 years'); + assert.equal(intervalValue(-24, 0, 0n).toString(), '-2 years'); + assert.equal(intervalValue( 25, 0, 0n).toString(), '2 years 1 month'); + assert.equal(intervalValue(-25, 0, 0n).toString(), '-2 years -1 month'); + + assert.equal(intervalValue(0, 1, 0n).toString(), '1 day'); + assert.equal(intervalValue(0, -1, 0n).toString(), '-1 day'); + assert.equal(intervalValue(0, 2, 0n).toString(), '2 days'); + assert.equal(intervalValue(0, -2, 0n).toString(), '-2 days'); + assert.equal(intervalValue(0, 30, 0n).toString(), '30 days'); + assert.equal(intervalValue(0, 365, 0n).toString(), '365 days'); + + assert.equal(intervalValue(0, 0, 1n).toString(), '00:00:00.000001'); + assert.equal(intervalValue(0, 0, -1n).toString(), '-00:00:00.000001'); + assert.equal(intervalValue(0, 0, 987654n).toString(), '00:00:00.987654'); + assert.equal(intervalValue(0, 0, -987654n).toString(), '-00:00:00.987654'); + assert.equal(intervalValue(0, 0, 1000000n).toString(), '00:00:01'); + assert.equal(intervalValue(0, 0, -1000000n).toString(), '-00:00:01'); + assert.equal(intervalValue(0, 0, 59n * 1000000n).toString(), '00:00:59'); + assert.equal(intervalValue(0, 0, -59n * 1000000n).toString(), '-00:00:59'); + assert.equal(intervalValue(0, 0, 60n * 1000000n).toString(), '00:01:00'); + assert.equal(intervalValue(0, 0, -60n * 1000000n).toString(), '-00:01:00'); + assert.equal(intervalValue(0, 0, 59n * 60n * 1000000n).toString(), '00:59:00'); + assert.equal(intervalValue(0, 0, -59n * 60n * 1000000n).toString(), '-00:59:00'); + assert.equal(intervalValue(0, 0, 60n * 60n * 1000000n).toString(), '01:00:00'); + assert.equal(intervalValue(0, 0, -60n * 60n * 1000000n).toString(), '-01:00:00'); + assert.equal(intervalValue(0, 0, 24n * 60n * 60n * 1000000n).toString(), '24:00:00'); + assert.equal(intervalValue(0, 0, -24n * 60n * 60n * 1000000n).toString(), '-24:00:00'); + assert.equal(intervalValue(0, 0, 2147483647n * 60n * 60n * 1000000n).toString(), '2147483647:00:00'); + assert.equal(intervalValue(0, 0, -2147483647n * 60n * 60n * 1000000n).toString(), '-2147483647:00:00'); + assert.equal(intervalValue(0, 0, 2147483647n * 60n * 60n * 1000000n + 1n ).toString(), '2147483647:00:00.000001'); + assert.equal(intervalValue(0, 0, -(2147483647n * 60n * 60n * 1000000n + 1n)).toString(), '-2147483647:00:00.000001'); + + assert.equal(intervalValue(2 * 12 + 3, 5, (7n * 60n * 60n + 11n * 60n + 13n) * 1000000n + 17n).toString(), + '2 years 3 months 5 days 07:11:13.000017'); + assert.equal(intervalValue(-(2 * 12 + 3), -5, -((7n * 60n * 60n + 11n * 60n + 13n) * 1000000n + 17n)).toString(), + '-2 years -3 months -5 days -07:11:13.000017'); + + // list + assert.equal(listValue([]).toString(), '[]'); + assert.equal(listValue([1, 2, 3]).toString(), '[1, 2, 3]'); + assert.equal(listValue(['a', 'b', 'c']).toString(), `['a', 'b', 'c']`); + + // map + assert.equal(mapValue([]).toString(), '{}'); + assert.equal(mapValue([{ key: 1, value: 'a' }, { key: 2, value: 'b' }]).toString(), `{1: 'a', 2: 'b'}`); + + // struct + assert.equal(structValue({}).toString(), '{}'); + assert.equal(structValue({a: 1, b: 2}).toString(), `{'a': 1, 'b': 2}`); + + // timestamp milliseconds + assert.equal(DuckDBTimestampMillisecondsValue.Epoch.toString(), '1970-01-01 00:00:00'); + assert.equal(DuckDBTimestampMillisecondsValue.Max.toString(), '294247-01-10 04:00:54.775'); + assert.equal(DuckDBTimestampMillisecondsValue.Min.toString(), '290309-12-22 (BC) 00:00:00'); + + // timestamp nanoseconds + assert.equal(DuckDBTimestampNanosecondsValue.Epoch.toString(), '1970-01-01 00:00:00'); + assert.equal(DuckDBTimestampNanosecondsValue.Max.toString(), '2262-04-11 23:47:16.854775806'); + assert.equal(DuckDBTimestampNanosecondsValue.Min.toString(), '1677-09-22 00:00:00'); + + // timestamp seconds + assert.equal(DuckDBTimestampSecondsValue.Epoch.toString(), '1970-01-01 00:00:00'); + assert.equal(DuckDBTimestampSecondsValue.Max.toString(), '294247-01-10 04:00:54'); + assert.equal(DuckDBTimestampSecondsValue.Min.toString(), '290309-12-22 (BC) 00:00:00'); + + // timestamp tz + assert.equal(DuckDBTimestampTZValue.Epoch.toString(), '1970-01-01 00:00:00'); + // assert.equal(DuckDBTimestampTZValue.Max.toString(), '294247-01-09 20:00:54.775806-08'); // in PST + assert.equal(DuckDBTimestampTZValue.Max.toString(), '294247-01-10 04:00:54.775806'); // TODO TZ + // assert.equal(DuckDBTimestampTZValue.Min.toString(), '290309-12-21 (BC) 16:00:00-08'); // in PST + assert.equal(DuckDBTimestampTZValue.Min.toString(), '290309-12-22 (BC) 00:00:00'); // TODO TZ + assert.equal(DuckDBTimestampTZValue.PosInf.toString(), 'infinity'); + assert.equal(DuckDBTimestampTZValue.NegInf.toString(), '-infinity'); + + // timestamp + assert.equal(DuckDBTimestampValue.Epoch.toString(), '1970-01-01 00:00:00'); + assert.equal(DuckDBTimestampValue.Max.toString(), '294247-01-10 04:00:54.775806'); + assert.equal(DuckDBTimestampValue.Min.toString(), '290309-12-22 (BC) 00:00:00'); + assert.equal(DuckDBTimestampValue.PosInf.toString(), 'infinity'); + assert.equal(DuckDBTimestampValue.NegInf.toString(), '-infinity'); + + // time tz + assert.equal(timeTZValue(0, 0).toString(), '00:00:00'); + // assert.equal(DuckDBTimeTZValue.Max.toString(), '24:00:00-15:59:59'); + assert.equal(DuckDBTimeTZValue.Max.toString(), '24:00:00'); // TODO TZ + // assert.equal(DuckDBTimeTZValue.Max.toString(), '00:00:00+15:59:59'); + assert.equal(DuckDBTimeTZValue.Min.toString(), '00:00:00'); // TODO TZ + + // time + assert.equal(DuckDBTimeValue.Max.toString(), '24:00:00'); + assert.equal(DuckDBTimeValue.Min.toString(), '00:00:00'); + assert.equal(timeValue((12n * 60n * 60n + 34n * 60n + 56n) * 1000000n + 987654n).toString(), '12:34:56.987654'); + + // union + assert.equal(unionValue('a', 42).toString(), '42'); + assert.equal(unionValue('b', 'duck').toString(), 'duck'); + + // uuid + assert.equal(uuidValue(0n).toString(), '00000000-0000-0000-0000-000000000000'); + assert.equal(uuidValue(2n ** 128n - 1n).toString(), 'ffffffff-ffff-ffff-ffff-ffffffffffff'); }); });