aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--scripts/post-x-events.mjs82
1 files changed, 79 insertions, 3 deletions
diff --git a/scripts/post-x-events.mjs b/scripts/post-x-events.mjs
index 0465cac..0f05302 100644
--- a/scripts/post-x-events.mjs
+++ b/scripts/post-x-events.mjs
@@ -9,6 +9,9 @@
// node scripts/post-x-events.mjs --check # 認証情報が通るか確認する(投稿しない)
// node scripts/post-x-events.mjs --no-reply # X_REPLY_TO_ID を無視して通常のポストにする
// node scripts/post-x-events.mjs --days 10 # 10日分の予定をまとめて投稿(既定は7日分)
+// node scripts/post-x-events.mjs --at 18:00 # 終了判定の時刻を指定(動作確認用)
+//
+// 今日の分は、終了時刻を過ぎた予定を除く(終了時刻が読めないものは残す)。
//
// 週1回の運転:
// 毎日タスクを動かしても、前回から X_POST_INTERVAL_DAYS 日(未設定なら --days の値)
@@ -40,7 +43,7 @@ import { createHmac, randomBytes } from 'node:crypto';
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
-import { entryDates, toKey } from '../src/lib/schedule-dates.ts';
+import { entryDates, normalizeDigits, toKey } from '../src/lib/schedule-dates.ts';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const LOG_DIR = join(root, 'logs');
@@ -75,6 +78,17 @@ const target = dateArg
const todayKey = toKey(target.getFullYear(), target.getMonth() + 1, target.getDate());
const WEEKDAYS = ['日', '月', '火', '水', '木', '金', '土'];
const dateLabel = `${target.getMonth() + 1}月${target.getDate()}日(${WEEKDAYS[target.getDay()]})`;
+// 今日の分だけ「すでに終了した予定」を除くために使う(--at で時刻を差し替えられる)
+const atArg = valueOf('--at');
+const nowMinutes = (() => {
+ if (atArg) {
+ const [h, m] = atArg.split(':').map(Number);
+ if (Number.isFinite(h)) return h * 60 + (Number.isFinite(m) ? m : 0);
+ }
+ return now.getHours() * 60 + now.getMinutes();
+})();
+const isTodayTarget =
+ toKey(now.getFullYear(), now.getMonth() + 1, now.getDate()) === todayKey;
// ---------------------------------------------------------------- 予定の抽出
@@ -117,6 +131,53 @@ function venueOf(placeText) {
return parts[parts.length - 1].replace(/([^)]*)/g, '').trim();
}
+/**
+ * 時間・会場セルから開始・終了時刻(0時からの分)を読む。
+ * 読み取れないときは null。終了時刻が「未定」のものは unknown を立てて残す。
+ */
+function timeRange(placeText) {
+ const text = normalizeDigits(stripTags(placeText));
+ const unknown = /未定|終日/.test(text);
+ const startMatch = text.match(/(午前|午後)?\s*(\d{1,2})時(?:(\d{1,2})分|半)?/);
+ if (!startMatch) return { start: null, end: null, unknown };
+
+ const toMinutes = (meridiem, hourText, minuteText, whole) => {
+ let hour = Number(hourText);
+ if (meridiem === '午後' && hour < 12) hour += 12;
+ if (meridiem === '午前' && hour === 12) hour = 0;
+ const minute = minuteText ? Number(minuteText) : whole.includes('半') ? 30 : 0;
+ return hour * 60 + minute;
+ };
+
+ const start = toMinutes(startMatch[1], startMatch[2], startMatch[3], startMatch[0]);
+ const rest = text.slice(startMatch.index + startMatch[0].length);
+ const startMeridiem = startMatch[1] ?? null;
+
+ // 「正午まで」
+ if (/正午まで/.test(rest)) return { start, end: 12 * 60, unknown };
+
+ // 「午後3時まで」「午後3時45分まで」
+ const until = rest.match(/(?:午前|午後)?\s*(\d{1,2})時(?:(\d{1,2})分|半)?\s*まで/);
+ if (until) {
+ return { start, end: toMinutes(startMeridiem, until[1], until[2], until[0]), unknown };
+ }
+
+ // 「から午後3時」「〜3時」
+ const range = rest.match(/(?:から|〜|~|[-−—])\s*(午前|午後)?\s*(\d{1,2})時(?:(\d{1,2})分|半)?/);
+ if (range) {
+ const meridiem = range[1] ?? startMeridiem;
+ return { start, end: toMinutes(meridiem, range[2], range[3], range[0]), unknown };
+ }
+
+ // 「午後1時ごろ解散」
+ const dismiss = rest.match(/(?:午前|午後)?\s*(\d{1,2})時(?:(\d{1,2})分|半)?\s*(?:ごろ)?(?:に)?解散/);
+ if (dismiss) {
+ return { start, end: toMinutes(startMeridiem, dismiss[1], dismiss[2], dismiss[0]), unknown };
+ }
+
+ return { start, end: null, unknown };
+}
+
// 対象期間(--days N で N日分。既定は7日分=1週間)
const rangeDays = (() => {
const n = Number(valueOf('--days'));
@@ -133,10 +194,14 @@ const collect = (rows, type) =>
(date) => date >= todayKey && date <= endKey,
);
const time = startTime(row.place);
+ const range = timeRange(row.place);
return {
type,
dates,
time,
+ startMinutes: range.start,
+ endMinutes: range.end,
+ unknownEnd: range.unknown,
sortKey: time || '99:99',
title: stripTags(row.title),
venue: venueOf(row.place),
@@ -144,6 +209,15 @@ const collect = (rows, type) =>
})
.filter((entry) => entry.dates.length > 0);
+/** 今日の分で、すでに終了しているか(終了時刻が読めないものは残す) */
+const endedOnToday = (entry) => {
+ if (!isTodayTarget) return false;
+ if (entry.startMinutes === null || entry.unknownEnd) return false;
+ // 終了時刻が書かれていない場合は2時間と仮定する
+ const end = entry.endMinutes ?? entry.startMinutes + 120;
+ return nowMinutes > end;
+};
+
const entries = [
...collect(readRows('events.json'), 'event'),
...(eventsOnly ? [] : collect(readRows('meetings.json'), 'meeting')),
@@ -152,6 +226,7 @@ const entries = [
// 1日分のときは、その日の分を時刻順に並べて使う
const items = entries
.filter((entry) => entry.dates.includes(todayKey))
+ .filter((entry) => !endedOnToday(entry))
.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
// ---------------------------------------------------------------- 投稿文の組み立て
@@ -167,8 +242,7 @@ function weightPlain(text) {
return [...text].reduce((sum, ch) => sum + (WIDE.test(ch) ? 2 : 1), 0);
}
-const isToday =
- toKey(now.getFullYear(), now.getMonth() + 1, now.getDate()) === todayKey;
+const isToday = isTodayTarget;
const header = `【${isToday ? '今日の小平市' : '小平市'}】${dateLabel}の予定`;
// 自動投稿であることを明示する
const autoNote = '※この投稿は自動で作成しています(予定データから)。';
@@ -221,6 +295,8 @@ function composeDigest() {
for (const entry of entries) {
if (ongoing.includes(entry)) continue;
for (const date of entry.dates) {
+ // 今日の分は、すでに終了しているものを除く
+ if (date === todayKey && endedOnToday(entry)) continue;
if (!byDate.has(date)) byDate.set(date, []);
byDate.get(date).push(entry);
}