AIで作る予定表商品の時刻を検査する|9時が18時になる初稿

「9 時に販売準備」と書いた予定が、購入者のカレンダーでは 18 時になる。予定表を商品にするなら、このずれは見逃せません。
2026 年 10 月 2 日、AI で下書きする販売準備用の予定表を想定し、1 件の予定を持つ ICS の最小サンプルを作りました。これは実際の購入者に配ったファイルではなく、時刻と改行の間違いを意図的に入れた実験用データです。初稿はDTSTART:20261005T090000Zで、東京では 18 時になります。さらに改行が LF だけでした。修正稿では開始を20261005T000000Zに直し、CRLF へ変えています。
前回から今回への差分
前回は、AI で下書きした EPUB 商品の目次、欠落ファイル、リンクを検査しました。読むためのファイルが揃っていても、予定の時刻が合っているとは限りません。今回はファイルの種類を iCalendar 形式の ICS へ変え、買い手に見える開始時刻を検査します。
| 時点 | 確認できること | 残ること |
|---|---|---|
| 前回完了時 | EPUBの構造と参照先を検査できます | ICSの時刻は未確認です |
| 今回完了後 | 予定1件のUTCからJSTへの換算と改行を検査できます | カレンダーアプリでの表示、繰り返し予定、長い行は別途確認します |
飛ばせる章: UTCとJSTの違い、RFC 5545のCRLF規則を知っている場合は「初稿と修正稿を同じ検査にかける」から進めます。
なぜ見た目の9時を信じないのか
Z付きの日時は UTC です。20261005T090000Zの「09」は東京の午前 9 時ではありません。Python のAsia/Tokyoへ換算すると 18 時です。Zを外すだけでは floating time になります。特定の時間帯に固定されず、受け手によって解釈が変わります。今回は予定を特定の瞬間へ固定したいので、UTC の 0 時を記録します。
RFC 5545 の content line は CRLF で終えます。テキストエディターで改行して見えていても、LF だけではこの規則を満たしません。商品として配る前に、表示時刻とバイト列の両方を見ます。
[AIで予定の文案を下書き]
↓
[ICSの開始日時と改行を検査]
├─ 18:00 / LF → 修正して再検査
└─ 09:00 / CRLF → 実アプリで確認
↓
[人が販売可否を判断]
自動検査に合格しても、販売の承認にはなりません。今回のスクリプトは ICS 全体の仕様検証器ではなく、時刻と改行に絞った小さな門番です。
用語
| 用語 | この実験での意味 |
|---|---|
| ICS | iCalendar形式で予定を交換するためのテキストファイルです |
| UTC | Zを末尾に付けるときの基準時刻です |
| JST | Asia/Tokyoとして換算した日本の時刻です |
| floating time | Zも時間帯参照も付けない、特定の時間帯に固定されない時刻です |
| CRLF | 行末の復帰文字と改行文字の組み合わせです |
| LF | 改行文字だけの行末です |
| DTSTART / DTEND | 予定の開始と終了を表すプロパティです |
初稿と修正稿を同じ検査にかける
下のコマンドはファイルを作らず、予定 1 件の行をメモリー上で組み立てます。初稿には UTC の 9 時と LF 改行を入れ、修正稿には UTC の 0 時と CRLF 改行を入れます。終了は両方とも UTC の 1 時です。この実験では、開始が東京の 9 時かどうかを比較します。環境には Python 3 とAsia/Tokyoの時間帯データが必要です。
python3 -c 'from datetime import datetime, timezone
from zoneinfo import ZoneInfo
def audit(label, start, ending):
lines = ["BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//hibanas//ICS lab//JA", "BEGIN:VEVENT", "UID:[email protected]", "DTSTAMP:20261002T140000Z", "DTSTART:" + start, "DTEND:20261005T010000Z", "SUMMARY:販売準備", "END:VEVENT", "END:VCALENDAR"]
blob = (ending.join(lines) + ending).encode("utf-8")
instant = datetime.strptime(start, "%Y%m%dT%H%M%SZ").replace(tzinfo=timezone.utc)
local = instant.astimezone(ZoneInfo("Asia/Tokyo"))
problems = []
if b"\r\n" not in blob or blob.replace(b"\r\n", b"").find(b"\n") >= 0:
problems.append("改行がCRLFでない")
if local.strftime("%Y-%m-%d %H:%M") != "2026-10-05 09:00":
problems.append("開始時刻が09:00 JSTでない")
print(label + ": " + local.strftime("%Y-%m-%d %H:%M JST") + " / " + ("合格" if not problems else "不合格: " + "、".join(problems)))
return problems
bad = audit("初稿", "20261005T090000Z", "\n")
good = audit("修正稿", "20261005T000000Z", "\r\n")
assert len(bad) == 2 and not good'
初稿: 2026-10-05 18:00 JST / 不合格: 改行がCRLFでない、開始時刻が09:00 JSTでない
修正稿: 2026-10-05 09:00 JST / 合格
同じ検査条件で、初稿は 2 件の問題を検出し、修正稿は 0 件でした。assertがあるため、想定した件数と違えばコマンドは失敗します。本文の「9 時」と ICS のZ付き時刻を目で照らすだけでは、初稿を通してしまいます。
ここで調べているのは開始時刻と改行だけです。SUMMARYの表記、UIDの重複、DTENDが開始より後かどうか、75 オクテットを超える行の折り返し、繰り返し予定は検査していません。特に予定表を複数件へ増やす前には、実際の ICS ファイルを対象にした仕様検査とアプリへのインポートが必要です。売り物の説明文にも、適用する時間帯を明記します。
よくあるエラー
| 症状 | 原因 | 対処 |
|---|---|---|
| 9時の予定が18時になります | JSTの9時にZを付けています | UTCへ換算し、東京の9時なら000000Zを使います |
| 改行がCRLFでないと出ます | LFだけで行をつないでいます | "\r\n"で行をつなぎ、末尾にも付けます |
ZoneInfoNotFoundErrorが出ます | 実行環境にAsia/Tokyoの時間帯データがありません | IANA時間帯データまたはPythonのtzdataを用意して再実行します |
| 検査は通るのにアプリ表示が異なります | この簡易検査の対象外です | 配布予定のICSを実際の利用アプリへ取り込み、表示を確認します |
出典と検証範囲
| 一次情報 | この回で確認した事項 |
|---|---|
| RFC 5545 §3.1 Content Lines | 行末はCRLFです。75オクテットを超える行は折り返しが推奨されます |
| RFC 5545 §3.3.5 Date-Time | ZはUTCで、時間帯参照のない日時はfloating timeです |
Python zoneinfo | IANA時間帯データを使ってAsia/Tokyoへ換算できます |
実測したのは上の最小サンプルです。カレンダー製品として販売できるかどうかは、複数予定を含む完成ファイルで別に判定します。


