+/**
+ * Why is rendering a Date as a string this complicated?
+ * We handle the infinities because pg-promise might provide those in
+ * lieu of a Date object from timestamp fields.
+ * @param {Date|Number|String} date
+ * @param {String=} pInf
+ * @param {String=} nInf
+ * @param {String=} otherwise
+ */
+const dateFormat = (date, pInf = 'Never', nInf = 'Forever', otherwise = '') => {
+ const isDatableType = ['number', 'string'].includes(typeof date);
+ switch (date) {
+ case Infinity:
+ return pInf;
+ case -Infinity:
+ return nInf;
+ default:
+ if (!date
+ || Number.isNaN(date.valueOf())
+ || (!(date instanceof Date) && !isDatableType)) {
+ return otherwise;
+ }
+ }
+ if (isDatableType) {
+ date = new Date(date);
+ }
+ const parts = dateFormat._dtf.formatToParts(date);
+ return parts.map((p) => p.value).join('');
+};
+lazy(dateFormat, '_dtf', () => {
+ const dateTimeFormatOptions = {
+ dateStyle: 'medium',
+ timeStyle: 'long',
+ };
+ return new Intl.DateTimeFormat(undefined, dateTimeFormatOptions);
+});
+
+
+/**
+ * Wrap a Date in a <time> block.
+ * @param {Date} date
+ * @param {Object} options
+ * @param {String=} options.title
+ */
+const timeElement = (date, options = {}) => {
+ const {
+ title,
+ pInf,
+ nInf,
+ otherwise,
+ } = options;
+ const attributes = {
+ ...(title && { title }),
+ ...(date instanceof Date && { datetime: date.toISOString() }),
+ };
+ return [
+ '<time',
+ elementAttributes(attributes),
+ '>',
+ dateFormat(date, pInf, nInf, otherwise),
+ '</time>',
+ ].join('');
+}
+
+