From 8da814de863f6f94e5baa5d3f1e364e7f2453ad1 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Mon, 9 Nov 2026 00:03:41 +0900 Subject: post-x-events.mjs: 今日すでに終了した予定を投稿から除く MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 時間・会場セルから終了時刻を読み取り、今日の分は終了時刻を過ぎた予定を除外する。終了時刻の記載がない場合は開始の2時間後とみなし、未定・時刻なしの予定は残す。動作確認用に --at HH:MM を追加。 --- scripts/post-x-events.mjs | 82 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 3 deletions(-) (limited to 'scripts') 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); } -- cgit v1.3.1