54 lines
2.1 KiB
TypeScript
54 lines
2.1 KiB
TypeScript
/**
|
||
* Profile に関する派生計算ヘルパー。
|
||
*
|
||
* ADR-0014「shadcn 標準フォームによる Pane 4 編集 UI」で Profile から `age` を
|
||
* 撤去し、`birthday` から派生計算する方針に変更したため、その単一ソースとしてここに集約する。
|
||
*
|
||
* 設計原則:
|
||
* - 純粋関数のみ(副作用なし、`new Date()` の都度呼び出しは許容)
|
||
* - 不正/空入力は空文字 ""(または undefined)で返す(throw しない)
|
||
* - 日付の保存形式は ANSI / ISO 8601 短形式(YYYY-MM-DD)に統一
|
||
*/
|
||
|
||
/**
|
||
* `YYYY-MM-DD` 形式の文字列を `Date` に変換する。
|
||
* 空文字 / 不正フォーマットは `undefined` を返す。
|
||
*
|
||
* 時刻は `T00:00:00` をローカルタイムで補い、タイムゾーン跨ぎを避ける。
|
||
*/
|
||
export function parseISODate(s: string): Date | undefined {
|
||
if (!s) return undefined;
|
||
const d = new Date(s + "T00:00:00");
|
||
return Number.isNaN(d.getTime()) ? undefined : d;
|
||
}
|
||
|
||
/**
|
||
* `Date` を `YYYY-MM-DD` 形式の文字列に整形する。
|
||
* `undefined` または不正な Date は空文字を返す。
|
||
*/
|
||
export function formatISODate(d: Date | undefined): string {
|
||
if (!d || Number.isNaN(d.getTime())) return "";
|
||
const y = d.getFullYear();
|
||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||
const day = String(d.getDate()).padStart(2, "0");
|
||
return `${y}-${m}-${day}`;
|
||
}
|
||
|
||
/**
|
||
* 生年月日から年齢を派生計算し、「N 歳」形式で返す。
|
||
* 空 / 不正な birthday は空文字を返す(UI 側で「未表示」になる)。
|
||
*
|
||
* 「今日」は `new Date()`。Pane 4 は `"use client"` なのでクライアント側で都度評価される。
|
||
* SSR / hydration mismatch は発生しない。
|
||
*/
|
||
export function calculateAge(birthday: string): string {
|
||
const birth = parseISODate(birthday);
|
||
if (!birth) return "";
|
||
const today = new Date();
|
||
let age = today.getFullYear() - birth.getFullYear();
|
||
const m = today.getMonth() - birth.getMonth();
|
||
if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) age--;
|
||
if (age < 0) return "";
|
||
return `${age} 歳`;
|
||
}
|