summaryrefslogtreecommitdiff
path: root/node_modules/luxon/src/impl/util.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/luxon/src/impl/util.js')
-rw-r--r--node_modules/luxon/src/impl/util.js330
1 files changed, 330 insertions, 0 deletions
diff --git a/node_modules/luxon/src/impl/util.js b/node_modules/luxon/src/impl/util.js
new file mode 100644
index 0000000..9164250
--- /dev/null
+++ b/node_modules/luxon/src/impl/util.js
@@ -0,0 +1,330 @@
+/*
+ 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";
+import Settings from "../settings.js";
+import { dayOfWeek, isoWeekdayToLocal } from "./conversions.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 hasRelative() {
+ try {
+ return typeof Intl !== "undefined" && !!Intl.RelativeTimeFormat;
+ } catch (e) {
+ return false;
+ }
+}
+
+export function hasLocaleWeekInfo() {
+ try {
+ return (
+ typeof Intl !== "undefined" &&
+ !!Intl.Locale &&
+ ("weekInfo" in Intl.Locale.prototype || "getWeekInfo" in Intl.Locale.prototype)
+ );
+ } 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);
+}
+
+export function validateWeekSettings(settings) {
+ if (settings == null) {
+ return null;
+ } else if (typeof settings !== "object") {
+ throw new InvalidArgumentError("Week settings must be an object");
+ } else {
+ if (
+ !integerBetween(settings.firstDay, 1, 7) ||
+ !integerBetween(settings.minimalDays, 1, 7) ||
+ !Array.isArray(settings.weekend) ||
+ settings.weekend.some((v) => !integerBetween(v, 1, 7))
+ ) {
+ throw new InvalidArgumentError("Invalid week settings");
+ }
+ return {
+ firstDay: settings.firstDay,
+ minimalDays: settings.minimalDays,
+ weekend: Array.from(settings.weekend),
+ };
+ }
+}
+
+// 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) {
+ const isNeg = input < 0;
+ let padded;
+ if (isNeg) {
+ padded = "-" + ("" + -input).padStart(n, "0");
+ } else {
+ padded = ("" + input).padStart(n, "0");
+ }
+ return padded;
+}
+
+export function parseInteger(string) {
+ if (isUndefined(string) || string === null || string === "") {
+ return undefined;
+ } else {
+ return parseInt(string, 10);
+ }
+}
+
+export function parseFloating(string) {
+ if (isUndefined(string) || string === null || string === "") {
+ return undefined;
+ } else {
+ return parseFloat(string);
+ }
+}
+
+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, rounding = "round") {
+ const factor = 10 ** digits;
+ switch (rounding) {
+ case "expand":
+ return number > 0
+ ? Math.ceil(number * factor) / factor
+ : Math.floor(number * factor) / factor;
+ case "trunc":
+ return Math.trunc(number * factor) / factor;
+ case "round":
+ return Math.round(number * factor) / factor;
+ case "floor":
+ return Math.floor(number * factor) / factor;
+ case "ceil":
+ return Math.ceil(number * factor) / factor;
+ default:
+ throw new RangeError(`Value rounding ${rounding} is out of range`);
+ }
+}
+
+// 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];
+ }
+}
+
+// convert 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);
+ // set the month and day again, this is necessary because year 2000 is a leap year, but year 100 is not
+ // so if obj.year is in 99, but obj.day makes it roll over into year 100,
+ // the calculations done by Date.UTC are using year 2000 - which is incorrect
+ d.setUTCFullYear(obj.year, obj.month - 1, obj.day);
+ }
+ return +d;
+}
+
+// adapted from moment.js: https://github.com/moment/moment/blob/000ac1800e620f770f4eb31b5ae908f6167b0ab2/src/lib/units/week-calendar-utils.js
+function firstWeekOffset(year, minDaysInFirstWeek, startOfWeek) {
+ const fwdlw = isoWeekdayToLocal(dayOfWeek(year, 1, minDaysInFirstWeek), startOfWeek);
+ return -fwdlw + minDaysInFirstWeek - 1;
+}
+
+export function weeksInWeekYear(weekYear, minDaysInFirstWeek = 4, startOfWeek = 1) {
+ const weekOffset = firstWeekOffset(weekYear, minDaysInFirstWeek, startOfWeek);
+ const weekOffsetNext = firstWeekOffset(weekYear + 1, minDaysInFirstWeek, startOfWeek);
+ return (daysInYear(weekYear) - weekOffset + weekOffsetNext) / 7;
+}
+
+export function untruncateYear(year) {
+ if (year > 99) {
+ return year;
+ } else return year > Settings.twoDigitCutoffYear ? 1900 + year : 2000 + year;
+}
+
+// PARSING
+
+export function parseZoneInfo(ts, offsetFormat, locale, timeZone = null) {
+ const date = new Date(ts),
+ intlOpts = {
+ hourCycle: "h23",
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ };
+
+ if (timeZone) {
+ intlOpts.timeZone = timeZone;
+ }
+
+ const modified = { timeZoneName: offsetFormat, ...intlOpts };
+
+ const parsed = new Intl.DateTimeFormat(locale, modified)
+ .formatToParts(date)
+ .find((m) => m.type.toLowerCase() === "timezonename");
+ return parsed ? parsed.value : 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.isFinite(numericValue))
+ throw new InvalidArgumentError(`Invalid unit value ${value}`);
+ return numericValue;
+}
+
+export function normalizeObject(obj, normalizer) {
+ const normalized = {};
+ for (const u in obj) {
+ if (hasOwnProperty(obj, u)) {
+ const v = obj[u];
+ if (v === undefined || v === null) continue;
+ normalized[normalizer(u)] = asNumber(v);
+ }
+ }
+ return normalized;
+}
+
+/**
+ * Returns the offset's value as a string
+ * @param {number} ts - Epoch milliseconds for which to get the offset
+ * @param {string} format - What style of offset to return.
+ * Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively
+ * @return {string}
+ */
+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"]);
+}