Skip to content

Instantly share code, notes, and snippets.

@mattijs
Last active June 8, 2020 15:59
Show Gist options
  • Save mattijs/7d13502078da448ee5bd837526a2ef77 to your computer and use it in GitHub Desktop.
Save mattijs/7d13502078da448ee5bd837526a2ef77 to your computer and use it in GitHub Desktop.
strftime for JavaScript Dates

strftime

Selective implementation of strftime

This is an implementation of Unix strftime for JavaScript Dates supporting only American English (en_US). Not all format options are implemented.

I only had use cases for formatting in American English (en_US) for this function and most conversion characters are also implemented based on use cases I had.

It would be possible to adapt this to support a different language or multiple languages but I have no use cases for this. Feel free to use this code any way you like. I just copy this into projects and adapt it to the project specific needs, this is just a capture of the code at a certain point in time.

Other Implementations

/**
* 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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment