Claude Codeで日付・時間処理を壊さない実装ガイド
Claude Codeでタイムゾーン、DST、Intl、DB保存方針を安全に実装・テストする実務ガイド。
なぜ日付・時間処理はClaude Code任せだけでは危ないのか
日付と時間のバグは、レビューで見落とされやすい割に本番影響が大きい領域です。日本時間では問題なく見えても、米国の夏時間、欧州の月末、インドの30分刻みのオフセット、ブラウザとサーバーのタイムゾーン差で一気に壊れます。Claude Codeに「日付処理を実装して」とだけ頼むと、見た目のフォーマットは整っても、保存方針、API契約、テストケースが曖昧なままになります。
この記事では、Claude Codeを「コード生成係」ではなく、日時仕様を確認するペアエンジニアとして使う前提で、実務の安全な型を作ります。対象は、予約、請求、通知、SLA、管理画面など、タイムゾーンの境界をまたぐWebアプリです。関連するテスト全体の作り方はClaude Codeテスト戦略、DB変更の進め方はデータベースマイグレーションも合わせて読むと整理しやすいです。
公式情報は作業前に確認します。表示はMDNのIntl.DateTimeFormat、Temporalの現在地はTC39 Temporal proposalとMDN Temporal、PostgreSQLの保存挙動はPostgreSQL Date/Time Types、Claude Codeの自動検証はClaude Code hooksを基準にします。
最初に決める用語と保存ポリシー
Claude Codeに実装を頼む前に、チーム内の言葉を固定します。ここが曖昧だと、レビューで「UTCにしているから安全」「JST固定だから大丈夫」のような雑な判断になります。
| 用語 | 平易な意味 | 保存の基本 |
|---|---|---|
| instant | 世界で一意に決まる時点。2026-06-02T00:00:00Zのような値 | DBではUTC基準のtimestampとして扱う |
| local date | ユーザーのカレンダー上の日付。誕生日、締切日、営業日など | YYYY-MM-DDとして時刻と混ぜない |
| wall clock time | 壁の時計で見る時刻。09:00開始の会議など | タイムゾーンIDとセットで扱う |
| IANA timezone | Asia/TokyoやAmerica/New_Yorkのような地域名 | オフセットだけで代用しない |
| DST | 夏時間。1日が23時間または25時間になる日がある | 境界日のテストを必ず置く |
実務では、次の3つを最初にAGENTS.mdやCLAUDE.mdに書いておくと、Claude Codeの出力が安定します。
- APIに送る「発生済みの時点」はISO 8601のUTC文字列にする。
- 予約や通知のような「未来の現地時刻」は、
localDate、localTime、timeZoneを分けて保存する。 - 画面表示では
Intl.DateTimeFormatにlocaleとtimeZoneを必ず渡し、環境のデフォルトに任せない。
概念図にすると、境界はこうです。
flowchart LR
A["Client: local date/time input"] --> B["API contract: localDate + localTime + timeZone"]
B --> C["Server: validate and convert when needed"]
C --> D["Database: instant in timestamptz + original timeZone"]
D --> E["UI: format with Intl.DateTimeFormat"]
E --> A
Intl.DateTimeFormatでまず安全な土台を作る
表示だけなら、最初に検討するのは標準APIのIntl.DateTimeFormatです。MDNが説明している通り、言語に応じた日付・時刻フォーマットを行うための組み込みAPIで、timeZoneを明示できます。Claude Codeには「toLocaleString()を引数なしで使わない」「日付だけの文字列をDateに直接渡さない」という制約も渡します。
以下は依存ライブラリなしで使える最小の日時ポリシーです。src/lib/date-policy.tsとして置き、画面、API、テストから同じ関数を使います。
// src/lib/date-policy.ts
export const TIME_POLICY = {
defaultLocale: 'ja-JP',
defaultTimeZone: 'Asia/Tokyo',
} as const;
type FormatOptions = {
locale?: string;
timeZone?: string;
includeWeekday?: boolean;
};
function toDate(input: string | Date): Date {
const date = input instanceof Date ? input : new Date(input);
if (!Number.isFinite(date.getTime())) {
throw new Error(`Invalid date value: ${String(input)}`);
}
return date;
}
export function toUtcIso(input: string | Date): string {
if (typeof input === 'string' && !/(Z|[+-]\d{2}:?\d{2})$/i.test(input)) {
throw new Error('Timestamp must include Z or an explicit UTC offset.');
}
return toDate(input).toISOString();
}
function dateParts(date: Date, timeZone: string): Record<string, string> {
const parts = new Intl.DateTimeFormat('en-US', {
timeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).formatToParts(date);
return Object.fromEntries(
parts
.filter((part) => part.type !== 'literal')
.map((part) => [part.type, part.value]),
);
}
export function dayKeyInTimeZone(
input: string | Date,
timeZone = TIME_POLICY.defaultTimeZone,
): string {
const parts = dateParts(toDate(input), timeZone);
return `${parts.year}-${parts.month}-${parts.day}`;
}
export function formatInstant(
input: string | Date,
{
locale = TIME_POLICY.defaultLocale,
timeZone = TIME_POLICY.defaultTimeZone,
includeWeekday = true,
}: FormatOptions = {},
): string {
return new Intl.DateTimeFormat(locale, {
timeZone,
weekday: includeWeekday ? 'short' : undefined,
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
timeZoneName: 'short',
}).format(toDate(input));
}
このコードの狙いは、日付処理を全部ライブラリで隠すことではありません。UTCの時点、現地日付のキー、表示フォーマットを明確に分けることです。Claude Codeには、この関数を直接増やさせる前に「新しい要件はこの3分類のどれかを説明してから実装して」と指示します。
未来の現地時刻はLuxonなどで明示的に変換する
Intl.DateTimeFormatは表示に強い一方、2026-11-01 01:30 America/New_Yorkのような現地時刻を安全にUTCへ変換する用途には向きません。ここはLuxonの公式ドキュメントやAPI docs、date-fnsの現在の機能を確認し、プロジェクトで1つに寄せます。TemporalはTC39でStage 4ですが、対象ブラウザとNodeの実装状況をMDNで確認し、必要ならポリフィル前提で採用します。
予約作成の例では、ユーザーが入力した「現地の日付と時刻」を、DB保存用のinstantに変換します。存在しない時刻と曖昧な時刻は仕様で決める必要があります。
// src/lib/schedule-time.ts
import { DateTime } from 'luxon';
type LocalScheduleInput = {
localDate: string; // YYYY-MM-DD
localTime: string; // HH:mm
timeZone: string; // IANA time zone, for example "America/New_York"
};
export function scheduleToUtcIso(input: LocalScheduleInput): string {
const rawLocal = `${input.localDate}T${input.localTime}`;
const local = DateTime.fromISO(rawLocal, { zone: input.timeZone });
if (!local.isValid) {
throw new Error(local.invalidExplanation ?? `Invalid local time: ${rawLocal}`);
}
const roundTrip = local.setZone(input.timeZone).toFormat("yyyy-MM-dd'T'HH:mm");
if (roundTrip !== rawLocal) {
throw new Error(`Nonexistent local time in ${input.timeZone}: ${rawLocal}`);
}
const iso = local.toUTC().toISO({ suppressMilliseconds: true });
if (!iso) {
throw new Error(`Could not convert local time to UTC: ${rawLocal}`);
}
return iso;
}
この関数だけで「夏時間終了日の重複した1時台」を完全に解決したことにはなりません。重複する時刻では、早い方のオフセットを選ぶのか、遅い方を選ぶのか、ユーザーに確認するのかを業務仕様として決めます。Claude Codeには「DSTで存在しない時刻、重複する時刻、月末、うるう日をテストに入れて」と明示します。
サーバー、クライアント、DBの境界を分ける
一番多い失敗は、サーバーとクライアントのどちらのタイムゾーンで解釈するかを決めずにDateへ渡すことです。ブラウザの入力欄、API、DB、メール送信、CSV出力は、それぞれ別の境界です。
具体的なユースケースは少なくとも次の3つに分けて考えます。
- 予約システム: 入力は施設の現地時刻、保存はUTC instant、表示は閲覧者または施設のタイムゾーン。
- 請求締め: 「月末23:59」は顧客の契約タイムゾーンのlocal dateで判定し、24時間加算で翌月を作らない。
- サポートSLA: 営業日、祝日、営業時間を地域ごとに持ち、期限のinstantと表示用の現地時刻を両方残す。
PostgreSQLを使う場合、公式ドキュメントはtimestamp with time zoneの入力がUTCへ変換され、元のタイムゾーン名は保持されないことを説明しています。つまり、timestamptzだけでは「ユーザーがAmerica/New_Yorkで登録した」という情報は戻せません。必要なら別カラムで保持します。
create table scheduled_events (
id uuid primary key,
title text not null,
starts_at timestamptz not null,
original_time_zone text not null check (original_time_zone <> ''),
local_date date not null,
local_time time not null,
created_at timestamptz not null default now()
);
create index scheduled_events_starts_at_idx
on scheduled_events (starts_at);
timestamp without time zoneに2026-06-02T09:00:00+09:00のような文字列を入れれば安全、という判断は危険です。PostgreSQLでは型がwithout time zoneに決まった値ではタイムゾーン指定が無視されます。Claude Codeのレビュー観点に「日時カラムの型とAPI契約が一致しているか」を入れてください。
DSTと境界日のテストを固定時刻で書く
テストでは「今日」「現在時刻」「ローカルマシンのタイムゾーン」を使わないことが重要です。入力をUTC instantで固定し、期待するlocal dateや表示を明記します。Vitestを使うなら、次のようなテストから始めます。高度なテスト設計はVitest活用ガイドも参考になります。
// src/lib/date-policy.test.ts
import { describe, expect, it } from 'vitest';
import { dayKeyInTimeZone, formatInstant, toUtcIso } from './date-policy';
describe('date/time policy', () => {
it('requires an explicit offset for API timestamps', () => {
expect(() => toUtcIso('2026-06-02T09:00:00')).toThrow(/offset/);
expect(toUtcIso('2026-06-02T09:00:00+09:00')).toBe('2026-06-02T00:00:00.000Z');
});
it('calculates a local day key across the UTC date boundary', () => {
expect(dayKeyInTimeZone('2026-03-31T15:01:00Z', 'Asia/Tokyo')).toBe('2026-04-01');
expect(dayKeyInTimeZone('2026-04-01T00:30:00Z', 'America/Los_Angeles')).toBe('2026-03-31');
});
it('formats a DST transition instant in the requested time zone', () => {
const label = formatInstant('2026-03-08T07:30:00Z', {
locale: 'en-US',
timeZone: 'America/New_York',
});
expect(label).toMatch(/03:30|3:30/);
expect(label).toMatch(/EDT|GMT-4/);
});
});
このテストは、タイムゾーンDBの表示名差まで完全固定しないようにしています。CIのNode/ICUデータ差でEDTではなくGMT-4になる場合があるためです。逆に、local dateのキーやUTC変換結果は業務ロジックなので強く固定します。
Claude Codeへの依頼文とフックで再発を防ぐ
Claude Codeには、実装前に制約を渡します。プロンプトは長くても構いません。曖昧な「いい感じの日付処理」より、壊してはいけない境界を列挙したほうが差分の品質が上がります。
日時処理を実装する前に、既存のdate-policy.tsとDBスキーマを読んでください。
制約:
- APIの発生済みtimestampはZまたはUTC offset必須のISO文字列にする
- 未来の予約はlocalDate、localTime、timeZoneを分ける
- Intl.DateTimeFormatにはlocaleとtimeZoneを必ず渡す
- new Date('YYYY-MM-DD')でlocal dateを扱わない
- DST開始日、DST終了日、月末、うるう日のVitestを追加する
- PostgreSQLのtimestamp/timestamptzの違いをレビューに書く
完了条件:
- npm test -- --run date-time が通る
- 変更したAPIレスポンス例をREADMEに追記する
チームで使うなら、Claude Code hooksで日時テストを自動実行するのも有効です。公式ドキュメントの通り、hooksはClaude Codeのライフサイクルでシェルコマンドを実行できます。たとえば編集後に日時テストを回す設定は次のように置けます。詳しい運用はClaude Codeフック機能ガイドを参照してください。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npm test -- --run date-time"
}
]
}
]
}
}
ライブラリ選定の現実的な基準
ライブラリ選定は「人気」より「扱う日時の種類」で決めます。単純な表示ならIntl.DateTimeFormat、日付の加減算やフォーマット補助ならdate-fns、IANAタイムゾーンを含む予約や変換ならLuxon、長期的に標準APIへ寄せたいならTemporalを検討します。ただしTemporalはStage 4であっても、全ターゲット環境で追加設定なしに使えるとは限りません。Claude Codeには、対象ランタイム、バンドルサイズ、ポリフィル有無、テスト方針を比較表にしてから実装させます。
| 選択肢 | 向いている場面 | 注意点 |
|---|---|---|
| Intl | ロケール対応の表示、タイムゾーン指定のフォーマット | 現地時刻からinstantへの変換は別途考える |
| date-fns | 純粋な日付計算、関数単位の利用、軽いユーティリティ | タイムゾーン要件は公式ドキュメントで現状確認する |
| Luxon | IANAタイムゾーン、予約、相対時間、ISO変換 | 曖昧なDST時刻の業務仕様は別途必要 |
| Temporal | Instant、PlainDate、ZonedDateTimeを型として分けたい設計 | ランタイム互換性とポリフィル方針を確認する |
AdSenseや記事収益の観点でも、単なるライブラリ紹介では弱いです。読者が本当に困るのは「どの値をDBに持つか」「どこで変換するか」「何をテストするか」です。チーム導入でこのポリシーをCLAUDE.md、hooks、DBレビューに落とし込むなら、Claude Code研修・導入相談でプロジェクトに合わせたレビュー表を作るのが自然です。個人で始める場合は無料チートシートに、この記事のプロンプトを足して使うと実装漏れを減らせます。
この記事で紹介した内容を実際に試した結果
Masaの検証メモとして、固定したUTC instantを東京、ニューヨーク、ロサンゼルスで表示し、local dateのキーが日付境界をまたぐケースを重点的に見ました。いちばん再現しやすい失敗は、YYYY-MM-DDを「ユーザーの現地日付」のつもりでDateに変換し、別タイムゾーンの画面で前日表示になるケースです。この記事の方針では、local dateを文字列のまま扱い、instantの保存と表示だけを明示的に変換するため、Claude Codeが生成した差分もレビューしやすくなりました。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。