@datastax/astra-db-ts
Version:
Data API TypeScript client
546 lines (545 loc) • 21 kB
JavaScript
// Copyright Datastax, Inc
// SPDX-License-Identifier: Apache-2.0
import { $CustomInspect } from '../../lib/constants.js';
import { $SerializeForCollection } from '../../documents/collections/ser-des/constants.js';
import { $DeserializeForTable, $SerializeForTable } from '../../documents/tables/ser-des/constants.js';
import { mkInvArgsError } from '../../documents/utils.js';
import { numDigits } from '../../lib/utils.js';
import { mkTypeUnsupportedForCollectionsError } from '../../lib/api/ser-des/utils.js';
export class DataAPIDuration {
[$SerializeForCollection]() {
throw mkTypeUnsupportedForCollectionsError('DataAPIDuration', '_duration', [
'Use another durations representation, such as a string, or an object containing the months, days, and nanoseconds',
]);
}
;
[$SerializeForTable](ctx) {
return ctx.done(durationToShortString(this));
}
;
static [$DeserializeForTable](value, ctx) {
return ctx.done(new DataAPIDuration(value, true));
}
static builder(base) {
return new DataAPIDurationBuilder(base);
}
constructor(i1, i2, i3) {
Object.defineProperty(this, "months", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "days", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "nanoseconds", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
switch (arguments.length) {
case 1:
case 2:
if (typeof i1 !== 'string') {
throw mkInvArgsError('DataAPIDuration', [['duration', 'string']], i1);
}
[this.months, this.days, this.nanoseconds] = parseDurationStr(i1, !!i2);
break;
case 3:
if (typeof i1 !== 'number' || typeof i2 !== 'number' || (typeof i3 !== 'number' && typeof i3 !== 'bigint')) {
throw mkInvArgsError('new DataAPIDuration', [['months', 'number'], ['days', 'number'], ['nanoseconds', 'number | bigint']], i1, i2, i3);
}
validateDuration(i1, i2, BigInt(i3));
[this.months, this.days, this.nanoseconds] = [i1, i2, BigInt(i3)];
break;
default: {
throw RangeError(`Invalid number of arguments; expected 1..=3, got ${arguments.length}`);
}
}
Object.defineProperty(this, $CustomInspect, {
value: () => `DataAPIDuration("${this.toString()}")`,
});
}
equals(other) {
return (other instanceof DataAPIDuration)
&& this.months === other.months
&& this.days === other.days
&& this.nanoseconds === other.nanoseconds;
}
hasDayPrecision() {
return this.nanoseconds === 0n;
}
hasMillisecondPrecision() {
return this.nanoseconds % DataAPIDuration.NS_PER_MS === 0n;
}
isNegative() {
return this.months < 0 || this.days < 0 || this.nanoseconds < 0n;
}
isZero() {
return this.months === 0 && this.days === 0 && this.nanoseconds === 0n;
}
plus(other) {
if (this.isNegative() !== other.isNegative()) {
return null;
}
return new DataAPIDuration(this.months + other.months, this.days + other.days, this.nanoseconds + other.nanoseconds);
}
negate() {
return new DataAPIDuration(-this.months, -this.days, -this.nanoseconds);
}
abs() {
return this.isNegative() ? this.negate() : this;
}
toYears() {
return ~~(this.months / 12);
}
toHours() {
return Number(this.nanoseconds / DataAPIDuration.NS_PER_HOUR);
}
toMinutes() {
return Number(this.nanoseconds / DataAPIDuration.NS_PER_MIN);
}
toSeconds() {
return Number(this.nanoseconds / DataAPIDuration.NS_PER_SEC);
}
toMillis() {
return Number(this.nanoseconds / DataAPIDuration.NS_PER_MS);
}
toMicros() {
return this.nanoseconds / DataAPIDuration.NS_PER_US;
}
toString() {
return durationToLongString(this);
}
}
Object.defineProperty(DataAPIDuration, "NS_PER_HOUR", {
enumerable: true,
configurable: true,
writable: true,
value: 3600000000000n
});
Object.defineProperty(DataAPIDuration, "NS_PER_MIN", {
enumerable: true,
configurable: true,
writable: true,
value: 60000000000n
});
Object.defineProperty(DataAPIDuration, "NS_PER_SEC", {
enumerable: true,
configurable: true,
writable: true,
value: 1000000000n
});
Object.defineProperty(DataAPIDuration, "NS_PER_MS", {
enumerable: true,
configurable: true,
writable: true,
value: 1000000n
});
Object.defineProperty(DataAPIDuration, "NS_PER_US", {
enumerable: true,
configurable: true,
writable: true,
value: 1000n
});
export const duration = Object.assign((...params) => (params[0] instanceof DataAPIDuration) ? params[0] : new DataAPIDuration(...params), { builder: DataAPIDuration.builder });
export class DataAPIDurationBuilder {
constructor(base, _validateOrder = false) {
Object.defineProperty(this, "_validateOrder", {
enumerable: true,
configurable: true,
writable: true,
value: _validateOrder
});
Object.defineProperty(this, "_months", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "_days", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "_nanoseconds", {
enumerable: true,
configurable: true,
writable: true,
value: 0n
});
Object.defineProperty(this, "_index", {
enumerable: true,
configurable: true,
writable: true,
value: -1
});
Object.defineProperty(this, "_negative", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
if (base) {
this._months = Math.abs(base.months);
this._days = Math.abs(base.days);
this._nanoseconds = base.nanoseconds < 0n ? -base.nanoseconds : base.nanoseconds;
this._negative = base.isNegative();
}
}
negate(negative = !this._negative) {
this._negative = negative;
return this;
}
addYears(years) {
this._validateIndex(0);
this._validateMonths(years, 12, 'years');
this._months += years * 12;
return this;
}
addMonths(months) {
this._validateIndex(1);
this._validateMonths(months, 1, 'months');
this._months += months;
return this;
}
addWeeks(weeks) {
this._validateIndex(2);
this._validateDays(weeks, 7, 'weeks');
this._days += weeks * 7;
return this;
}
addDays(days) {
this._validateIndex(3);
this._validateDays(days, 1, 'days');
this._days += days;
return this;
}
addHours(hours) {
this._validateIndex(4);
const big = this._validateNanos(hours, DataAPIDuration.NS_PER_HOUR, 'hours');
this._nanoseconds += big * DataAPIDuration.NS_PER_HOUR;
return this;
}
addMinutes(minutes) {
this._validateIndex(5);
const big = this._validateNanos(minutes, DataAPIDuration.NS_PER_MIN, 'minutes');
this._nanoseconds += big * DataAPIDuration.NS_PER_MIN;
return this;
}
addSeconds(seconds) {
this._validateIndex(6);
const big = this._validateNanos(seconds, DataAPIDuration.NS_PER_SEC, 'seconds');
this._nanoseconds += big * DataAPIDuration.NS_PER_SEC;
return this;
}
addMillis(milliseconds) {
this._validateIndex(7);
const big = this._validateNanos(milliseconds, DataAPIDuration.NS_PER_MS, 'milliseconds');
this._nanoseconds += big * DataAPIDuration.NS_PER_MS;
return this;
}
addMicros(microseconds) {
this._validateIndex(8);
const big = this._validateNanos(microseconds, DataAPIDuration.NS_PER_US, 'microseconds');
this._nanoseconds += big * DataAPIDuration.NS_PER_US;
return this;
}
addNanos(nanoseconds) {
this._validateIndex(9);
const big = this._validateNanos(nanoseconds, 1n, 'nanoseconds');
this._nanoseconds += big;
return this;
}
build() {
return (this._negative)
? new DataAPIDuration(-this._months, -this._days, -this._nanoseconds)
: new DataAPIDuration(this._months, this._days, this._nanoseconds);
}
clone() {
const clone = new DataAPIDurationBuilder(undefined, this._validateOrder);
clone._months = this._months;
clone._days = this._days;
clone._nanoseconds = this._nanoseconds;
clone._index = this._index;
clone._negative = this._negative;
return clone;
}
raw() {
return (this._negative)
? [-this._months, -this._days, -this._nanoseconds]
: [this._months, this._days, this._nanoseconds];
}
_validateMonths(units, monthsPerUnit, unit) {
if (!Number.isInteger(units)) {
throw new TypeError(`Invalid duration; ${unit} must be an integer; got: ${units}`);
}
const exceedsMax = units > (2147483647 - this._months) / monthsPerUnit;
const becomesNegative = units < 0 && units < -this._months / monthsPerUnit;
if (exceedsMax || becomesNegative) {
const actualValue = BigInt(this._months) + BigInt(units) * BigInt(monthsPerUnit);
throw new RangeError(`Invalid duration. The total number of months must be in range [0, 2147483647]; got: ${actualValue} (tried to add ${units} ${unit})`);
}
}
_validateDays(units, daysPerUnit, unit) {
if (!Number.isInteger(units)) {
throw new TypeError(`Invalid duration; ${unit} must be an integer; got: ${units}`);
}
const exceedsMax = units > (2147483647 - this._days) / daysPerUnit;
const becomesNegative = units < 0 && units < -this._days / daysPerUnit;
if (exceedsMax || becomesNegative) {
const actualValue = BigInt(this._days) + BigInt(units) * BigInt(daysPerUnit);
throw new RangeError(`Invalid duration. The total number of days must be in range [0, 2147483647]; got: ${actualValue} (tried to add ${units} ${unit})`);
}
}
_validateNanos(units, nanosPerUnit, unit) {
if (typeof units !== 'bigint' && !Number.isInteger(units)) {
throw new TypeError(`Invalid duration; ${unit} must be an integer/bigint; got: ${units}`);
}
const big = BigInt(units);
const exceedsMax = big > (9223372036854775807n - this._nanoseconds) / nanosPerUnit;
const becomesNegative = big < 0n && big < -this._nanoseconds / nanosPerUnit;
if (exceedsMax || becomesNegative) {
const actualValue = this._nanoseconds + big * nanosPerUnit;
throw new RangeError(`Invalid duration. The total number of nanoseconds must be in range [0, 9223372036854775807]; got: ${actualValue} (tried to add ${units} ${unit})`);
}
return big;
}
_validateIndex(index) {
if (!this._validateOrder) {
return;
}
if (this._index === index) {
throw new SyntaxError(`Invalid duration; ${BuilderAddNamesLUT[index]} may not be set multiple times`);
}
if (this._index > index) {
throw new SyntaxError(`Invalid duration; ${BuilderAddNamesLUT[index]} must be set before ${BuilderAddNamesLUT[this._index]}`);
}
this._index = index;
}
}
const BuilderAddNamesLUT = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds'];
const parseDurationStr = (str, fromDataAPI) => {
const isNegative = str.startsWith('-');
const durationStr = isNegative ? str.slice(1) : str;
if (!durationStr) {
throw new SyntaxError('Invalid duration; empty string (or just a sign) is not allowed. To pass a zero-duration, use something like 0s, PT0S, or some other zero-unit.');
}
if (fromDataAPI) {
return parseDataAPIDuration(durationStr, isNegative);
}
const builder = new DataAPIDurationBuilder(undefined, true).negate(isNegative);
if (durationStr.startsWith('P')) {
if (durationStr.at(-1) === 'W') {
return parseISOWeekDuration(durationStr, builder);
}
if (durationStr.includes('-')) {
return parseISOAlternateDuration(durationStr, builder);
}
return parseISOStandardDuration(durationStr, builder);
}
return parseBasicDuration(durationStr, builder);
};
const DataAPIDurationMethodsLUT1 = {
'Y': (d, ys) => d[0] += ys * 12,
'M': (d, ms) => d[0] += ms,
'W': (d, ds) => d[1] += ds * 7,
'D': (d, ds) => d[1] += ds,
};
const DataAPIDurationMethodsLUT2 = {
'H': (d, hs) => d[2] += BigInt(hs) * DataAPIDuration.NS_PER_HOUR,
'M': (d, ms) => d[2] += BigInt(ms) * DataAPIDuration.NS_PER_MIN,
'.': (d, s) => d[2] += BigInt(s) * DataAPIDuration.NS_PER_SEC,
'S': (d, s) => d[2] += BigInt(s) * DataAPIDuration.NS_PER_SEC,
};
const parseDataAPIDuration = (str, negative) => {
const duration = [0, 0, 0n];
let lut = DataAPIDurationMethodsLUT1;
let index = 1;
while (index < str.length) {
if (str[index] === 'T') {
lut = DataAPIDurationMethodsLUT2;
index++;
}
const num = parseInt(str.slice(index), 10);
index += numDigits(num);
const unit = str[index++];
lut[unit](duration, num);
if (unit === '.') {
parseDataAPIDurationNanos(str.slice(index), duration);
break;
}
}
if (negative) {
duration[0] = -duration[0];
duration[1] = -duration[1];
duration[2] = -duration[2];
}
return duration;
};
const parseDataAPIDurationNanos = (str, duration) => {
duration[2] += BigInt(parseInt(str, 10) * Math.pow(10, 10 - str.length));
};
const BasicDurationRegex = /(\d+)(y|mo|w|d|h|s|ms|us|µs|ns|m)/gyi;
const parseBasicDuration = (str, builder) => {
let match;
BasicDurationRegex.lastIndex = 0;
while ((match = BasicDurationRegex.exec(str))) {
const num = parseInt(match[1], 10);
const unit = match[2];
switch (unit.toLowerCase()) {
case 'y':
builder.addYears(num);
break;
case 'mo':
builder.addMonths(num);
break;
case 'w':
builder.addWeeks(num);
break;
case 'd':
builder.addDays(num);
break;
case 'h':
builder.addHours(num);
break;
case 'm':
builder.addMinutes(num);
break;
case 's':
builder.addSeconds(num);
break;
case 'ms':
builder.addMillis(num);
break;
case 'us':
case 'µs':
builder.addMicros(num);
break;
case 'ns':
builder.addNanos(num);
break;
}
if (BasicDurationRegex.lastIndex === str.length) {
return builder.raw();
}
}
throw mkSyntaxErr('standard', str);
};
const ISOStandardDurationRegex = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)(?:\.(\d+))?S)?)?$/;
const parseISOStandardDuration = (str, builder) => {
const match = ISOStandardDurationRegex.exec(str);
if (!match) {
throw mkSyntaxErr('ISO-8601 standard', str);
}
if (match[1])
builder.addYears(parseInt(match[1], 10));
if (match[2])
builder.addMonths(parseInt(match[2], 10));
if (match[3])
builder.addDays(parseInt(match[3], 10));
if (match[4])
builder.addHours(parseInt(match[4], 10));
if (match[5])
builder.addMinutes(parseInt(match[5], 10));
if (match[6])
builder.addSeconds(parseInt(match[6], 10));
if (match[7])
builder.addNanos(parseInt(match[7], 10) * Math.pow(10, 9 - match[7].length));
return builder.raw();
};
const ISOWeekDurationRegex = /^P(\d+)W$/;
const parseISOWeekDuration = (str, builder) => {
const match = ISOWeekDurationRegex.exec(str);
if (!match) {
throw mkSyntaxErr('ISO-8601 week', str);
}
return builder
.addWeeks(parseInt(match[1], 10))
.raw();
};
const ISOAlternateDurationRegex = /^P(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})$/;
const parseISOAlternateDuration = (str, builder) => {
const match = ISOAlternateDurationRegex.exec(str);
if (!match) {
throw mkSyntaxErr('ISO-8601 alternate', str);
}
return builder
.addYears(parseInt(match[1], 10))
.addMonths(parseInt(match[2], 10))
.addDays(parseInt(match[3], 10))
.addHours(parseInt(match[4], 10))
.addMinutes(parseInt(match[5], 10))
.addSeconds(parseInt(match[6], 10))
.raw();
};
const validateDuration = (months, days, nanoseconds) => {
const allPositive = months >= 0 && days >= 0 && nanoseconds >= 0n;
const allNegative = months <= 0 && days <= 0 && nanoseconds <= 0n;
if (!(allPositive || allNegative)) {
throw new RangeError(`Invalid duration (${months}, ${days}, ${nanoseconds}); all parts (months, days, nanoseconds) must have the same sign`);
}
if (!Number.isInteger(months) || !Number.isInteger(days)) {
throw new TypeError(`Invalid duration (${months}, ${days}, ${nanoseconds}); all parts (months, days, nanoseconds) must be integer`);
}
if (months > 2147483647 || days > 2147483647 || nanoseconds > 9223372036854775807n) {
throw new RangeError(`Invalid duration (${months}, ${days}, ${nanoseconds}); months and days must be in range [-2147483647, 2147483647], nanoseconds must be in range [-9223372036854775807, 9223372036854775807]`);
}
};
const durationToShortString = (duration) => {
let res = duration.isNegative() ? '-' : '';
if (duration.months) {
res += Math.abs(duration.months) + 'mo';
}
if (duration.days) {
res += Math.abs(duration.days) + 'd';
}
if (duration.nanoseconds) {
res += (duration.nanoseconds < 0n ? -duration.nanoseconds : duration.nanoseconds) + 'ns';
}
return res || '0s';
};
const durationToLongString = (duration) => {
const res = { ref: duration.isNegative() ? '-' : '' };
if (duration.months) {
let remainingMonths = Math.abs(duration.months);
remainingMonths = appendNumberUnit(res, remainingMonths, 12, 'y');
appendNumberUnit(res, remainingMonths, 1, 'mo');
}
if (duration.days) {
appendNumberUnit(res, Math.abs(duration.days), 1, 'd');
}
if (duration.nanoseconds) {
let remainingNanos = duration.nanoseconds < 0 ? -duration.nanoseconds : duration.nanoseconds;
remainingNanos = appendBigIntUnit(res, remainingNanos, DataAPIDuration.NS_PER_HOUR, 'h');
remainingNanos = appendBigIntUnit(res, remainingNanos, DataAPIDuration.NS_PER_MIN, 'm');
remainingNanos = appendBigIntUnit(res, remainingNanos, DataAPIDuration.NS_PER_SEC, 's');
remainingNanos = appendBigIntUnit(res, remainingNanos, DataAPIDuration.NS_PER_MS, 'ms');
remainingNanos = appendBigIntUnit(res, remainingNanos, DataAPIDuration.NS_PER_US, 'us');
appendBigIntUnit(res, remainingNanos, 1n, 'ns');
}
return res.ref || '0s';
};
const appendNumberUnit = (result, value, unitSize, unitLabel) => {
if (value >= unitSize) {
result.ref += Math.floor(value / unitSize) + unitLabel;
return value % unitSize;
}
return value;
};
const appendBigIntUnit = (result, value, unitSize, unitLabel) => {
if (value >= unitSize) {
result.ref += (value / unitSize).toString() + unitLabel;
return value % unitSize;
}
return value;
};
const mkSyntaxErr = (fmtAttempted, str) => {
return new SyntaxError(`Invalid duration string: '${str}'. Attempted to parse as ${fmtAttempted} duration format, but failed. Please provide a valid duration string (see DataAPIDuration documentation for format info).`);
};