atomic-fns
Version:
Like Lodash, but for ESNext and with types. Stop shipping code built for browsers from 2015.
291 lines (290 loc) • 9.86 kB
JavaScript
/**
* Returns x % n but takes the sign of n instead of x
* @param x
* @param n
* @returns {number}
*/
function floorMod(x, n) {
return x - n * Math.floor(x / n);
}
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;
}
function parseInteger(str) {
if (str != null && str !== '')
return parseInt(str, 10);
}
function parseFloating(str) {
if (str != null && str !== '')
return parseFloat(str);
}
function parseMilliseconds(str) {
if (str != null && str !== '') {
const f = parseFloat('0.' + str) * 1000;
return Math.floor(f);
}
}
export function minDate(...dates) {
let ans = dates[0];
if (Array.isArray(ans)) {
dates = ans;
ans = ans[0];
}
for (let i = 1; i < dates.length; i++) {
if (dates[i].valueOf() < ans.valueOf()) {
ans = dates[i];
}
}
return ans;
}
export function maxDate(...dates) {
let ans = dates[0];
if (Array.isArray(ans)) {
dates = ans;
ans = ans[0];
}
for (let i = 1; i < dates.length; i++) {
if (dates[i].valueOf() > ans.valueOf()) {
ans = dates[i];
}
}
return ans;
}
/**
* Returns `true` if the year is a leap year, otherwise `false`.
* @param year The date year
* @returns {boolean} `true` if is a leap year
*/
export function isLeapYear(year) {
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
}
/**
* Get the number of days in the given year.
* @param year The date year
* @returns {number} Number of days in year
*/
export function daysInYear(year) {
return isLeapYear(year) ? 366 : 365;
}
/**
* Get the number of days in the given date's month and year.
* @param date The specified date
* @returns {number} Number of days in month
*/
export function daysInMonth(date) {
const d = new Date(date), year = d.getFullYear(), month = d.getMonth() + 1;
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)
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) {
let dt = new Date(d);
dt.setUTCFullYear(dt.getUTCFullYear() - 1900);
d = dt.getTime();
}
return +d;
}
function untruncateYear(year) {
if (year > 99) {
return year;
}
else
return year > 60 ? 1900 + year : 2000 + year;
}
// PARSING
function parseZoneInfo(ts, offsetFormat, locale, timeZone) {
const date = new Date(ts), intlOpts = {
hourCycle: 'h23',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
timeZone,
timeZoneName: offsetFormat
};
const parsed = new Intl.DateTimeFormat(locale, intlOpts)
.formatToParts(date)
.find((m) => m.type === 'timeZoneName');
return parsed?.value;
}
// signedOffset('-5', '30') -> -330
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 Error(`Invalid unit value ${value}`);
return numericValue;
}
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`);
}
}
function timeObject(obj) {
const { hour, minute, second, millisecond } = obj;
return { hour, minute, second, millisecond };
}
const ianaRegex = /[A-Za-z_+-]{1,256}(?::?\/[A-Za-z0-9_+-]{1,256}(?:\/[A-Za-z0-9_+-]{1,256})?)?/;
const isoDuration = /^-?P(?:(?:(-?\d{1,20}(?:\.\d{1,20})?)Y)?(?:(-?\d{1,20}(?:\.\d{1,20})?)M)?(?:(-?\d{1,20}(?:\.\d{1,20})?)W)?(?:(-?\d{1,20}(?:\.\d{1,20})?)D)?(?:T(?:(-?\d{1,20}(?:\.\d{1,20})?)H)?(?:(-?\d{1,20}(?:\.\d{1,20})?)M)?(?:(-?\d{1,20})(?:[.,](-?\d{1,20}))?S)?)?)$/;
function extractISODuration(match) {
const [s, yearStr, monthStr, weekStr, dayStr, hourStr, minuteStr, secondStr, millisecondsStr] = match;
const hasNegativePrefix = s[0] === '-';
const negativeSeconds = secondStr && secondStr[0] === '-';
const maybeNegate = (num, force = false) => num !== undefined && (force || (num && hasNegativePrefix)) ? -num : num;
return [
{
years: maybeNegate(parseFloating(yearStr)),
months: maybeNegate(parseFloating(monthStr)),
weeks: maybeNegate(parseFloating(weekStr)),
days: maybeNegate(parseFloating(dayStr)),
hours: maybeNegate(parseFloating(hourStr)),
minutes: maybeNegate(parseFloating(minuteStr)),
seconds: maybeNegate(parseFloating(secondStr), secondStr === '-0'),
milliseconds: maybeNegate(parseMilliseconds(millisecondsStr), negativeSeconds)
}
];
}
function parse(s, ...patterns) {
if (s == null) {
return [null, null];
}
for (const [regex, extractor] of patterns) {
const m = regex.exec(s);
if (m) {
return extractor(m);
}
}
return [null, null];
}
export function parseISODuration(s) {
return parse(s, [isoDuration, extractISODuration]);
}
function createUTCDate(...args) {
let date, y = args[0];
// the Date.UTC function remaps years 0-99 to 1900-1999
if (y < 100 && y >= 0) {
args = Array.from(arguments);
// preserve leap years using a full 400 year cycle, then reset
args[0] = y + 400;
date = new Date(Date.UTC.apply(null, args));
if (isFinite(date.getUTCFullYear())) {
date.setUTCFullYear(y);
}
}
else {
date = new Date(Date.UTC.apply(null, arguments));
}
return date;
}
function firstWeekOffset(year, dow, doy) {
var // first-week day -- which january is always in the first week (4 for iso, 1 for other)
fwd = 7 + dow - doy,
// first-week day local weekday -- which local weekday is fwd
fwdlw = (7 + createUTCDate(year, 0, fwd).getUTCDay() - dow) % 7;
return -fwdlw + fwd - 1;
}
// https://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
function dayOfYearFromWeeks(year, week, weekday, dow, doy) {
var localWeekday = (7 + weekday - dow) % 7, weekOffset = firstWeekOffset(year, dow, doy), dayOfYear = 1 + 7 * (week - 1) + localWeekday + weekOffset, resYear, resDayOfYear;
if (dayOfYear <= 0) {
resYear = year - 1;
resDayOfYear = daysInYear(resYear) + dayOfYear;
}
else if (dayOfYear > daysInYear(year)) {
resYear = year + 1;
resDayOfYear = dayOfYear - daysInYear(year);
}
else {
resYear = year;
resDayOfYear = dayOfYear;
}
return {
year: resYear,
dayOfYear: resDayOfYear
};
}
/**
* Returns the number of weeks in the specified year.
* @param year
* @param firstDayOfWeek
* @param firstDayOfYear
*/
export function weeksInYear(year, firstDayOfWeek = 1, firstDayOfYear = 1) {
var weekOffset = firstWeekOffset(year, firstDayOfWeek, firstDayOfYear), weekOffsetNext = firstWeekOffset(year + 1, firstDayOfWeek, firstDayOfYear);
return (daysInYear(year) - weekOffset + weekOffsetNext) / 7;
}
/**
* Returns the week of the year for the specified date
* @param year
* @param dayOfYear
* @param firstDayOfWeek
* @param firstDayOfYear
* @returns
*/
export function weekOfYear(date, firstDayOfWeek = 1, firstDayOfYear = 1) {
const year = new Date(date).getFullYear();
const doy = dayOfYear(date);
let weekOffset = firstWeekOffset(year, firstDayOfWeek, firstDayOfYear), week = Math.floor((doy - weekOffset - 1) / 7) + 1, resWeek, resYear;
if (week < 1) {
resYear = year - 1;
resWeek = week + weeksInYear(resYear, firstDayOfWeek, firstDayOfYear);
}
else if (week > weeksInYear(year, firstDayOfWeek, firstDayOfYear)) {
resWeek = week - weeksInYear(year, firstDayOfWeek, firstDayOfYear);
resYear = year + 1;
}
else {
resYear = year;
resWeek = week;
}
return {
week: resWeek,
year: resYear
};
}
export function isoWeeksInYear(year) {
return weeksInYear(year, 1, 4);
}
export function dayOfYear(input) {
input = new Date(input);
input.setHours(0);
input.setMinutes(0);
input.setSeconds(0);
input.setMilliseconds(0);
let start = new Date(input.getFullYear(), 0, 1, 0, 0, 0, 0);
return Math.round((+input - start.getTime()) / 864e5) + 1;
}