|
/** |
|
* Selective implementation of `strftime` for JavaScript Dates in Amrican English. |
|
* |
|
* For the strftime specification see {@link https://www.man7.org/linux/man-pages/man3/strftime.3.html}. |
|
* |
|
* @param {string} format The format for the resulting string |
|
* @param {Date|Number} value The Date or timestamp value to use |
|
* @returns {string} Formatted date as a string |
|
*/ |
|
function strftime(formatString, value) { |
|
if (typeof value === "number") value = new Date(value); |
|
|
|
const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; |
|
const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; |
|
const daysPerMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; |
|
|
|
// Detect if the date is a leap year. Leap Year Rules: |
|
// + year is devisible by 4 |
|
// - year is not divisible by 100 |
|
// + or year is divisible by 400 |
|
const isLeapYear = (year) => (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0); |
|
|
|
// const isLeapYear = (date) => |
|
|
|
const zeroPad = (value, length = 2) => String(value).padStart(length, "0"); |
|
const unsupportedKey = (key) => { |
|
throw new Error(`'${key}' is not supported in this implementation of stftime`); |
|
}; |
|
|
|
const converters = { |
|
// %a - The abbreviated name of the day of the week. |
|
"%a": (date, fmt) => fmt("%A", date).slice(0, 3), |
|
// %A - The full name of the day of the week. |
|
"%A": (date, fmt) => days[date.getDay()], |
|
// %b - The abbreviated month name |
|
"%b": (date, fmt) => fmt("%B", date).slice(0, 3), |
|
// %B - The full month name. |
|
"%B": (date, fmt) => monthNames[date.getMonth() + 1], |
|
// %c - The preferred date and time representation for the current locale. |
|
"%c": (date, fmt) => fmt("%x %X", date), |
|
// %C - The century number (year/100) as a 2-digit integer. |
|
"%C": (date, fmt) => String(Math.floor(date.getFullYear() / 100)), |
|
// %d - The day of the month as a decimal number (range 01 to 31). |
|
"%d": (date, fmt) => zeroPad(date.getDate()), |
|
// %D - Equivalent to `%m/%d/%y`. |
|
"%D": (date, fmt) => fmt("%m/%d/%y", date), |
|
// %e - Like %d, the day of the month as a decimal number, but a leading zero is replaced by a space. |
|
"%e": (date, fmt) => fmt("%d", date).replace(/^[0]/, " "), |
|
// %E - Modifier (unsupported). |
|
"%E": (date, fmt) => unsupportedKey("%E"), |
|
// %F - Equivalent to %Y-%m-%d (the ISO 8601 date format). |
|
"%F": (date, fmt) => fmt("%Y-%m-%d", date), |
|
// %g - Like %G, but without century, that is, with a 2-digit year (00–99). |
|
"%g": (date, fmt) => unsupportedKey("%g"), |
|
// %G - The ISO 8601 week-based year with century as a decimal number. The 4-digit year corresponding to the ISO week number (see %V). This has the same format and value as %Y, except that if the ISO week number belongs to the previous or next year, that year is used instead. |
|
"%G": (date, fmt) => unsupportedKey("%G"), |
|
// %h - Equivalent to %b. |
|
"%h": (date, fmt) => fmt("%b", date), |
|
// %H - The hour as a decimal number using a 24-hour clock (range 00 to 23). |
|
"%H": (date, fmt) => zeroPad(date.getHours()), |
|
// %I - The hour as a decimal number using a 12-hour clock (range 01 to 12). |
|
"%I": (date, fmt) => zeroPad((date.getHours() + 11) % 12 + 1), |
|
// %j - The day of the year as a decimal number (range 001 to 366). |
|
"%j": (date, fmt) => { |
|
let days = date.getDate() + (daysPerMonth |
|
.slice(0, date.getMonth()) |
|
.reduce((total, days) => total + days, 0)); |
|
if (date.getMonth() > 1 && isLeapYear(date.getFullYear())) days += 1; |
|
return zeroPad(days, 3); |
|
}, |
|
// %k - The hour (24-hour clock) as a decimal number (range 0 to 23); single digits are preceded by a blank. |
|
"%k": (date, fmt) => fmt("%H", date).replace(/^[0]/, " "), |
|
// %l - The hour (12-hour clock) as a decimal number (range 1 to 12); single digits are preceded by a blank. |
|
"%l": (date, fmt) => fmt("%I", date).replace(/^[0]/, " "), |
|
// %m - The month as a decimal number (range 01 to 12). |
|
"%m": (date, fmt) => zeroPad(date.getMonth() + 1), |
|
// %M - The minute as a decimal number (range 00 to 59). |
|
"%M": (date, fmt) => zeroPad(date.getMinutes()), |
|
// %n - A newline character. |
|
"%n": (date, fmt) => "\n", |
|
// %O - Modifier (unsupported). |
|
"%O": (date, fmt) => unsupportedKey("%O"), |
|
// %p - Either "AM" or "PM" according to the given time value. Noon is treated as "PM" and midnight as "AM". |
|
"%p": (date, fmt) => date.getHours() < 12 ? "AM": "PM", |
|
// %P - Like %p but in lowercase: "am" or "pm". |
|
"%P": (date, fmt) => fmt("%p", date).toLowerCase(), |
|
// %r - The time in a.m. or p.m. notation. This is equivalent to `%I:%M:%S %p`. |
|
"%r": (date, fmt) => fmt("%I:%M:%S %p", date), |
|
// %R - The time in 24-hour notation (%H:%M). |
|
"%R": (date, fmt) => fmt("%H:%M", date), |
|
// %s - The number of seconds since the Epoch, 1970-01-01 00:00:00+0000 (UTC). |
|
"%s": (date, fmt) => String(date.getTime()), |
|
// %S - The second as a decimal number (range 00 to 60). |
|
"%S": (date, fmt) => zeroPad(value.getSeconds()), |
|
// %t - A tab character. |
|
"%t": (date, fmt) => "\t", |
|
// %T - The time in 24-hour notation (%H:%M:%S). |
|
"%T": (date, fmt) => fmt("%H:%M:%S", date), |
|
// %u - The day of the week as a decimal, range 1 to 7, Monday being 1. See also %w. |
|
"%u": (date, fmt) => String(date.getDay() + 1), |
|
// %U - The week number of the current year as a decimal number, range 00 to 53, starting with the first Sunday as the first day of week 01. See also %V and %W. |
|
"%U": (date, fmt) => unsupportedKey("%U"), |
|
// %V - The ISO 8601 week number (see NOTES) of the current year as a decimal number, range 01 to 53, where week 1 is the first week that has at least 4 days in the new year. See also %U and %W. |
|
"%V": (date, fmt) => unsupportedKey("%V"), |
|
// %w - The day of the week as a decimal, range 0 to 6, Sunday being 0. See also %u. |
|
"%w": (date, fmt) => String(date.getDay()), |
|
// %W - The week number of the current year as a decimal number, range 00 to 53, starting with the first Monday as the first day of week 01. |
|
"%W": (date, fmt) => unsupportedKey("%W"), |
|
// %x - The preferred date representation for en_US |
|
"%x": (date, fmt) => fmt("%m/%d/%y", date), |
|
// %X - The preferred time representation for en_US without the date. |
|
"%X": (date, fmt) => fmt("%H:%M:%S", date), |
|
// %y - The year as a decimal number without a century (range 00 to 99). |
|
"%y": (date, fmt) => zeroPad(date.getYear()), |
|
// %Y - The year as a decimal number including the century. |
|
"%Y": (date, fmt) => String(date.getFullYear()), |
|
// %z - The +hhmm or -hhmm numeric timezone (that is, the hour and minute offset from UTC). |
|
"%z": (date, fmt) => date.toTimeString().replace(/.+GMT([+-]\d+).+/, '$1'), |
|
// %Z - The timezone name or abbreviation. |
|
"%Z": (date, fmt) => date.toTimeString().replace(/.+\((.+?)\)$/, '$1'), |
|
// %+ - The date and time in date(1) format. |
|
"%+": (date, fmt) => unsupportedKey("%+"), |
|
// %% - A literal '%' character. |
|
"%%": (date, fmt) => "%" |
|
}; |
|
|
|
function fmt(formatString, value) { |
|
return formatString.replace(/%[a-z\%\+]/gi, (match) => { |
|
const convert = converters[match]; |
|
return (typeof convert == "function") ? convert(value, fmt) : match; |
|
}); |
|
} |
|
|
|
return fmt(formatString, value); |
|
} |