const DAY_ABBR = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as const
const MONTH_NAMES = [ '', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ] as const
function toCalendarInteger(value: unknown, functionName: string): number { if (typeof value === 'boolean') { return value ? 1 : 0 }
if (typeof value === 'bigint') { const numericValue = Number(value) if (!Number.isSafeInteger(numericValue)) { throw new RangeError(`${functionName}() integer arguments must fit within JS safe integer precision`) }
return numericValue }
if (typeof value === 'number' && Number.isFinite(value) && Number.isSafeInteger(value)) { return value }
throw new TypeError(`${functionName}() requires integer arguments`) }
function normalizeCalendarYear(value: unknown, functionName: string): number { return toCalendarInteger(value, functionName) }
function normalizeCalendarMonth(value: unknown, functionName: string): number { const month = toCalendarInteger(value, functionName) if (month < 1 || month > 12) { throw new RangeError('bad month number') }
return month }
function normalizeMonthWidth(value: unknown, functionName: string): number { return Math.max(toCalendarInteger(value, functionName), 2) }
function normalizeLineSpacing(value: unknown, functionName: string): number { return Math.max(toCalendarInteger(value, functionName), 1) }
function isLeapYear(year: number): boolean { return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) }
function daysInMonth(year: number, month: number): number { if (month === 2) { return isLeapYear(year) ? 29 : 28 }
return [4, 6, 9, 11].includes(month) ? 30 : 31 }
function pythonWeekday(year: number, month: number, day: number): number { const ordinal = daysBeforeYear(year) + daysBeforeMonth(year, month) + day return positiveModulo(ordinal - 1, 7) }
function buildWeekHeader(width: number): string { return DAY_ABBR.map((name) => { const truncated = name.slice(0, width) return width > truncated.length ? centerText(truncated, width) : truncated }).join(' ') }
function buildMonthCalendar(year: number, month: number): number[][] { const totalDays = daysInMonth(year, month) const firstWeekday = pythonWeekday(year, month, 1) const weeks: number[][] = []
let currentWeek = new Array<number>(7).fill(0) let weekdayIndex = firstWeekday
for (let day = 1; day <= totalDays; day += 1) { currentWeek[weekdayIndex] = day weekdayIndex += 1
if (weekdayIndex === 7) { weeks.push(currentWeek) currentWeek = new Array<number>(7).fill(0) weekdayIndex = 0 } }
if (weekdayIndex !== 0) { weeks.push(currentWeek) }
return weeks }
function formatWeek(theweek: unknown, width: unknown): string { if (!Array.isArray(theweek)) { throw new TypeError("'int' object is not iterable") }
const rawWidth = toCalendarInteger(width, 'week') return theweek .map((entry) => { if (!Array.isArray(entry)) { throw new TypeError("'int' object is not iterable") }
const day = entry[0] if (typeof day !== 'number' || !Number.isInteger(day)) { throw new TypeError("'int' object is not iterable") }
if (day === 0) { return centerText('', rawWidth) }
return centerText(String(day).padStart(2, ' '), rawWidth) }) .join(' ') }
function formatMonth(year: number, month: number, width: number, lineSpacing: number): string { const monthWidth = 7 * (width + 1) - 1 const header = centerText(`${MONTH_NAMES[month]} ${year}`, monthWidth) const weekHeader = buildWeekHeader(width) const weeks = formatMonthWeeks(year, month, width)
return joinCalendarLines([header, weekHeader, ...weeks], lineSpacing) }
function daysBeforeYear(year: number): number { const adjusted = year - 1 return adjusted * 365 + Math.floor(adjusted / 4) - Math.floor(adjusted / 100) + Math.floor(adjusted / 400) }
function daysBeforeMonth(year: number, month: number): number { const offsets = isLeapYear(year) ? [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335] : [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334] return offsets[month - 1] ?? 0 }
function centerText(value: string, width: number): string { if (width <= value.length) { return value }
const leftPadding = Math.floor((width - value.length) / 2) const rightPadding = width - value.length - leftPadding return `${' '.repeat(leftPadding)}${value}${' '.repeat(rightPadding)}` }
function positiveModulo(value: number, modulus: number): number { return ((value % modulus) + modulus) % modulus }
function joinCalendarLines(lines: string[], lineSpacing: number): string { return `${lines.map((line) => line.replace(/\s+$/, '')).join('\n'.repeat(lineSpacing))}\n` }
function formatMonthWeeks(year: number, month: number, width: number): string[] { return buildMonthCalendar(year, month).map((week) => formatWeek( week.map((day, weekday) => [day, weekday]), width, ), ) }
function month(theyear: unknown, themonth: unknown, w = 0, l = 0): string {
if (arguments.length === 0) { throw new TypeError("TextCalendar.formatmonth() missing 2 required positional arguments: 'theyear' and 'themonth'") } if (arguments.length === 1) { throw new TypeError("TextCalendar.formatmonth() missing 1 required positional argument: 'themonth'") }
return formatMonth( normalizeCalendarYear(theyear, 'month'), normalizeCalendarMonth(themonth, 'month'), normalizeMonthWidth(w, 'month'), normalizeLineSpacing(l, 'month'), ) }
|