diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/ScheduleInteractive.astro | 113 | ||||
| -rw-r--r-- | src/lib/schedule-dates.ts | 130 |
2 files changed, 131 insertions, 112 deletions
diff --git a/src/components/ScheduleInteractive.astro b/src/components/ScheduleInteractive.astro index 8c14f38..3dd6048 100644 --- a/src/components/ScheduleInteractive.astro +++ b/src/components/ScheduleInteractive.astro @@ -1,6 +1,7 @@ <script> // URLは src/data/schedule/config.ts で一元管理している(ページ移転時はそこだけ変える) import { scheduleAbsoluteUrl } from '@/data/schedule/config'; + import { parseDates, applyExclusions, toKey, keyToDate } from '@/lib/schedule-dates'; const CATEGORY_DEFS = [ { icon: '👶', label: '子育て' }, @@ -23,121 +24,9 @@ const isLibrary = (e: { source?: string }) => (e.source ?? '').includes('library.kodaira.ed.jp/viewer/info.html'); const WEEKDAYS = ['日', '月', '火', '水', '木', '金', '土']; - const WEEKDAY_MAP = { 日: 0, 月: 1, 火: 2, 水: 3, 木: 4, 金: 5, 土: 6 }; - - const normalizeDigits = (s) => - s.replace(/[0-9]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xfee0)); - const pad2 = (n) => String(n).padStart(2, '0'); - const toKey = (y, m, d) => `${y}-${pad2(m)}-${pad2(d)}`; - const keyToDate = (key) => { - const [y, m, d] = key.split('-').map(Number); - return new Date(y, m - 1, d); - }; const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); - function applyExclusions(dateSet, raw) { - 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日以内なら全日、曜日指定があればその曜日、それ以外は開始日と終了日を拾う。 - function parseDates(raw) { - const text = normalizeDigits(raw); - const dates = new Set(); - const add = (y, m, d) => { - if (y && m >= 1 && m <= 12 && d >= 1 && d <= 31) dates.add(toKey(y, m, d)); - }; - const yearFor = (index, month) => { - 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 - start) / 86400000) + 1; - const after = text.slice(m.index + m[0].length); - let weekdays = 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 = []; - 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 = 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(); - } - function initSchedule() { const calendarEl = document.getElementById('schedule-calendar'); const detailsEl = document.getElementById('schedule-day-details'); diff --git a/src/lib/schedule-dates.ts b/src/lib/schedule-dates.ts new file mode 100644 index 0000000..5f0900b --- /dev/null +++ b/src/lib/schedule-dates.ts @@ -0,0 +1,130 @@ +// 行事予定の日付解析(ブラウザとビルド時の両方で使う)。 +// +// 日付セルには「11月1日(日)」「11月1日・18日、12月6日」「10月24日(土)〜11月22日(日)」 +// 「11月2日〜12月14日の月曜日」など、さまざまな書き方がある。 +// ここでは、それらから YYYY-MM-DD を組み立てる。 + +export const WEEKDAY_MAP: Record<string, number> = { + 日: 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<string>, 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<string>(); + 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(); +} |
