// Loaded from https://unpkg.com/luxon@1.25.0/src/impl/util.js /* 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})?)?/;