Skip to content

Commit

Permalink
rest of toString tests and some fixes to datetime conversions
Browse files Browse the repository at this point in the history
  • Loading branch information
jraymakers committed Nov 1, 2024
1 parent 6cfcf94 commit 47ee8de
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 16 deletions.
92 changes: 77 additions & 15 deletions api/src/conversion/dateTimeStringConversion.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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) {
Expand Down
137 changes: 136 additions & 1 deletion api/test/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,12 @@ import {
listValue,
mapValue,
structValue,
timeTZValue,
timeValue,
timestampTZValue,
timestampValue,
unionValue,
uuidValue,
version
} from '../src';

Expand Down Expand Up @@ -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, [
Expand Down Expand Up @@ -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');
});
});

0 comments on commit 47ee8de

Please sign in to comment.