UNPKG

@datastax/astra-db-ts

Version:
546 lines (545 loc) 21 kB
// 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).`); };