add duckdb-ui-client & other ts pkgs (#10)
* add duckdb-ui-client & other ts pkgs * workflow fixes * fix working dir * no sparse checkout; specify package.json path * path to pnpm-lock.yaml * add check & build test * workflow step descriptions * use comments & names * one more naming tweak
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
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 NEGATIVE_INFINITY_TIMESTAMP = BigInt('-9223372036854775807'); // -(2^63-1)
|
||||
const POSITIVE_INFINITY_TIMESTAMP = BigInt('9223372036854775807'); // 2^63-1
|
||||
|
||||
export function getDuckDBDateStringFromYearMonthDay(
|
||||
year: number,
|
||||
month: number,
|
||||
dayOfMonth: number,
|
||||
): string {
|
||||
const yearStr = String(Math.abs(year)).padStart(4, '0');
|
||||
const monthStr = String(month).padStart(2, '0');
|
||||
const dayOfMonthStr = String(dayOfMonth).padStart(2, '0');
|
||||
return `${yearStr}-${monthStr}-${dayOfMonthStr}${year < 0 ? ' (BC)' : ''}`;
|
||||
}
|
||||
|
||||
export function getDuckDBDateStringFromDays(days: number): string {
|
||||
const absDays = Math.abs(days);
|
||||
const sign = days < 0 ? -1 : 1;
|
||||
// 400 years is the shortest interval with a fixed number of days. (Leap years and different length months can result
|
||||
// in shorter intervals having different number of days.) By separating the number of 400 year intervals from the
|
||||
// interval covered by the remaining days, we can guarantee that the date resulting from shifting the epoch by the
|
||||
// remaining interval is within the valid range of the JS Date object. This allows us to use JS Date to calculate the
|
||||
// year, month, and day of month for the date represented by the remaining interval, thus accounting for leap years
|
||||
// and different length months. We can then safely add back the years from the 400 year intervals, because the month
|
||||
// and day of month won't change when a date is shifted by a whole number of such intervals.
|
||||
const num400YearIntervals = Math.floor(absDays / DAYS_IN_400_YEARS);
|
||||
const yearsFrom400YearIntervals = sign * num400YearIntervals * 400;
|
||||
const absDaysFromRemainingInterval = absDays % DAYS_IN_400_YEARS;
|
||||
const millisecondsFromRemainingInterval =
|
||||
sign * absDaysFromRemainingInterval * MILLISECONDS_PER_DAY_NUM;
|
||||
const date = new Date(millisecondsFromRemainingInterval);
|
||||
let year = yearsFrom400YearIntervals + date.getUTCFullYear();
|
||||
if (year < 0) {
|
||||
year--; // correct for non-existence of year zero
|
||||
}
|
||||
const month = date.getUTCMonth() + 1; // getUTCMonth returns zero-indexed month, but we want a one-index month for display
|
||||
const dayOfMonth = date.getUTCDate(); // getUTCDate returns one-indexed day-of-month
|
||||
return getDuckDBDateStringFromYearMonthDay(year, month, dayOfMonth);
|
||||
}
|
||||
|
||||
export function getTimezoneOffsetString(
|
||||
timezoneOffsetInMinutes?: number,
|
||||
): string | undefined {
|
||||
if (timezoneOffsetInMinutes === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const negative = timezoneOffsetInMinutes < 0;
|
||||
const positiveMinutes = Math.abs(timezoneOffsetInMinutes);
|
||||
const minutesPart = positiveMinutes % 60;
|
||||
const hoursPart = Math.floor(positiveMinutes / 60);
|
||||
const minutesStr =
|
||||
minutesPart !== 0 ? String(minutesPart).padStart(2, '0') : '';
|
||||
const hoursStr = String(hoursPart).padStart(2, '0');
|
||||
return `${negative ? '-' : '+'}${hoursStr}${minutesStr ? `:${minutesStr}` : ''}`;
|
||||
}
|
||||
|
||||
export function getAbsoluteOffsetStringFromParts(
|
||||
hoursPart: number,
|
||||
minutesPart: number,
|
||||
secondsPart: number,
|
||||
): string {
|
||||
const hoursStr = String(hoursPart).padStart(2, '0');
|
||||
const minutesStr =
|
||||
minutesPart !== 0 || secondsPart !== 0
|
||||
? String(minutesPart).padStart(2, '0')
|
||||
: '';
|
||||
const secondsStr =
|
||||
secondsPart !== 0 ? String(secondsPart).padStart(2, '0') : '';
|
||||
let result = hoursStr;
|
||||
if (minutesStr) {
|
||||
result += `:${minutesStr}`;
|
||||
if (secondsStr) {
|
||||
result += `:${secondsStr}`;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getOffsetStringFromAbsoluteSeconds(
|
||||
absoluteOffsetInSeconds: number,
|
||||
): string {
|
||||
const secondsPart = absoluteOffsetInSeconds % 60;
|
||||
const minutes = Math.floor(absoluteOffsetInSeconds / 60);
|
||||
const minutesPart = minutes % 60;
|
||||
const hoursPart = Math.floor(minutes / 60);
|
||||
return getAbsoluteOffsetStringFromParts(hoursPart, minutesPart, secondsPart);
|
||||
}
|
||||
|
||||
export function getOffsetStringFromSeconds(offsetInSeconds: number): string {
|
||||
const negative = offsetInSeconds < 0;
|
||||
const absoluteOffsetInSeconds = negative ? -offsetInSeconds : offsetInSeconds;
|
||||
const absoluteString = getOffsetStringFromAbsoluteSeconds(
|
||||
absoluteOffsetInSeconds,
|
||||
);
|
||||
return `${negative ? '-' : '+'}${absoluteString}`;
|
||||
}
|
||||
|
||||
export function getDuckDBTimeStringFromParts(
|
||||
hoursPart: bigint,
|
||||
minutesPart: bigint,
|
||||
secondsPart: bigint,
|
||||
microsecondsPart: bigint,
|
||||
): string {
|
||||
const hoursStr = String(hoursPart).padStart(2, '0');
|
||||
const minutesStr = String(minutesPart).padStart(2, '0');
|
||||
const secondsStr = String(secondsPart).padStart(2, '0');
|
||||
const microsecondsStr = String(microsecondsPart)
|
||||
.padStart(6, '0')
|
||||
.replace(/0+$/, '');
|
||||
return `${hoursStr}:${minutesStr}:${secondsStr}${
|
||||
microsecondsStr.length > 0 ? `.${microsecondsStr}` : ''
|
||||
}`;
|
||||
}
|
||||
|
||||
export function getDuckDBTimeStringFromPositiveMicroseconds(
|
||||
positiveMicroseconds: bigint,
|
||||
): string {
|
||||
const microsecondsPart = positiveMicroseconds % MICROSECONDS_PER_SECOND;
|
||||
const seconds = positiveMicroseconds / MICROSECONDS_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 getDuckDBTimeStringFromParts(
|
||||
hoursPart,
|
||||
minutesPart,
|
||||
secondsPart,
|
||||
microsecondsPart,
|
||||
);
|
||||
}
|
||||
|
||||
export function getDuckDBTimeStringFromMicrosecondsInDay(
|
||||
microsecondsInDay: bigint,
|
||||
): string {
|
||||
const positiveMicroseconds =
|
||||
microsecondsInDay < 0
|
||||
? microsecondsInDay + MICROSECONDS_PER_DAY
|
||||
: microsecondsInDay;
|
||||
return getDuckDBTimeStringFromPositiveMicroseconds(positiveMicroseconds);
|
||||
}
|
||||
|
||||
export function getDuckDBTimeStringFromMicroseconds(
|
||||
microseconds: bigint,
|
||||
): string {
|
||||
const negative = microseconds < 0;
|
||||
const positiveMicroseconds = negative ? -microseconds : microseconds;
|
||||
const positiveString =
|
||||
getDuckDBTimeStringFromPositiveMicroseconds(positiveMicroseconds);
|
||||
return negative ? `-${positiveString}` : positiveString;
|
||||
}
|
||||
|
||||
export function getDuckDBTimestampStringFromDaysAndMicroseconds(
|
||||
days: bigint,
|
||||
microsecondsInDay: bigint,
|
||||
timezonePart?: string,
|
||||
): string {
|
||||
// This conversion of BigInt to Number is safe, because the largest absolute value that `days` can has is 106751991,
|
||||
// which fits without loss of precision in a JS Number. (106751991 = (2^63-1) / MICROSECONDS_PER_DAY)
|
||||
const dateStr = getDuckDBDateStringFromDays(Number(days));
|
||||
const timeStr = getDuckDBTimeStringFromMicrosecondsInDay(microsecondsInDay);
|
||||
return `${dateStr} ${timeStr}${timezonePart ?? ''}`;
|
||||
}
|
||||
|
||||
export function getDuckDBTimestampStringFromMicroseconds(
|
||||
microseconds: bigint,
|
||||
timezoneOffsetInMinutes?: number,
|
||||
): string {
|
||||
// Note that -infinity and infinity are only representable in TIMESTAMP (and TIMESTAMPTZ), not the other timestamp
|
||||
// variants. This is by-design and matches DuckDB.
|
||||
if (microseconds === NEGATIVE_INFINITY_TIMESTAMP) {
|
||||
return '-infinity';
|
||||
}
|
||||
if (microseconds === POSITIVE_INFINITY_TIMESTAMP) {
|
||||
return 'infinity';
|
||||
}
|
||||
const offsetMicroseconds =
|
||||
timezoneOffsetInMinutes !== undefined
|
||||
? microseconds +
|
||||
BigInt(timezoneOffsetInMinutes) *
|
||||
MICROSECONDS_PER_SECOND *
|
||||
SECONDS_PER_MINUTE
|
||||
: microseconds;
|
||||
let days = offsetMicroseconds / MICROSECONDS_PER_DAY;
|
||||
let microsecondsPart = offsetMicroseconds % MICROSECONDS_PER_DAY;
|
||||
if (microsecondsPart < 0) {
|
||||
days--;
|
||||
microsecondsPart += MICROSECONDS_PER_DAY;
|
||||
}
|
||||
return getDuckDBTimestampStringFromDaysAndMicroseconds(
|
||||
days,
|
||||
microsecondsPart,
|
||||
getTimezoneOffsetString(timezoneOffsetInMinutes),
|
||||
);
|
||||
}
|
||||
|
||||
export function getDuckDBTimestampStringFromSeconds(seconds: bigint): string {
|
||||
return getDuckDBTimestampStringFromMicroseconds(
|
||||
seconds * MICROSECONDS_PER_SECOND,
|
||||
);
|
||||
}
|
||||
|
||||
export function getDuckDBTimestampStringFromMilliseconds(
|
||||
milliseconds: bigint,
|
||||
): string {
|
||||
return getDuckDBTimestampStringFromMicroseconds(
|
||||
milliseconds * MICROSECONDS_PER_MILLISECOND,
|
||||
);
|
||||
}
|
||||
|
||||
export function getDuckDBTimestampStringFromNanoseconds(
|
||||
nanoseconds: bigint,
|
||||
): 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,
|
||||
);
|
||||
}
|
||||
|
||||
// Assumes baseUnit can be pluralized by adding an 's'.
|
||||
function numberAndUnit(value: number, baseUnit: string): string {
|
||||
return `${value} ${baseUnit}${value !== 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
export function getDuckDBIntervalString(
|
||||
months: number,
|
||||
days: number,
|
||||
microseconds: bigint,
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
if (months !== 0) {
|
||||
const sign = months < 0 ? -1 : 1;
|
||||
const absMonths = Math.abs(months);
|
||||
const absYears = Math.floor(absMonths / 12);
|
||||
const years = sign * absYears;
|
||||
const extraMonths = sign * (absMonths - absYears * 12);
|
||||
if (years !== 0) {
|
||||
parts.push(numberAndUnit(years, 'year'));
|
||||
}
|
||||
if (extraMonths !== 0) {
|
||||
parts.push(numberAndUnit(extraMonths, 'month'));
|
||||
}
|
||||
}
|
||||
if (days !== 0) {
|
||||
parts.push(numberAndUnit(days, 'day'));
|
||||
}
|
||||
if (microseconds !== BigInt(0)) {
|
||||
parts.push(getDuckDBTimeStringFromMicroseconds(microseconds));
|
||||
}
|
||||
if (parts.length > 0) {
|
||||
return parts.join(' ');
|
||||
}
|
||||
return '00:00:00';
|
||||
}
|
||||
Reference in New Issue
Block a user