"use strict";
const Long = require("long");
const util = require("util");
const utils = require("../utils");
const rust = require("../../index");
const { bigintToLong, longToBigint } = require("../new-utils");
/** @module types */
// Reuse the same buffers that should perform slightly better than built-in buffer pool
const reusableBuffers = {
months: utils.allocBuffer(9),
days: utils.allocBuffer(9),
nanoseconds: utils.allocBuffer(9),
};
const maxInt32 = 0x7fffffff;
const longOneThousand = Long.fromInt(1000);
const nanosPerMicro = longOneThousand;
const nanosPerMilli = longOneThousand.multiply(nanosPerMicro);
const nanosPerSecond = longOneThousand.multiply(nanosPerMilli);
const nanosPerMinute = Long.fromInt(60).multiply(nanosPerSecond);
const nanosPerHour = Long.fromInt(60).multiply(nanosPerMinute);
const daysPerWeek = 7;
const monthsPerYear = 12;
const standardRegex = /(\d+)(y|mo|w|d|h|s|ms|us|µs|ns|m)/gi;
const iso8601Regex =
/P((\d+)Y)?((\d+)M)?((\d+)D)?(T((\d+)H)?((\d+)M)?((\d+)S)?)?/;
const iso8601WeekRegex = /P(\d+)W/;
const iso8601AlternateRegex =
/P(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/;
/**
* Represents a duration. A duration stores separately months, days, and seconds due to the fact that the number of
* days in a month varies, and a day can have 23 or 25 hours if a daylight saving is involved.
*/
class Duration {
/**
* @type {rust.DurationWrapper}
* @private
*/
#internal;
/**
* Creates a new instance of {@link Duration}.
* @param {Number} months The number of months.
* @param {Number} days The number of days.
* @param {Number|Long|BigInt} nanoseconds The number of nanoseconds.
* @constructor
*/
constructor(months, days, nanoseconds) {
if (typeof nanoseconds === "bigint") {
this.#internal = rust.DurationWrapper.new(
months,
days,
nanoseconds,
);
} else if (nanoseconds instanceof Long) {
this.#internal = rust.DurationWrapper.new(
months,
days,
longToBigint(nanoseconds),
);
} else if (typeof nanoseconds === "number") {
this.#internal = rust.DurationWrapper.new(
months,
days,
BigInt(nanoseconds),
);
} else {
throw new TypeError(
`Invalid nanosecond argument type: ${typeof nanoseconds}`,
);
}
}
equals(other) {
if (!(other instanceof Duration)) {
return false;
}
return (
this.months === other.months &&
this.days === other.days &&
this.nanoseconds.compare(other.nanoseconds) === 0
);
}
/**
* Get duration from rust object. Not intended to be exposed in the API
* @package
* @param {rust.DurationWrapper} arg
* @returns {Duration}
*/
static fromRust(arg) {
let res = new Duration(arg.months, arg.days, arg.getNanoseconds());
return res;
}
/**
* Gets the number of months
* @readonly
* @type {Number}
*/
get months() {
return this.#internal.months;
}
set months(_) {
throw new SyntaxError("Duration months is read-only");
}
/**
* Gets the number of days
* @readonly
* @type {Number}
*/
get days() {
return this.#internal.days;
}
set days(_) {
throw new SyntaxError("Duration days is read-only");
}
/**
* Gets the number of nanoseconds
* @readonly
* @type {Long}
*/
get nanoseconds() {
return bigintToLong(this.#internal.getNanoseconds());
}
set nanoseconds(_) {
throw new SyntaxError("Duration nanoseconds is read-only");
}
/**
* Serializes the duration and returns the representation of the value in bytes.
* @returns {Buffer}
*/
toBuffer() {
let nanoseconds = bigintToLong(this.#internal.getNanoseconds());
const lengthMonths = VIntCoding.writeVInt(
Long.fromNumber(this.#internal.months),
reusableBuffers.months,
);
const lengthDays = VIntCoding.writeVInt(
Long.fromNumber(this.#internal.days),
reusableBuffers.days,
);
const lengthNanoseconds = VIntCoding.writeVInt(
nanoseconds,
reusableBuffers.nanoseconds,
);
const buffer = utils.allocBufferUnsafe(
lengthMonths + lengthDays + lengthNanoseconds,
);
reusableBuffers.months.copy(buffer, 0, 0, lengthMonths);
let offset = lengthMonths;
reusableBuffers.days.copy(buffer, offset, 0, lengthDays);
offset += lengthDays;
reusableBuffers.nanoseconds.copy(buffer, offset, 0, lengthNanoseconds);
return buffer;
}
/**
* Returns the string representation of the value.
* @return {string}
*/
toString() {
let nanoseconds = bigintToLong(this.#internal.getNanoseconds());
let value = "";
function append(dividend, divisor, unit) {
if (dividend === 0 || dividend < divisor) {
return dividend;
}
// string concatenation is supposed to be faster than join()
value += (dividend / divisor).toFixed(0) + unit;
return dividend % divisor;
}
function append64(dividend, divisor, unit) {
if (dividend.equals(Long.ZERO) || dividend.lessThan(divisor)) {
return dividend;
}
// string concatenation is supposed to be faster than join()
value += dividend.divide(divisor).toString() + unit;
return dividend.modulo(divisor);
}
if (
this.#internal.months < 0 ||
this.#internal.days < 0 ||
nanoseconds.isNegative()
) {
value = "-";
}
let remainder = append(
Math.abs(this.#internal.months),
monthsPerYear,
"y",
);
append(remainder, 1, "mo");
append(Math.abs(this.#internal.days), 1, "d");
if (!nanoseconds.equals(Long.ZERO)) {
const nanos = nanoseconds.isNegative()
? nanoseconds.negate()
: nanoseconds;
remainder = append64(nanos, nanosPerHour, "h");
remainder = append64(remainder, nanosPerMinute, "m");
remainder = append64(remainder, nanosPerSecond, "s");
remainder = append64(remainder, nanosPerMilli, "ms");
remainder = append64(remainder, nanosPerMicro, "us");
append64(remainder, Long.ONE, "ns");
}
return value;
}
/**
* Creates a new {@link Duration} instance from the binary representation of the value.
* @param {Buffer} buffer
* @returns {Duration}
*/
static fromBuffer(buffer) {
const offset = { value: 0 };
const months = VIntCoding.readVInt(buffer, offset).toNumber();
const days = VIntCoding.readVInt(buffer, offset).toNumber();
const nanoseconds = VIntCoding.readVInt(buffer, offset);
return new Duration(months, days, nanoseconds);
}
/**
* Creates a new {@link Duration} instance from the string representation of the value.
* @param {String} input
*
* Accepted formats:
*
* - multiple digits followed by a time unit like: 12h30m where the time unit can be:
* - `y`: years
* - `mo`: months
* - `w`: weeks
* - `d`: days
* - `h`: hours
* - `m`: minutes
* - `s`: seconds
* - `ms`: milliseconds
* - `us` or `µs`: microseconds
* - `ns`: nanoseconds
* - ISO 8601 format: `P[n]Y[n]M[n]DT[n]H[n]M[n]S or P[n]W`
* - ISO 8601 alternative format: `P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]`
*
* Duration can be made negative by adding `-` at the beginning of the input
* @returns {Duration}
* @example <caption>From formatted string</caption>
* let date = fromString("4mo7d20ns"); // 1 month, 7 days, 20 nanoseconds
* @example <caption>From ISO 8601</caption>
* let date = fromString("P2DT5M"); // 2 days, 5 minutes
*/
static fromString(input) {
const isNegative = input.charAt(0) === "-";
const source = isNegative ? input.substring(1) : input;
if (source.charAt(0) === "P") {
if (source.charAt(source.length - 1) === "W") {
return parseIso8601WeekFormat(isNegative, source);
}
if (source.indexOf("-") > 0) {
return parseIso8601AlternativeFormat(isNegative, source);
}
return parseIso8601Format(isNegative, source);
}
return parseStandardFormat(isNegative, source);
}
/**
* @package
* @returns {rust.DurationWrapper}
*/
getInternal() {
return this.#internal;
}
}
/**
* @param {Boolean} isNegative
* @param {String} source
* @returns {Duration}
* @private
*/
function parseStandardFormat(isNegative, source) {
const builder = new Builder(isNegative);
standardRegex.lastIndex = 0;
let matches;
while ((matches = standardRegex.exec(source)) && matches.length <= 3) {
builder.add(matches[1], matches[2]);
}
return builder.build();
}
/**
* @param {Boolean} isNegative
* @param {String} source
* @returns {Duration}
* @private
*/
function parseIso8601Format(isNegative, source) {
const matches = iso8601Regex.exec(source);
if (!matches || matches[0] !== source) {
throw new TypeError(
util.format("Unable to convert '%s' to a duration", source),
);
}
const builder = new Builder(isNegative);
if (matches[1]) {
builder.addYears(matches[2]);
}
if (matches[3]) {
builder.addMonths(matches[4]);
}
if (matches[5]) {
builder.addDays(matches[6]);
}
if (matches[7]) {
if (matches[8]) {
builder.addHours(matches[9]);
}
if (matches[10]) {
builder.addMinutes(matches[11]);
}
if (matches[12]) {
builder.addSeconds(matches[13]);
}
}
return builder.build();
}
/**
* @param {Boolean} isNegative
* @param {String} source
* @returns {Duration}
* @private
*/
function parseIso8601WeekFormat(isNegative, source) {
const matches = iso8601WeekRegex.exec(source);
if (!matches || matches[0] !== source) {
throw new TypeError(
util.format("Unable to convert '%s' to a duration", source),
);
}
return new Builder(isNegative).addWeeks(matches[1]).build();
}
/**
* @param {Boolean} isNegative
* @param {String} source
* @returns {Duration}
* @private
*/
function parseIso8601AlternativeFormat(isNegative, source) {
const matches = iso8601AlternateRegex.exec(source);
if (!matches || matches[0] !== source) {
throw new TypeError(
util.format("Unable to convert '%s' to a duration", source),
);
}
return new Builder(isNegative)
.addYears(matches[1])
.addMonths(matches[2])
.addDays(matches[3])
.addHours(matches[4])
.addMinutes(matches[5])
.addSeconds(matches[6])
.build();
}
/**
* @param {Boolean} isNegative
* @private
* @constructor
*/
function Builder(isNegative) {
this._isNegative = isNegative;
this._unitIndex = 0;
this._months = 0;
this._days = 0;
this._nanoseconds = Long.ZERO;
this._addMethods = {
y: this.addYears,
mo: this.addMonths,
w: this.addWeeks,
d: this.addDays,
h: this.addHours,
m: this.addMinutes,
s: this.addSeconds,
ms: this.addMillis,
// µs
"\u00B5s": this.addMicros,
us: this.addMicros,
ns: this.addNanos,
};
this._unitByIndex = [
null,
"years",
"months",
"weeks",
"days",
"hours",
"minutes",
"seconds",
"milliseconds",
"microseconds",
"nanoseconds",
];
}
Builder.prototype._validateOrder = function (unitIndex) {
if (unitIndex === this._unitIndex) {
throw new TypeError(
util.format(
"Invalid duration. The %s are specified multiple times",
this._getUnitName(unitIndex),
),
);
}
if (unitIndex <= this._unitIndex) {
throw new TypeError(
util.format(
"Invalid duration. The %s should be after %s",
this._getUnitName(this._unitIndex),
this._getUnitName(unitIndex),
),
);
}
this._unitIndex = unitIndex;
};
/**
* @param {Number} units
* @param {Number} monthsPerUnit
*/
Builder.prototype._validateMonths = function (units, monthsPerUnit) {
this._validate32(
units,
(maxInt32 - this._months) / monthsPerUnit,
"months",
);
};
/**
* @param {Number} units
* @param {Number} daysPerUnit
*/
Builder.prototype._validateDays = function (units, daysPerUnit) {
this._validate32(units, (maxInt32 - this._days) / daysPerUnit, "days");
};
/**
* @param {Long} units
* @param {Long} nanosPerUnit
*/
Builder.prototype._validateNanos = function (units, nanosPerUnit) {
this._validate64(
units,
Long.MAX_VALUE.subtract(this._nanoseconds).divide(nanosPerUnit),
"nanoseconds",
);
};
/**
* @param {Number} units
* @param {Number} limit
* @param {String} unitName
*/
Builder.prototype._validate32 = function (units, limit, unitName) {
if (units > limit) {
throw new TypeError(
util.format(
"Invalid duration. The total number of %s must be less or equal to %s",
unitName,
maxInt32,
),
);
}
};
/**
* @param {Long} units
* @param {Long} limit
* @param {String} unitName
*/
Builder.prototype._validate64 = function (units, limit, unitName) {
if (units.greaterThan(limit)) {
throw new TypeError(
util.format(
"Invalid duration. The total number of %s must be less or equal to %s",
unitName,
Long.MAX_VALUE.toString(),
),
);
}
};
Builder.prototype._getUnitName = function (unitIndex) {
const name = this._unitByIndex[+unitIndex];
if (!name) {
throw new Error("unknown unit index: " + unitIndex);
}
return name;
};
Builder.prototype.add = function (textValue, symbol) {
const addMethod = this._addMethods[symbol.toLowerCase()];
if (!addMethod) {
throw new TypeError(
util.format("Unknown duration symbol '%s'", symbol),
);
}
return addMethod.call(this, textValue);
};
/**
* @param {String|Number} years
* @return {Builder}
*/
Builder.prototype.addYears = function (years) {
const value = +years;
this._validateOrder(1);
this._validateMonths(value, monthsPerYear);
this._months += value * monthsPerYear;
return this;
};
/**
* @param {String|Number} months
* @return {Builder}
*/
Builder.prototype.addMonths = function (months) {
const value = +months;
this._validateOrder(2);
this._validateMonths(value, 1);
this._months += value;
return this;
};
/**
* @param {String|Number} weeks
* @return {Builder}
*/
Builder.prototype.addWeeks = function (weeks) {
const value = +weeks;
this._validateOrder(3);
this._validateDays(value, daysPerWeek);
this._days += value * daysPerWeek;
return this;
};
/**
* @param {String|Number} days
* @return {Builder}
*/
Builder.prototype.addDays = function (days) {
const value = +days;
this._validateOrder(4);
this._validateDays(value, 1);
this._days += value;
return this;
};
/**
* @param {String|Long} hours
* @return {Builder}
*/
Builder.prototype.addHours = function (hours) {
const value = typeof hours === "string" ? Long.fromString(hours) : hours;
this._validateOrder(5);
this._validateNanos(value, nanosPerHour);
this._nanoseconds = this._nanoseconds.add(value.multiply(nanosPerHour));
return this;
};
/**
* @param {String|Long} minutes
* @return {Builder}
*/
Builder.prototype.addMinutes = function (minutes) {
const value =
typeof minutes === "string" ? Long.fromString(minutes) : minutes;
this._validateOrder(6);
this._validateNanos(value, nanosPerMinute);
this._nanoseconds = this._nanoseconds.add(value.multiply(nanosPerMinute));
return this;
};
/**
* @param {String|Long} seconds
* @return {Builder}
*/
Builder.prototype.addSeconds = function (seconds) {
const value =
typeof seconds === "string" ? Long.fromString(seconds) : seconds;
this._validateOrder(7);
this._validateNanos(value, nanosPerSecond);
this._nanoseconds = this._nanoseconds.add(value.multiply(nanosPerSecond));
return this;
};
/**
* @param {String|Long} millis
* @return {Builder}
*/
Builder.prototype.addMillis = function (millis) {
const value = typeof millis === "string" ? Long.fromString(millis) : millis;
this._validateOrder(8);
this._validateNanos(value, nanosPerMilli);
this._nanoseconds = this._nanoseconds.add(value.multiply(nanosPerMilli));
return this;
};
/**
* @param {String|Long} micros
* @return {Builder}
*/
Builder.prototype.addMicros = function (micros) {
const value = typeof micros === "string" ? Long.fromString(micros) : micros;
this._validateOrder(9);
this._validateNanos(value, nanosPerMicro);
this._nanoseconds = this._nanoseconds.add(value.multiply(nanosPerMicro));
return this;
};
/**
* @param {String|Long} nanos
* @return {Builder}
*/
Builder.prototype.addNanos = function (nanos) {
const value = typeof nanos === "string" ? Long.fromString(nanos) : nanos;
this._validateOrder(10);
this._validateNanos(value, Long.ONE);
this._nanoseconds = this._nanoseconds.add(value);
return this;
};
/** @return {Duration} */
Builder.prototype.build = function () {
return this._isNegative
? new Duration(-this._months, -this._days, this._nanoseconds.negate())
: new Duration(this._months, this._days, this._nanoseconds);
};
/**
* Contains the methods for reading and writing vints into binary format.
* Exposes only 2 internal methods, the rest are hidden.
* @private
*/
const VIntCoding = (function () {
/** @param {Long} n */
function encodeZigZag64(n) {
// (n << 1) ^ (n >> 63);
return n.toUnsigned().shiftLeft(1).xor(n.shiftRight(63));
}
/** @param {Long} n */
function decodeZigZag64(n) {
// (n >>> 1) ^ -(n & 1);
return n.shiftRightUnsigned(1).xor(n.and(Long.ONE).negate());
}
/**
* @param {Long} value
* @param {Buffer} buffer
* @returns {Number}
*/
function writeVInt(value, buffer) {
return writeUnsignedVInt(encodeZigZag64(value), buffer);
}
/**
* @param {Long} value
* @param {Buffer} buffer
* @returns {number}
*/
function writeUnsignedVInt(value, buffer) {
const size = computeUnsignedVIntSize(value);
if (size === 1) {
buffer[0] = value.getLowBits();
return 1;
}
encodeVInt(value, size, buffer);
return size;
}
/**
* @param {Long} value
* @returns {number}
*/
function computeUnsignedVIntSize(value) {
const magnitude = numberOfLeadingZeros(value.or(Long.ONE));
return (639 - magnitude * 9) >> 6;
}
/**
* @param {Long} value
* @param {Number} size
* @param {Buffer} buffer
*/
function encodeVInt(value, size, buffer) {
const extraBytes = size - 1;
let intValue = value.getLowBits();
let i;
let intBytes = 4;
for (i = extraBytes; i >= 0 && intBytes-- > 0; i--) {
buffer[i] = 0xff & intValue;
intValue >>= 8;
}
intValue = value.getHighBits();
for (; i >= 0; i--) {
buffer[i] = 0xff & intValue;
intValue >>= 8;
}
buffer[0] |= encodeExtraBytesToRead(extraBytes);
}
/**
* Returns the number of zero bits preceding the highest-order one-bit in the binary representation of the value.
* @param {Long} value
* @returns {Number}
*/
function numberOfLeadingZeros(value) {
if (value.equals(Long.ZERO)) {
return 64;
}
let n = 1;
let x = value.getHighBits();
if (x === 0) {
n += 32;
x = value.getLowBits();
}
if (x >>> 16 === 0) {
n += 16;
x <<= 16;
}
if (x >>> 24 === 0) {
n += 8;
x <<= 8;
}
if (x >>> 28 === 0) {
n += 4;
x <<= 4;
}
if (x >>> 30 === 0) {
n += 2;
x <<= 2;
}
n -= x >>> 31;
return n;
}
function encodeExtraBytesToRead(extraBytesToRead) {
return ~(0xff >> extraBytesToRead);
}
/**
* @param {Buffer} buffer
* @param {{value: number}} offset
* @returns {Long}
*/
function readVInt(buffer, offset) {
return decodeZigZag64(readUnsignedVInt(buffer, offset));
}
/**
* @param {Buffer} input
* @param {{ value: number}} offset
* @returns {Long}
*/
function readUnsignedVInt(input, offset) {
const firstByte = input[offset.value++];
if ((firstByte & 0x80) === 0) {
return Long.fromInt(firstByte);
}
const sByteInt = fromSignedByteToInt(firstByte);
const size = numberOfExtraBytesToRead(sByteInt);
let result = Long.fromInt(sByteInt & firstByteValueMask(size));
for (let ii = 0; ii < size; ii++) {
const b = Long.fromInt(input[offset.value++]);
// (result << 8) | b
result = result.shiftLeft(8).or(b);
}
return result;
}
function fromSignedByteToInt(value) {
if (value > 0x7f) {
return value - 0x0100;
}
return value;
}
function numberOfLeadingZerosInt32(i) {
if (i === 0) {
return 32;
}
let n = 1;
if (i >>> 16 === 0) {
n += 16;
i <<= 16;
}
if (i >>> 24 === 0) {
n += 8;
i <<= 8;
}
if (i >>> 28 === 0) {
n += 4;
i <<= 4;
}
if (i >>> 30 === 0) {
n += 2;
i <<= 2;
}
n -= i >>> 31;
return n;
}
/**
* @param {Number} firstByte
* @returns {Number}
*/
function numberOfExtraBytesToRead(firstByte) {
// Instead of counting 1s of the byte, we negate and count 0 of the byte
return numberOfLeadingZerosInt32(~firstByte) - 24;
}
/**
* @param {Number} extraBytesToRead
* @returns {Number}
*/
function firstByteValueMask(extraBytesToRead) {
return 0xff >> extraBytesToRead;
}
return {
readVInt: readVInt,
writeVInt: writeVInt,
};
})();
module.exports = Duration;