atomic-fns
Version:
Like Lodash, but for ESNext and with types. Stop shipping code built for browsers from 2015.
496 lines (495 loc) • 14.3 kB
JavaScript
const NATIVE_DATE = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/;
const REGEX_PARSE = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/;
const REGEX_FORMAT = /\[([^\]]+)]|Y{1,4}|M{1,4}|Q|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|z{1,4}|N{1,4}|LTS?|L{1,4}|l{1,4}|x|X|SSS/g;
const FORMAT_DEFAULT = 'YYYY-MM-DDTHH:mm:ssZ';
const padStart = (string, length, pad) => String(string).padStart(length, pad);
const padZoneStr = (instance) => {
const negMinutes = -instance.getTimezoneOffset();
const minutes = Math.abs(negMinutes);
const hourOffset = Math.floor(minutes / 60);
const minuteOffset = minutes % 60;
return `${negMinutes <= 0 ? '+' : '-'}${padStart(hourOffset, 2, '0')}:${padStart(minuteOffset, 2, '0')}`;
};
/**
* Returns a new Date from the given arguments.
* @param {string | number | Date} date The date value
* @param {?boolean} [utc=false] Interprets the given value as a UTC date
* @returns {Date} The new date object
*/
export function asDate(date, utc = false) {
if (date === null) {
return new Date(NaN);
}
if (date === undefined) {
date = Date.now();
}
if (typeof date === 'string' && !/Z$/i.test(date)) {
const d = date.match(NATIVE_DATE);
if (d) {
const m = d[2] - 1 || 0;
const ms = (d[7] || '0').slice(0, 3);
if (utc) {
return new Date(Date.UTC(d[1], m, d[3] || 1, d[4] || 0, d[5] || 0, d[6] || 0, ms));
}
return new Date(d[1], m, d[3] || 1, d[4] || 0, d[5] || 0, d[6] || 0, ms);
}
}
const fromDate = new Date(date);
if (utc) {
fromDate.setFullYear(fromDate.getUTCFullYear());
fromDate.setMonth(fromDate.getUTCMonth());
fromDate.setDate(fromDate.getUTCDate());
fromDate.setHours(fromDate.getUTCHours());
fromDate.setMinutes(fromDate.getUTCMinutes());
fromDate.setSeconds(fromDate.getUTCSeconds());
fromDate.setMilliseconds(fromDate.getUTCMilliseconds());
}
return fromDate;
}
const n = 'numeric';
const s = 'short';
const l = 'long';
const dd = '2-digit';
const STRFTIME = {
// weekday
'%A': { weekday: l },
'%a': { weekday: s },
'%aa': { weekday: n },
// day
'%d': { day: dd },
'%-d': { day: n },
// month
'%m': { month: dd },
'%-m': { month: n },
'%B': { month: l },
'%b': { month: s },
'%bb': { month: n },
// year
'%y': { year: n },
'%Y': { year: dd },
// hour
'%K': { hourCycle: 'h11', hour: dd },
'%I': { hourCycle: 'h12', hour: dd },
'%-I': { hourCycle: 'h12', hour: n },
'%H': { hourCycle: 'h23', hour: dd },
'%-H': { hourCycle: 'h23', hour: n },
'%k': { hourCycle: 'h24', hour: n },
// minutes
'%M': { minute: dd },
'%-M': { minute: n },
// seconds
'%S': { second: dd },
'%-S': { second: n },
// day period
'%P': { dayPeriod: l },
'%p': { dayPeriod: s },
'%pp': { dayPeriod: 'narrow' },
// era
'%N': { era: l, year: dd },
'%n': { era: s, year: dd },
'%nn': { era: n, year: 'narrow' },
// timeZoneName
// '%Z': { timeZoneName: 'longGeneric', year: dd },
// '%-Z': { timeZoneName: 'shortGeneric', year: dd },
'%z': { timeZoneName: 'longOffset', year: dd },
'%-z': { timeZoneName: 'shortOffset', year: dd },
'%Z': { timeZoneName: l, year: dd },
'%-Z': { timeZoneName: s, year: dd }
};
const TOKENS = {
// weekday
dddd: { weekday: l },
ddd: { weekday: s },
E: { weekday: n },
// day
DD: { day: dd },
D: { day: n },
// month
MM: { month: dd },
M: { month: n },
MMMM: { month: l },
MMM: { month: s },
// year
YYYY: { year: n },
YY: { year: dd },
// hour
hh: { hourCycle: 'h12', hour: dd },
h: { hourCycle: 'h12', hour: n },
HH: { hourCycle: 'h23', hour: dd },
H: { hourCycle: 'h23', hour: n },
kk: { hourCycle: 'h11', hour: dd },
k: { hourCycle: 'h24', hour: n },
// minutes
mm: { minute: dd },
m: { minute: n },
// seconds
ss: { second: dd },
s: { second: n },
// day period
A: { dayPeriod: l },
a: { dayPeriod: s },
// era
N: { era: s, year: dd },
NN: { era: s, year: dd },
NNN: { era: s, year: dd },
NNNN: { era: l, year: dd },
// timeZoneName
ZZ: { timeZoneName: 'longOffset', year: dd },
Z: { timeZoneName: 'shortOffset', year: dd },
zzz: { timeZoneName: l, year: dd },
z: { timeZoneName: s, year: dd }
};
const HOUR_PARTS = { '%I': 1, '%-I': 1, '%K': 1, '%k': 1 };
const ERA_PARTS = { '%n': 1, '%N': 1, '%nn': 1 };
const TZ_PARTS = { '%ZZ': 1, '%-ZZ': 1, '%-z': 1, '%z': 1, '%-Z': 1, '%Z': 1 };
const formatter = (locale, opts) => new Intl.DateTimeFormat(locale, opts);
const formatHour = (h, maxLength) => String(h % 12 || 12).padStart(maxLength, '0');
const formatMeridiem = (hour, isLowercase) => {
const m = hour < 12 ? 'AM' : 'PM';
return isLowercase ? m.toLowerCase() : m;
};
const formatPart = (locale, t, date) => {
const value = formatter(locale, TOKENS[t]).format(date);
return value.split(/\s+/g).slice(1).join(' ');
};
/**
* Returns a localized string representation of this date, according to the given format string. Format codes use the same specification as {@link https://momentjs.com/docs/#/displaying/format/ moment}.
* @param {string} str The format string to use
* @see {@link https://momentjs.com/docs/#/displaying/format/ List of formats}
* @example
```js
formatDate(new Date(), 'MM/DD/YYYY') // '10/31/2022'
```
*/
export function formatDate(formatStr, date, locale) {
const str = formatStr || FORMAT_DEFAULT;
const zoneStr = padZoneStr(date);
const time = {
H: date.getHours(),
HH: padStart(date.getHours(), 2, '0'),
h: formatHour(date.getHours(), 1),
hh: formatHour(date.getHours(), 2),
a: formatMeridiem(date.getHours(), true),
A: formatMeridiem(date.getHours()),
m: date.getMinutes(),
mm: padStart(date.getMinutes(), 2, '0'),
s: date.getSeconds(),
ss: padStart(date.getSeconds(), 2, '0'),
SSS: padStart(date.getMilliseconds(), 3, '0'),
X: Math.floor(date.getTime() / 1000),
x: date.getTime()
};
const matches = {
...time,
N: formatPart(locale, 'N', date),
NN: formatPart(locale, 'NN', date),
NNN: formatPart(locale, 'NNN', date),
NNNN: formatPart(locale, 'NNNN', date),
YY: String(date.getFullYear()).slice(-2),
YYYY: date.getFullYear(),
M: date.getMonth() + 1,
MM: padStart(date.getMonth() + 1, 2, '0'),
MMM: formatter(locale, TOKENS.MMM).format(date),
MMMM: formatter(locale, TOKENS.MMMM).format(date),
Q: Math.ceil((date.getMonth() + 1) / 3),
D: date.getDate(),
DD: padStart(date.getDate(), 2, '0'),
d: date.getDay(),
ddd: formatter(locale, TOKENS.ddd).format(date),
dddd: formatter(locale, TOKENS.dddd).format(date),
Z: formatPart(locale, 'Z', date),
ZZ: formatPart(locale, 'ZZ', date),
z: formatPart(locale, 'z', date),
zzz: formatPart(locale, 'zzz', date),
LT: `${time.h}:${time.mm} ${time.A}`,
LTS: `${time.h}:${time.mm}:${time.ss} ${time.A}`,
L: formatter(locale, DATE_SHORT_DD).format(date),
LL: formatter(locale, DATE_FULL).format(date),
LLL: formatter(locale, DATE_LONG_TIME).format(date),
LLLL: formatter(locale, DATE_HUGE).format(date),
l: formatter(locale, DATE_SHORT).format(date),
ll: formatter(locale, DATE_MED).format(date),
lll: formatter(locale, DATE_MED_TIME).format(date),
llll: formatter(locale, DATE_MED_WITH_WEEKDAY).format(date)
};
return str.replace(REGEX_FORMAT, (match, $1) => $1 || matches[match] || zoneStr.replace(':', '')); // 'ZZ'
}
/**
* Formats a date using a formatting string in the {@link https://strftime.org/ strftime} format, in any given locale.
* @param {string} fmt The format string to apply
* @param {Date} date The date value
* @param {?string} locale The locale to use when formatting (default is system locale).
* @returns {string} The string representation of date
* @see {@link https://strftime.org/ strftime format}
*/
export function strftime(fmt, date, locale) {
const results = [];
let dayPeriod = '';
let dayPeriodIndex = -1;
for (let i = 0; i < fmt.length; i++) {
let part = fmt[i];
if (part === '%') {
// check for escaped '%'
if (fmt[++i] === '%') {
results.push('%');
continue;
}
if (fmt[i] === '-') {
part += fmt[i++];
}
part += fmt[i];
// add any repeated characters that come after
while (i < fmt.length - 1 && fmt[i + 1] === fmt[i])
part += fmt[++i];
if (HOUR_PARTS[part]) {
const res = formatter(locale, STRFTIME[part]).format(date).split(/\s+/g);
// save the AM/PM for the correct spot
results.push(res[0]);
dayPeriod = res[1];
continue;
}
if (part === '%P' || part === '%p') {
// leave this flag in place for later
dayPeriodIndex = results.length;
if (part === '%p' && dayPeriod) {
dayPeriod = dayPeriod.toLowerCase();
}
results.push(part);
continue;
}
if (ERA_PARTS[part] || TZ_PARTS[part]) {
// the formatting includes the date/year so we take what comes after
const res = formatter(locale, STRFTIME[part]).format(date).split(/\s+/g);
results.push(res.slice(1).join(' '));
continue;
}
// format any other parts
let res = formatter(locale, STRFTIME[part]).format(date);
// fix bugs in time formatter
if ((part === '%S' || part === '%M') && res.length === 1)
res = '0' + res;
results.push(res);
}
else {
// output as literal
results.push(part);
}
}
// insert the AM/PM at the correct spot
if (dayPeriodIndex >= 0)
results[dayPeriodIndex] = dayPeriod || '';
return results.join('').trim();
}
const DATETIME_DEFAULT = {};
/** L: 09/04/1983 */
const DATE_SHORT_DD = {
year: n,
month: dd,
day: dd
};
/** l: 9/4/1983 */
const DATE_SHORT = {
year: n,
month: n,
day: n
};
/** ll: Oct 14, 1983 */
const DATE_MED = {
year: n,
month: s,
day: n
};
/** LL: October 14, 1983 */
const DATE_FULL = {
year: n,
month: l,
day: n
};
/** lll: Oct 14, 1983 8:30 PM */
const DATE_MED_TIME = {
year: n,
month: s,
day: n,
hour: n,
minute: n,
hourCycle: 'h12'
};
/** LLL: October 14, 1983 8:30 PM */
const DATE_LONG_TIME = {
year: n,
month: l,
day: n,
hour: n,
minute: n,
hourCycle: 'h12'
};
/** llll: Fri, Oct 14, 1983, 8:30 PM */
const DATE_MED_WITH_WEEKDAY = {
year: n,
month: s,
day: n,
weekday: s,
hour: n,
minute: n,
hourCycle: 'h12'
};
/** LLLL: Tuesday, October 14, 1983, 8:30 PM */
const DATE_HUGE = {
year: n,
month: l,
day: n,
weekday: l,
hour: n,
minute: n,
hourCycle: 'h12'
};
/** LT: 09:30 AM */
const TIME_SIMPLE = {
hour: n,
minute: n,
hourCycle: 'h12'
};
/** LTS: 09:30:23 AM */
const TIME_WITH_SECONDS = {
hour: n,
minute: n,
second: n,
hourCycle: 'h12'
};
/** 09:30:23 AM EDT */
const TIME_WITH_SHORT_OFFSET = {
hour: n,
minute: n,
second: n,
timeZoneName: s,
hourCycle: 'h12'
};
/** 09:30:23 AM Eastern Daylight Time */
const TIME_WITH_LONG_OFFSET = {
hour: n,
minute: n,
second: n,
timeZoneName: l,
hourCycle: 'h12'
};
/** 09:30 */
const TIME_24_SIMPLE = {
hour: n,
minute: n,
hourCycle: 'h23'
};
/** 09:30:23 */
const TIME_24_WITH_SECONDS = {
hour: n,
minute: n,
second: n,
hourCycle: 'h23'
};
/** 09:30:23 EDT */
const TIME_24_WITH_SHORT_OFFSET = {
hour: n,
minute: n,
second: n,
hourCycle: 'h23',
timeZoneName: s
};
/** 09:30:23 Eastern Daylight Time */
const TIME_24_WITH_LONG_OFFSET = {
hour: n,
minute: n,
second: n,
hourCycle: 'h23',
timeZoneName: l
};
/** 10/14/1983, 9:30 AM */
const DATETIME_SHORT = {
year: n,
month: n,
day: n,
hour: n,
minute: n,
hourCycle: 'h12'
};
/** 10/14/1983, 9:30:33 AM */
const DATETIME_SHORT_WITH_SECONDS = {
year: n,
month: n,
day: n,
hour: n,
minute: n,
second: n,
hourCycle: 'h12'
};
/** Oct 14, 1983, 9:30 AM */
const DATETIME_MED = {
year: n,
month: s,
day: n,
hour: n,
minute: n,
hourCycle: 'h12'
};
/** Oct 14, 1983, 9:30:33 AM */
const DATETIME_MED_WITH_SECONDS = {
year: n,
month: s,
day: n,
hour: n,
minute: n,
second: n,
hourCycle: 'h12'
};
/** Fri, 14 Oct 1983, 9:30 AM */
const DATETIME_MED_WITH_WEEKDAY = {
year: n,
month: s,
day: n,
weekday: s,
hour: n,
minute: n,
hourCycle: 'h12'
};
/** October 14, 1983, 9:30 AM EDT */
const DATETIME_FULL = {
year: n,
month: l,
day: n,
hour: n,
minute: n,
timeZoneName: s,
hourCycle: 'h12'
};
/** October 14, 1983, 9:30:33 AM EDT */
const DATETIME_FULL_WITH_SECONDS = {
year: n,
month: l,
day: n,
hour: n,
minute: n,
second: n,
timeZoneName: s,
hourCycle: 'h12'
};
/** Friday, October 14, 1983, 9:30 AM Eastern Daylight Time */
const DATETIME_HUGE = {
year: n,
month: l,
day: n,
weekday: l,
hour: n,
minute: n,
timeZoneName: l,
hourCycle: 'h12'
};
/** Friday, October 14, 1983, 9:30:33 AM Eastern Daylight Time */
const DATETIME_HUGE_WITH_SECONDS = {
year: n,
month: l,
day: n,
weekday: l,
hour: n,
minute: n,
second: n,
timeZoneName: l,
hourCycle: 'h12'
};