Skip to content

Instantly share code, notes, and snippets.

@westc
Last active April 19, 2024 04:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save westc/4d4bf1d11d0ce074053c7d530d67d7d6 to your computer and use it in GitHub Desktop.
Save westc/4d4bf1d11d0ce074053c7d530d67d7d6 to your computer and use it in GitHub Desktop.
Makes it possible to format a Date object while leveraging the Intl.DateTimeFormat formatter.
function formatIntlDate(date, format, options) {
return format.replace(
/<<|>>|<(\w)(?:\|([^>]+))?>/g,
(fullMatch, type, option) => {
if (!type) return fullMatch.charAt(0);
if (/^[YMDHhmsf]$/.test(type ?? '')) option ??= 'numeric';
const subOptions = {timeZone: options?.timeZone ?? undefined};
let partType;
if (type === 'Y') {
partType = 'year';
subOptions.year = option;
}
else if (type === 'M') {
partType = 'month';
subOptions.month = option;
}
else if (type === 'D') {
partType = (option === 'numeric' || option === '2-digit') ? 'day' : 'weekday';
subOptions[partType] = option;
}
else if (type === 'H' || type === 'h') {
subOptions.hour = option;
partType = 'hour';
subOptions.hourCycle ??= 'h23';
}
else if (type === 'm') {
subOptions.hour = 'numeric'; // Hack to allow 2-digit minute to work.
subOptions.minute = option;
partType = 'minute';
}
else if (type === 's') {
subOptions.minute = 'numeric'; // Hack to allow 2-digit second to work.
subOptions.second = option;
partType = 'second';
}
else if (type === 'f') {
partType = 'fractionalSecond';
subOptions.fractionalSecondDigits = option === 'numeric' ? 3 : +option.charAt(0);
}
else if (type === 'P' || type === 'p') {
partType = 'hour';
subOptions.hour ??= 'numeric';
subOptions.hourCycle ??= 'h23';
}
else throw new Error(`Unrecognized format: ${fullMatch}`);
let output;
for (const part of new Intl.DateTimeFormat(options?.locale, subOptions).formatToParts(date)) {
if (part.type === partType) {
output = part.value;
break;
}
}
if (type === 'f' && option === 'numeric') return parseInt(output, 10);
if (type === 'h') {
output = (parseInt(output, 10) % 12) || 12;
return option === '2-digit' ? `0${output}` : output;
}
if (type === 'P' || type === 'p') {
output = parseInt(output, 10) < 12 ? 'am' : 'pm';
if (type === 'P') output = output.toUpperCase();
return option === 'period' ? output.replace(/./g, '$&.') : output;
}
return output;
}
);
}
const paramSets = [
{
format: 'It is <D|long>, <M|long> <D>, <Y> at <h>:<m|2-digit>:<s|2-digit><P> (<f|numeric>ms).',
options: {
timeZone: 'America/La_Paz',
locale: 'en-US'
}
},
{
format: 'Hoy es <D|long>, el <D> de <M|long>, <Y> a las <h>:<m|2-digit>:<s|2-digit><P> (<f|numeric>ms).',
options: {
timeZone: 'America/La_Paz',
locale: 'es'
}
},
{
format: 'Hoy es <D|long>, el <D> de <M|long>, <Y> a las <h>:<m|2-digit>:<s|2-digit><P> (<f|numeric>ms) en Adelaida de Australia.',
options: {
timeZone: 'Australia/Adelaide',
locale: 'es'
}
}
];
for (params of paramSets) {
console.log(formatIntlDate(new Date, params.format, params.options), params);
}
@westc
Copy link
Author

westc commented Apr 19, 2024

Working on this:

(() => {
  const codeToOpts = [...'Y=year M=month D=day H=hour h=hour m=minute s=second S=fractionalSecondDigits:1 SS=fractionalSecondDigits:2 SSS=fractionalSecondDigits:3 MMM=month:short MMMM=month:long DDD=weekday:short DDDD=weekday:long'.replace(/(\w)=\w+(?= |$)/g, '$&:numeric $1$&:2-digit').replace(/hour/g, 'hourCycle:h23,$&').matchAll(/(\w+)=(\S+)/g)]
    .reduce(
      (codeToOpts, [_, code, strOpts]) => {
        codeToOpts[code] = [...strOpts.matchAll(/(\w+):([^,]+)/g)].reduce(
          (opts, [_, key, value]) => {
            opts[key] = value;
            return opts;
          },
          {}
        );
        return codeToOpts;
      },
      {}
    );

  function formatDate(date, format, options) {
    const {locale, timeZone} = Object(options);
    return format.replace(
      /("|')((?:[^\1\\]+|\\.)*)\1|[YHhms]{1,2}|[DM]{1,4}|S{1,3}/g,
      (pattern, strDelim, strInnards) => {
        if (strDelim) {
          return strInnards.replace(/\\(.)/g, '$1');
        }

        let {value} = new Intl.DateTimeFormat(locale, {...codeToOpts[pattern], timeZone})
          .formatToParts(date)
          .find((v) => codeToOpts[pattern][v.type]);
        if (pattern === 'h' || pattern === 'hh') {
          value = value % 12 || 12;
          if (pattern === 'hh' && value < 10) value = '0' + value;
        }
        return value;
      }
    );
  }

  console.log(
    formatDate(new Date, 'M/D/Y H'),
    formatDate(new Date, 'Y-MM-DD h "(Y-MM-DD h)"')
  );
})();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment