aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-11-05 13:25:09 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-11-05 13:25:09 +0900
commit248d64c096930fa031ce48ddaa4cdb30681918fa (patch)
treec211eb963ac5162cc37cd9f23f75516a28a20eff /src
parent2bb8d5dcda26a0dfeedfb067cf6cc248ed1dad1d (diff)
schedule: 日付解析を共通モジュールに切り出し
- src/lib/schedule-dates.ts に parseDates などを移動 - カレンダー(ブラウザ)とビルド時の月別ページの両方から使えるようにした - 挙動は変わっていない(カレンダーのマーク・終了判定・日別予定のスナップショット比較で一致)
Diffstat (limited to 'src')
-rw-r--r--src/components/ScheduleInteractive.astro113
-rw-r--r--src/lib/schedule-dates.ts130
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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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();
+}