// 行事予定の日付解析(ブラウザとビルド時の両方で使う)。 // // 日付セルには「11月1日(日)」「11月1日・18日、12月6日」「10月24日(土)〜11月22日(日)」 // 「11月2日〜12月14日の月曜日」など、さまざまな書き方がある。 // ここでは、それらから YYYY-MM-DD を組み立てる。 export const WEEKDAY_MAP: Record = { 日: 0, 月: 1, 火: 2, 水: 3, 木: 4, 金: 5, 土: 6, }; export const normalizeDigits = (s: string) => s.replace(/[0-9]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xfee0)); export const pad2 = (n: number) => String(n).padStart(2, '0'); export const toKey = (y: number, m: number, d: number) => `${y}-${pad2(m)}-${pad2(d)}`; export const keyToDate = (key: string) => { const [y, m, d] = key.split('-').map(Number); return new Date(y, m - 1, d); }; /** 「(11月4日を除く)」などの除外日を、日付の集合から取り除く */ export function applyExclusions(dateSet: Set, raw: string) { const text = normalizeDigits(raw); const re = /(\d{1,2})月(\d{1,2})日を除く/g; let m; while ((m = re.exec(text)) !== null) { const month = Number(m[1]); const day = Number(m[2]); const year = text.slice(0, m.index).includes('令和9年') ? 2027 : month === 1 ? 2027 : 2026; dateSet.delete(toKey(year, month, day)); } } // 日付セル・時間セルから YYYY-MM-DD を抽出する。 // 期間は、7日以内なら全日、曜日指定があればその曜日、それ以外は開始日と終了日を拾う。 export function parseDates(raw: string): string[] { const text = normalizeDigits(raw); const dates = new Set(); const add = (y: number, m: number, d: number) => { if (y && m >= 1 && m <= 12 && d >= 1 && d <= 31) dates.add(toKey(y, m, d)); }; const yearFor = (index: number, month: number) => { if (text.slice(0, index).includes('令和9年')) return 2027; return month === 1 ? 2027 : 2026; }; let m; const rangeRe = /(\d{1,2})月(\d{1,2})日[^〜]*〜\s*(?:(\d{1,2})月)?(\d{1,2})日/g; while ((m = rangeRe.exec(text)) !== null) { const startMonth = Number(m[1]); const startDay = Number(m[2]); const endMonth = m[3] ? Number(m[3]) : startMonth; const endDay = Number(m[4]); const startYear = yearFor(m.index, startMonth); const endYear = startYear + (endMonth < startMonth ? 1 : 0); const start = new Date(startYear, startMonth - 1, startDay); const end = new Date(endYear, endMonth - 1, endDay); if (end < start) continue; const lengthDays = Math.round((end.getTime() - start.getTime()) / 86400000) + 1; const after = text.slice(m.index + m[0].length); let weekdays: number[] | null = null; const noWeek = after.match(/の([月火水木金土日])曜日/); if (noWeek) { weekdays = [WEEKDAY_MAP[noWeek[1]]]; } else { const group = after.match(/(([月火水木金土日](?:・[月火水木金土日])*))/); const hasStartWeekday = new RegExp(`${startMonth}月${startDay}日([月火水木金土日]`).test(text); if (group && (group[1].includes('・') || !hasStartWeekday)) { weekdays = group[1].split('・').map((w) => WEEKDAY_MAP[w]); } else { const startWeek = text.match(new RegExp(`${startMonth}月${startDay}日(([月火水木金土日])`)); const endWeek = text.match(new RegExp(`(?:${endMonth}月)?${endDay}日(([月火水木金土日])`)); if (startWeek && endWeek && startWeek[1] === endWeek[1]) { weekdays = [WEEKDAY_MAP[startWeek[1]]]; } } } if (lengthDays <= 7 || (!weekdays && lengthDays <= 14)) { for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { add(d.getFullYear(), d.getMonth() + 1, d.getDate()); } } else if (weekdays) { for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { if (weekdays.includes(d.getDay())) add(d.getFullYear(), d.getMonth() + 1, d.getDate()); } } else { add(start.getFullYear(), start.getMonth() + 1, start.getDate()); add(end.getFullYear(), end.getMonth() + 1, end.getDate()); } } const fullDates: { index: number; month: number }[] = []; const fullRe = /(\d{1,2})月(\d{1,2})日/g; while ((m = fullRe.exec(text)) !== null) { const month = Number(m[1]); const day = Number(m[2]); fullDates.push({ index: m.index, month }); add(yearFor(m.index, month), month, day); } const bareRe = /(\d{1,2})日/g; while ((m = bareRe.exec(text)) !== null) { const index = m.index; if (index > 0 && text[index - 1] === '月') continue; let previous: { index: number; month: number } | null = null; for (const fd of fullDates) { if (fd.index < index) previous = fd; else break; } if (!previous) continue; const day = Number(m[1]); add(yearFor(index, previous.month), previous.month, day); } // 「(11月4日を除く)」などの除外日を反映する。 applyExclusions(dates, text); return [...dates].sort(); } /** * 行事予定1行分の開催日を求める。 * - 会議:日付セルに加えて、時間・会場セル内の日付も拾う(「毎月1回(11月12日も開催)」など) * - イベント:時間セルの「(11月4日を除く)」を反映する * カレンダー(ブラウザ)と月別アーカイブ(ビルド時)で同じ結果になるよう、ここに集約する。 */ export function entryDates( type: 'meeting' | 'event', dateText: string, placeText: string, ): string[] { const dates = new Set(parseDates(dateText)); if (type === 'meeting') { for (const key of parseDates(placeText)) dates.add(key); } else { applyExclusions(dates, placeText); } return [...dates].sort(); }