ゴール
- JSTの一貫取得、UTC→JST変換、テストの時刻固定をシンプルに実装
- 時間起因の不具合(タイムゾーン差、月/年度またぎの揺れ、テストの不安定化)を防ぐ
よくある問題
- サーバーTZとユーザー期待のズレ(UTC基準で日付が前日/翌日になる)
- 境界月の不安定(3月末/4月初、9月末/10月初、年跨ぎ)
- new Date() 直書きで再現性がなく、テストが時期で壊れる
設計の原則
-
Pure関数 + 依存注入:
now?: Date
を引数で受ける - 変換をユーティリティに集約: DRYと責務分離
- 境界条件を先に決める: 例) 4–9月=H1、10–3月=H2
実装パターン(最小セット)
- JST要素を取得(汎用)
export function formatJst(now: Date = new Date()) {
const s = now.toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' });
const [d, t] = s.split(' ');
const [year, month, day] = d.split('/').map(Number);
const [hour, minute, second] = t.split(':').map(Number);
return { year, month, day, hour, minute, second };
}
export const formatJstYear = (now?: Date) => formatJst(now).year;
export const formatJstMonth = (now?: Date) => formatJst(now).month;
- 会計期(H1/H2)のDB形式を算出(例: 20251/20252)
export function getCurrentFiscalPeriodDb(now?: Date) {
const y = formatJstYear(now);
const m = formatJstMonth(now);
if (m >= 4 && m <= 9) return `${y}1`; // 4–9月: 当年H1
const fy = m >= 10 ? y : y - 1; // 10–3月: 当年H2(1–3月は前年)
return `${fy}2`;
}
- 表示形式 ⇄ DB形式の相互変換
const FISCAL_REGEX = /^FY(\d{4})H([12])$/;
export function toDbPeriod(fiscal: string) {
const m = fiscal.match(FISCAL_REGEX);
if (!m) throw new Error('無効な会計年度形式');
return `${m[1]}${m[2]}`; // FY2024H1 -> 20241
}
export function toDisplayPeriod(db: string) {
const year = db.slice(0, 4);
const half = db.slice(4, 5);
return `FY${year}H${half}`; // 20241 -> FY2024H1
}
テストの時刻固定(Vitest)
- Vitest
vi.useFakeTimers();
vi.setSystemTime(new Date('2025-08-01T00:00:00Z'));
// ...assert...
vi.useRealTimers();
- afterEachで必ず実時間に戻すと安全
afterEach(() => {
vi.useRealTimers(); // Jestなら jest.useRealTimers()
});
ありがちな落とし穴と対策
-
OS/コンテナのTZ依存:
toLocaleString({ timeZone: 'Asia/Tokyo' })
で明示 -
文字列パースの不安定:
.split(...).map(Number)
で安定化(ロケール差の影響最小化) -
実行時刻依存のテスト: 時刻固定 + Pure関数化(
now?: Date
)で再現性担保 - 境界の取りこぼし: 3/31, 4/1, 9/30, 10/1, 年跨ぎケースを必ずテスト
レシピ集(ショート)
- “今日のJST要素”を取る
const { year, month, day } = formatJst();
- “今の会計期(DB)”を得る
const period = getCurrentFiscalPeriodDb(); // 例: '20251'
``**
- **表示⇄DBの変換**
```ts
toDbPeriod('FY2024H1'); // '20241'
toDisplayPeriod('20252'); // 'FY2025H2'
チェックリスト
-
時間取得はユーティリティ経由(直に
new Date()
参照しない) - 変換ロジックは1か所に集約(重複・分散を避ける)
- テストは時刻固定 + 境界月ケース(揺れを潰す)
まとめ
- 設計原則: Pure関数化・依存注入・変換の集約
- 最小コード: JST要素取得・会計期計算・形式変換
- テスト: useFakeTimers + setSystemTime + useRealTimers
- これだけで、UTC/JSTずれや境界月の不具合、時間依存で壊れるテストを大幅に削減できます。