forked from waja/action-debian-package
287 lines
7 KiB
JavaScript
287 lines
7 KiB
JavaScript
/*
|
|
This is just a junk drawer, containing anything used across multiple classes.
|
|
Because Luxon is small(ish), this should stay small and we won't worry about splitting
|
|
it up into, say, parsingUtil.js and basicUtil.js and so on. But they are divided up by feature area.
|
|
*/
|
|
|
|
import { InvalidArgumentError } from "../errors.js";
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
|
|
// TYPES
|
|
|
|
export function isUndefined(o) {
|
|
return typeof o === "undefined";
|
|
}
|
|
|
|
export function isNumber(o) {
|
|
return typeof o === "number";
|
|
}
|
|
|
|
export function isInteger(o) {
|
|
return typeof o === "number" && o % 1 === 0;
|
|
}
|
|
|
|
export function isString(o) {
|
|
return typeof o === "string";
|
|
}
|
|
|
|
export function isDate(o) {
|
|
return Object.prototype.toString.call(o) === "[object Date]";
|
|
}
|
|
|
|
// CAPABILITIES
|
|
|
|
export function hasIntl() {
|
|
try {
|
|
return typeof Intl !== "undefined" && Intl.DateTimeFormat;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function hasFormatToParts() {
|
|
return !isUndefined(Intl.DateTimeFormat.prototype.formatToParts);
|
|
}
|
|
|
|
export function hasRelative() {
|
|
try {
|
|
return typeof Intl !== "undefined" && !!Intl.RelativeTimeFormat;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// OBJECTS AND ARRAYS
|
|
|
|
export function maybeArray(thing) {
|
|
return Array.isArray(thing) ? thing : [thing];
|
|
}
|
|
|
|
export function bestBy(arr, by, compare) {
|
|
if (arr.length === 0) {
|
|
return undefined;
|
|
}
|
|
return arr.reduce((best, next) => {
|
|
const pair = [by(next), next];
|
|
if (!best) {
|
|
return pair;
|
|
} else if (compare(best[0], pair[0]) === best[0]) {
|
|
return best;
|
|
} else {
|
|
return pair;
|
|
}
|
|
}, null)[1];
|
|
}
|
|
|
|
export function pick(obj, keys) {
|
|
return keys.reduce((a, k) => {
|
|
a[k] = obj[k];
|
|
return a;
|
|
}, {});
|
|
}
|
|
|
|
export function hasOwnProperty(obj, prop) {
|
|
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
}
|
|
|
|
// NUMBERS AND STRINGS
|
|
|
|
export function integerBetween(thing, bottom, top) {
|
|
return isInteger(thing) && thing >= bottom && thing <= top;
|
|
}
|
|
|
|
// x % n but takes the sign of n instead of x
|
|
export function floorMod(x, n) {
|
|
return x - n * Math.floor(x / n);
|
|
}
|
|
|
|
export function padStart(input, n = 2) {
|
|
if (input.toString().length < n) {
|
|
return ("0".repeat(n) + input).slice(-n);
|
|
} else {
|
|
return input.toString();
|
|
}
|
|
}
|
|
|
|
export function parseInteger(string) {
|
|
if (isUndefined(string) || string === null || string === "") {
|
|
return undefined;
|
|
} else {
|
|
return parseInt(string, 10);
|
|
}
|
|
}
|
|
|
|
export function parseMillis(fraction) {
|
|
// Return undefined (instead of 0) in these cases, where fraction is not set
|
|
if (isUndefined(fraction) || fraction === null || fraction === "") {
|
|
return undefined;
|
|
} else {
|
|
const f = parseFloat("0." + fraction) * 1000;
|
|
return Math.floor(f);
|
|
}
|
|
}
|
|
|
|
export function roundTo(number, digits, towardZero = false) {
|
|
const factor = 10 ** digits,
|
|
rounder = towardZero ? Math.trunc : Math.round;
|
|
return rounder(number * factor) / factor;
|
|
}
|
|
|
|
// DATE BASICS
|
|
|
|
export function isLeapYear(year) {
|
|
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
}
|
|
|
|
export function daysInYear(year) {
|
|
return isLeapYear(year) ? 366 : 365;
|
|
}
|
|
|
|
export function daysInMonth(year, month) {
|
|
const modMonth = floorMod(month - 1, 12) + 1,
|
|
modYear = year + (month - modMonth) / 12;
|
|
|
|
if (modMonth === 2) {
|
|
return isLeapYear(modYear) ? 29 : 28;
|
|
} else {
|
|
return [31, null, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][modMonth - 1];
|
|
}
|
|
}
|
|
|
|
// covert a calendar object to a local timestamp (epoch, but with the offset baked in)
|
|
export function objToLocalTS(obj) {
|
|
let d = Date.UTC(
|
|
obj.year,
|
|
obj.month - 1,
|
|
obj.day,
|
|
obj.hour,
|
|
obj.minute,
|
|
obj.second,
|
|
obj.millisecond
|
|
);
|
|
|
|
// for legacy reasons, years between 0 and 99 are interpreted as 19XX; revert that
|
|
if (obj.year < 100 && obj.year >= 0) {
|
|
d = new Date(d);
|
|
d.setUTCFullYear(d.getUTCFullYear() - 1900);
|
|
}
|
|
return +d;
|
|
}
|
|
|
|
export function weeksInWeekYear(weekYear) {
|
|
const p1 =
|
|
(weekYear +
|
|
Math.floor(weekYear / 4) -
|
|
Math.floor(weekYear / 100) +
|
|
Math.floor(weekYear / 400)) %
|
|
7,
|
|
last = weekYear - 1,
|
|
p2 = (last + Math.floor(last / 4) - Math.floor(last / 100) + Math.floor(last / 400)) % 7;
|
|
return p1 === 4 || p2 === 3 ? 53 : 52;
|
|
}
|
|
|
|
export function untruncateYear(year) {
|
|
if (year > 99) {
|
|
return year;
|
|
} else return year > 60 ? 1900 + year : 2000 + year;
|
|
}
|
|
|
|
// PARSING
|
|
|
|
export function parseZoneInfo(ts, offsetFormat, locale, timeZone = null) {
|
|
const date = new Date(ts),
|
|
intlOpts = {
|
|
hour12: false,
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit"
|
|
};
|
|
|
|
if (timeZone) {
|
|
intlOpts.timeZone = timeZone;
|
|
}
|
|
|
|
const modified = Object.assign({ timeZoneName: offsetFormat }, intlOpts),
|
|
intl = hasIntl();
|
|
|
|
if (intl && hasFormatToParts()) {
|
|
const parsed = new Intl.DateTimeFormat(locale, modified)
|
|
.formatToParts(date)
|
|
.find(m => m.type.toLowerCase() === "timezonename");
|
|
return parsed ? parsed.value : null;
|
|
} else if (intl) {
|
|
// this probably doesn't work for all locales
|
|
const without = new Intl.DateTimeFormat(locale, intlOpts).format(date),
|
|
included = new Intl.DateTimeFormat(locale, modified).format(date),
|
|
diffed = included.substring(without.length),
|
|
trimmed = diffed.replace(/^[, \u200e]+/, "");
|
|
return trimmed;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// signedOffset('-5', '30') -> -330
|
|
export function signedOffset(offHourStr, offMinuteStr) {
|
|
let offHour = parseInt(offHourStr, 10);
|
|
|
|
// don't || this because we want to preserve -0
|
|
if (Number.isNaN(offHour)) {
|
|
offHour = 0;
|
|
}
|
|
|
|
const offMin = parseInt(offMinuteStr, 10) || 0,
|
|
offMinSigned = offHour < 0 || Object.is(offHour, -0) ? -offMin : offMin;
|
|
return offHour * 60 + offMinSigned;
|
|
}
|
|
|
|
// COERCION
|
|
|
|
export function asNumber(value) {
|
|
const numericValue = Number(value);
|
|
if (typeof value === "boolean" || value === "" || Number.isNaN(numericValue))
|
|
throw new InvalidArgumentError(`Invalid unit value ${value}`);
|
|
return numericValue;
|
|
}
|
|
|
|
export function normalizeObject(obj, normalizer, nonUnitKeys) {
|
|
const normalized = {};
|
|
for (const u in obj) {
|
|
if (hasOwnProperty(obj, u)) {
|
|
if (nonUnitKeys.indexOf(u) >= 0) continue;
|
|
const v = obj[u];
|
|
if (v === undefined || v === null) continue;
|
|
normalized[normalizer(u)] = asNumber(v);
|
|
}
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
export function formatOffset(offset, format) {
|
|
const hours = Math.trunc(Math.abs(offset / 60)),
|
|
minutes = Math.trunc(Math.abs(offset % 60)),
|
|
sign = offset >= 0 ? "+" : "-";
|
|
|
|
switch (format) {
|
|
case "short":
|
|
return `${sign}${padStart(hours, 2)}:${padStart(minutes, 2)}`;
|
|
case "narrow":
|
|
return `${sign}${hours}${minutes > 0 ? `:${minutes}` : ""}`;
|
|
case "techie":
|
|
return `${sign}${padStart(hours, 2)}${padStart(minutes, 2)}`;
|
|
default:
|
|
throw new RangeError(`Value format ${format} is out of range for property format`);
|
|
}
|
|
}
|
|
|
|
export function timeObject(obj) {
|
|
return pick(obj, ["hour", "minute", "second", "millisecond"]);
|
|
}
|
|
|
|
export const ianaRegex = /[A-Za-z_+-]{1,256}(:?\/[A-Za-z_+-]{1,256}(\/[A-Za-z_+-]{1,256})?)?/;
|