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

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 全体の仕様検証器ではなく、時刻と改行に絞った小さな門番です。

用語

用語この実験での意味
ICSiCalendar形式で予定を交換するためのテキストファイルです
UTCZを末尾に付けるときの基準時刻です
JSTAsia/Tokyoとして換算した日本の時刻です
floating timeZも時間帯参照も付けない、特定の時間帯に固定されない時刻です
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-TimeZはUTCで、時間帯参照のない日時はfloating timeです
Python zoneinfoIANA時間帯データを使ってAsia/Tokyoへ換算できます

実測したのは上の最小サンプルです。カレンダー製品として販売できるかどうかは、複数予定を含む完成ファイルで別に判定します。

この記事をシェア