import { changeCase, padStart, padEnd } from './underscore' import { LiquidDate } from './liquid-date' import type { Limiter } from './limiter' const rFormat = /%([-_0^#:]+)?(\d+)?([EO])?(.)/ interface FormatOptions { flags: Record; width?: string; modifier?: string; memoryLimit?: Pick; } // prototype extensions function daysInMonth (d: LiquidDate) { const feb = isLeapYear(d) ? 29 : 28 return [31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] } function getDayOfYear (d: LiquidDate) { let num = 0 for (let i = 0; i < d.getMonth(); ++i) { num += daysInMonth(d)[i] } return num + d.getDate() } function getWeekOfYear (d: LiquidDate, startDay: number) { // Skip to startDay of this week const now = getDayOfYear(d) + (startDay - d.getDay()) // Find the first startDay of the year const jan1 = new Date(d.getFullYear(), 0, 1) const then = (7 - jan1.getDay() + startDay) return String(Math.floor((now - then) / 7) + 1) } function isLeapYear (d: LiquidDate) { const year = d.getFullYear() return !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year))) } function ordinal (d: LiquidDate) { const date = d.getDate() if ([11, 12, 13].includes(date)) return 'th' switch (date % 10) { case 1: return 'st' case 2: return 'nd' case 3: return 'rd' default: return 'th' } } function century (d: LiquidDate) { return parseInt(d.getFullYear().toString().substring(0, 2), 10) } // default to 0 const padWidths: Record = { d: 2, e: 2, H: 2, I: 2, j: 3, k: 2, l: 2, L: 3, m: 2, M: 2, S: 2, U: 2, W: 2 } const padSpaceChars = new Set('aAbBceklpP') function getTimezoneOffset (d: LiquidDate, opts: FormatOptions) { const nOffset = Math.abs(d.getTimezoneOffset()) const h = Math.floor(nOffset / 60) const m = nOffset % 60 return (d.getTimezoneOffset() > 0 ? '-' : '+') + padStart(h, 2, '0') + (opts.flags[':'] ? ':' : '') + padStart(m, 2, '0') } type FormatCodeHandler = (d: LiquidDate, opts: FormatOptions) => unknown const formatCodes: Record = { a: (d: LiquidDate) => d.getShortWeekdayName(), A: (d: LiquidDate) => d.getLongWeekdayName(), b: (d: LiquidDate) => d.getShortMonthName(), B: (d: LiquidDate) => d.getLongMonthName(), c: (d: LiquidDate) => d.toLocaleString(), C: (d: LiquidDate) => century(d), d: (d: LiquidDate) => d.getDate(), e: (d: LiquidDate) => d.getDate(), H: (d: LiquidDate) => d.getHours(), I: (d: LiquidDate) => String(d.getHours() % 12 || 12), j: (d: LiquidDate) => getDayOfYear(d), k: (d: LiquidDate) => d.getHours(), l: (d: LiquidDate) => String(d.getHours() % 12 || 12), L: (d: LiquidDate) => d.getMilliseconds(), m: (d: LiquidDate) => d.getMonth() + 1, M: (d: LiquidDate) => d.getMinutes(), N: (d: LiquidDate, opts: FormatOptions) => { const width = Number(opts.width) || 9 const str = padStart(String(d.getMilliseconds()), 3, '0').slice(0, width) opts.memoryLimit?.use(width - str.length) return padEnd(str, width, '0') }, p: (d: LiquidDate) => (d.getHours() < 12 ? 'AM' : 'PM'), P: (d: LiquidDate) => (d.getHours() < 12 ? 'am' : 'pm'), q: (d: LiquidDate) => ordinal(d), s: (d: LiquidDate) => Math.round(d.getTime() / 1000), S: (d: LiquidDate) => d.getSeconds(), u: (d: LiquidDate) => d.getDay() || 7, U: (d: LiquidDate) => getWeekOfYear(d, 0), w: (d: LiquidDate) => d.getDay(), W: (d: LiquidDate) => getWeekOfYear(d, 1), x: (d: LiquidDate) => d.toLocaleDateString(), X: (d: LiquidDate) => d.toLocaleTimeString(), y: (d: LiquidDate) => d.getFullYear().toString().slice(2, 4), Y: (d: LiquidDate) => d.getFullYear(), z: getTimezoneOffset, Z: (d: LiquidDate, opts: FormatOptions) => d.getTimeZoneName() || getTimezoneOffset(d, opts), 't': () => '\t', 'n': () => '\n', '%': () => '%' } formatCodes.h = formatCodes.b export function strftime (d: LiquidDate, formatStr: string, memoryLimit?: Pick) { let output = '' let remaining = formatStr let match while ((match = rFormat.exec(remaining))) { output += remaining.slice(0, match.index) remaining = remaining.slice(match.index + match[0].length) output += format(d, match, memoryLimit) } return output + remaining } function format (d: LiquidDate, match: RegExpExecArray, memoryLimit?: Pick) { const [input, flagStr = '', width, modifier, conversion] = match const convert = formatCodes[conversion] if (!convert) return input const flags: Record = {} for (const flag of flagStr) flags[flag] = true let ret = String(convert(d, { flags, width, modifier, memoryLimit })) let padChar = padSpaceChars.has(conversion) ? ' ' : '0' let padWidth = Number(width) || padWidths[conversion] || 0 if (flags['^']) ret = ret.toUpperCase() else if (flags['#']) ret = changeCase(ret) if (flags['_']) padChar = ' ' else if (flags['0']) padChar = '0' if (flags['-']) padWidth = 0 memoryLimit?.use(Number(padWidth) - ret.length) return padStart(ret, padWidth, padChar) }