Skip to main content
Glama
lis186

Taiwan Holiday MCP Server

by lis186
date-parser.ts7.01 kB
/** * 日期解析和驗證工具 * * 支援多種日期格式的解析、驗證和轉換功能 */ import { DateFormat, ErrorType, SUPPORTED_YEAR_RANGE } from '../types.js'; /** * 日期解析結果介面 */ export interface ParsedDate { /** 年份 */ year: number; /** 月份 (1-12) */ month: number; /** 日期 (1-31) */ day: number; /** 原始輸入 */ original: string; /** 標準化格式 (YYYYMMDD) */ normalized: string; } /** * 日期驗證錯誤 */ export class DateParseError extends Error { constructor( message: string, public readonly type: ErrorType, public readonly input: string ) { super(message); this.name = 'DateParseError'; } } /** * 日期格式正則表達式 */ const DATE_PATTERNS = { 'YYYYMMDD': /^(\d{4})(\d{2})(\d{2})$/, 'YYYY-MM-DD': /^(\d{4})-(\d{2})-(\d{2})$/, 'YYYY/MM/DD': /^(\d{4})\/(\d{2})\/(\d{2})$/ } as const; /** * 每月天數對照表(非閏年) */ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; /** * 檢查是否為閏年 */ export function isLeapYear(year: number): boolean { return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0); } /** * 取得指定年月的天數 */ export function getDaysInMonth(year: number, month: number): number { if (month === 2 && isLeapYear(year)) { return 29; } return DAYS_IN_MONTH[month - 1]; } /** * 驗證年份是否在支援範圍內 */ export function validateYear(year: number): void { if (year < SUPPORTED_YEAR_RANGE.start || year > SUPPORTED_YEAR_RANGE.end) { throw new DateParseError( `年份 ${year} 超出支援範圍 (${SUPPORTED_YEAR_RANGE.start}-${SUPPORTED_YEAR_RANGE.end})`, ErrorType.INVALID_YEAR, year.toString() ); } } /** * 驗證月份是否有效 */ export function validateMonth(month: number): void { if (month < 1 || month > 12) { throw new DateParseError( `無效的月份: ${month},月份必須在 1-12 之間`, ErrorType.INVALID_MONTH, month.toString() ); } } /** * 驗證日期是否有效 */ export function validateDay(year: number, month: number, day: number): void { if (day < 1) { throw new DateParseError( `無效的日期: ${day},日期必須大於 0`, ErrorType.INVALID_DATE, day.toString() ); } const maxDays = getDaysInMonth(year, month); if (day > maxDays) { throw new DateParseError( `無效的日期: ${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')},${month} 月最多只有 ${maxDays} 天`, ErrorType.INVALID_DATE, `${year}${month.toString().padStart(2, '0')}${day.toString().padStart(2, '0')}` ); } } /** * 自動偵測日期格式 */ export function detectDateFormat(dateString: string): DateFormat | null { for (const [format, pattern] of Object.entries(DATE_PATTERNS)) { if (pattern.test(dateString)) { return format as DateFormat; } } return null; } /** * 解析日期字串 */ export function parseDate(dateString: string, expectedFormat?: DateFormat): ParsedDate { if (!dateString || typeof dateString !== 'string') { throw new DateParseError( '日期字串不能為空', ErrorType.INVALID_DATE, String(dateString) ); } const trimmedDate = dateString.trim(); // 如果指定了格式,只使用該格式 if (expectedFormat) { const pattern = DATE_PATTERNS[expectedFormat]; const match = pattern.exec(trimmedDate); if (!match) { throw new DateParseError( `日期格式不符合預期格式 ${expectedFormat}: ${trimmedDate}`, ErrorType.INVALID_DATE, trimmedDate ); } return createParsedDate(match, trimmedDate); } // 自動偵測格式 const detectedFormat = detectDateFormat(trimmedDate); if (!detectedFormat) { throw new DateParseError( `無法識別的日期格式: ${trimmedDate},支援的格式: YYYYMMDD, YYYY-MM-DD, YYYY/MM/DD`, ErrorType.INVALID_DATE, trimmedDate ); } const pattern = DATE_PATTERNS[detectedFormat]; const match = pattern.exec(trimmedDate)!; return createParsedDate(match, trimmedDate); } /** * 建立解析結果物件 */ function createParsedDate(match: RegExpExecArray, original: string): ParsedDate { const year = parseInt(match[1], 10); const month = parseInt(match[2], 10); const day = parseInt(match[3], 10); // 驗證各個部分 validateYear(year); validateMonth(month); validateDay(year, month, day); const normalized = `${year}${month.toString().padStart(2, '0')}${day.toString().padStart(2, '0')}`; return { year, month, day, original, normalized }; } /** * 將日期轉換為指定格式 */ export function formatDate(date: ParsedDate, format: DateFormat): string { const { year, month, day } = date; const monthStr = month.toString().padStart(2, '0'); const dayStr = day.toString().padStart(2, '0'); switch (format) { case 'YYYYMMDD': return `${year}${monthStr}${dayStr}`; case 'YYYY-MM-DD': return `${year}-${monthStr}-${dayStr}`; case 'YYYY/MM/DD': return `${year}/${monthStr}/${dayStr}`; default: throw new DateParseError( `不支援的日期格式: ${format}`, ErrorType.INVALID_DATE, format ); } } /** * 驗證日期字串是否有效 */ export function isValidDate(dateString: string, expectedFormat?: DateFormat): boolean { try { parseDate(dateString, expectedFormat); return true; } catch { return false; } } /** * 取得今天的日期(台北時區) */ export function getTodayInTaipei(): ParsedDate { const now = new Date(); // 轉換為台北時區 (UTC+8) const taipeiTime = new Date(now.getTime() + (8 * 60 * 60 * 1000)); const year = taipeiTime.getUTCFullYear(); const month = taipeiTime.getUTCMonth() + 1; const day = taipeiTime.getUTCDate(); const normalized = `${year}${month.toString().padStart(2, '0')}${day.toString().padStart(2, '0')}`; return { year, month, day, original: normalized, normalized }; } /** * 比較兩個日期 * @returns -1 如果 date1 < date2, 0 如果相等, 1 如果 date1 > date2 */ export function compareDates(date1: ParsedDate, date2: ParsedDate): number { if (date1.year !== date2.year) { return date1.year < date2.year ? -1 : 1; } if (date1.month !== date2.month) { return date1.month < date2.month ? -1 : 1; } if (date1.day !== date2.day) { return date1.day < date2.day ? -1 : 1; } return 0; } /** * 計算兩個日期之間的天數差 */ export function daysBetween(startDate: ParsedDate, endDate: ParsedDate): number { const start = new Date(startDate.year, startDate.month - 1, startDate.day); const end = new Date(endDate.year, endDate.month - 1, endDate.day); const diffTime = end.getTime() - start.getTime(); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lis186/taiwan-holiday-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server