186 lines
5.2 KiB
JavaScript
186 lines
5.2 KiB
JavaScript
import {stripTime, today} from './date.js';
|
|
import {lastItemOf} from './utils.js';
|
|
|
|
// pattern for format parts
|
|
export const reFormatTokens = /dd?|DD?|mm?|MM?|yy?(?:yy)?/;
|
|
// pattern for non date parts
|
|
export const reNonDateParts = /[\s!-/:-@[-`{-~年月日]+/;
|
|
// cache for persed formats
|
|
let knownFormats = {};
|
|
// parse funtions for date parts
|
|
const parseFns = {
|
|
y(date, year) {
|
|
return new Date(date).setFullYear(parseInt(year, 10));
|
|
},
|
|
m(date, month, locale) {
|
|
const newDate = new Date(date);
|
|
let monthIndex = parseInt(month, 10) - 1;
|
|
|
|
if (isNaN(monthIndex)) {
|
|
if (!month) {
|
|
return NaN;
|
|
}
|
|
|
|
const monthName = month.toLowerCase();
|
|
const compareNames = name => name.toLowerCase().startsWith(monthName);
|
|
// compare with both short and full names because some locales have periods
|
|
// in the short names (not equal to the first X letters of the full names)
|
|
monthIndex = locale.monthsShort.findIndex(compareNames);
|
|
if (monthIndex < 0) {
|
|
monthIndex = locale.months.findIndex(compareNames);
|
|
}
|
|
if (monthIndex < 0) {
|
|
return NaN;
|
|
}
|
|
}
|
|
|
|
newDate.setMonth(monthIndex);
|
|
return newDate.getMonth() !== normalizeMonth(monthIndex)
|
|
? newDate.setDate(0)
|
|
: newDate.getTime();
|
|
},
|
|
d(date, day) {
|
|
return new Date(date).setDate(parseInt(day, 10));
|
|
},
|
|
};
|
|
// format functions for date parts
|
|
const formatFns = {
|
|
d(date) {
|
|
return date.getDate();
|
|
},
|
|
dd(date) {
|
|
return padZero(date.getDate(), 2);
|
|
},
|
|
D(date, locale) {
|
|
return locale.daysShort[date.getDay()];
|
|
},
|
|
DD(date, locale) {
|
|
return locale.days[date.getDay()];
|
|
},
|
|
m(date) {
|
|
return date.getMonth() + 1;
|
|
},
|
|
mm(date) {
|
|
return padZero(date.getMonth() + 1, 2);
|
|
},
|
|
M(date, locale) {
|
|
return locale.monthsShort[date.getMonth()];
|
|
},
|
|
MM(date, locale) {
|
|
return locale.months[date.getMonth()];
|
|
},
|
|
y(date) {
|
|
return date.getFullYear();
|
|
},
|
|
yy(date) {
|
|
return padZero(date.getFullYear(), 2).slice(-2);
|
|
},
|
|
yyyy(date) {
|
|
return padZero(date.getFullYear(), 4);
|
|
},
|
|
};
|
|
|
|
// get month index in normal range (0 - 11) from any number
|
|
function normalizeMonth(monthIndex) {
|
|
return monthIndex > -1 ? monthIndex % 12 : normalizeMonth(monthIndex + 12);
|
|
}
|
|
|
|
function padZero(num, length) {
|
|
return num.toString().padStart(length, '0');
|
|
}
|
|
|
|
function parseFormatString(format) {
|
|
if (typeof format !== 'string') {
|
|
throw new Error("Invalid date format.");
|
|
}
|
|
if (format in knownFormats) {
|
|
return knownFormats[format];
|
|
}
|
|
|
|
// sprit the format string into parts and seprators
|
|
const separators = format.split(reFormatTokens);
|
|
const parts = format.match(new RegExp(reFormatTokens, 'g'));
|
|
if (separators.length === 0 || !parts) {
|
|
throw new Error("Invalid date format.");
|
|
}
|
|
|
|
// collect format functions used in the format
|
|
const partFormatters = parts.map(token => formatFns[token]);
|
|
|
|
// collect parse function keys used in the format
|
|
// iterate over parseFns' keys in order to keep the order of the keys.
|
|
const partParserKeys = Object.keys(parseFns).reduce((keys, key) => {
|
|
const token = parts.find(part => part[0] !== 'D' && part[0].toLowerCase() === key);
|
|
if (token) {
|
|
keys.push(key);
|
|
}
|
|
return keys;
|
|
}, []);
|
|
|
|
return knownFormats[format] = {
|
|
parser(dateStr, locale) {
|
|
const dateParts = dateStr.split(reNonDateParts).reduce((dtParts, part, index) => {
|
|
if (part.length > 0 && parts[index]) {
|
|
const token = parts[index][0];
|
|
if (token === 'M') {
|
|
dtParts.m = part;
|
|
} else if (token !== 'D') {
|
|
dtParts[token] = part;
|
|
}
|
|
}
|
|
return dtParts;
|
|
}, {});
|
|
|
|
// iterate over partParserkeys so that the parsing is made in the oder
|
|
// of year, month and day to prevent the day parser from correcting last
|
|
// day of month wrongly
|
|
return partParserKeys.reduce((origDate, key) => {
|
|
const newDate = parseFns[key](origDate, dateParts[key], locale);
|
|
// ingnore the part failed to parse
|
|
return isNaN(newDate) ? origDate : newDate;
|
|
}, today());
|
|
},
|
|
formatter(date, locale) {
|
|
let dateStr = partFormatters.reduce((str, fn, index) => {
|
|
return str += `${separators[index]}${fn(date, locale)}`;
|
|
}, '');
|
|
// separators' length is always parts' length + 1,
|
|
return dateStr += lastItemOf(separators);
|
|
},
|
|
};
|
|
}
|
|
|
|
export function parseDate(dateStr, format, locale) {
|
|
if (dateStr instanceof Date || typeof dateStr === 'number') {
|
|
const date = stripTime(dateStr);
|
|
return isNaN(date) ? undefined : date;
|
|
}
|
|
if (!dateStr) {
|
|
return undefined;
|
|
}
|
|
if (dateStr === 'today') {
|
|
return today();
|
|
}
|
|
|
|
if (format && format.toValue) {
|
|
const date = format.toValue(dateStr, format, locale);
|
|
return isNaN(date) ? undefined : stripTime(date);
|
|
}
|
|
|
|
return parseFormatString(format).parser(dateStr, locale);
|
|
}
|
|
|
|
export function formatDate(date, format, locale) {
|
|
if (isNaN(date) || (!date && date !== 0)) {
|
|
return '';
|
|
}
|
|
|
|
const dateObj = typeof date === 'number' ? new Date(date) : date;
|
|
|
|
if (format.toDisplay) {
|
|
return format.toDisplay(dateObj, format, locale);
|
|
}
|
|
|
|
return parseFormatString(format).formatter(dateObj, locale);
|
|
}
|