/**
* 表示用の日付・時刻整形。
* APIの日時は ISO8601(JST, +09:00)。タイムゾーン変換せず文字列から切り出す。
*/
import { DB_DATETIME_OFFSET_MINUTES } from "@/lib/config";
/** 2026-08-27T02:00:00+09:00 → 8/27(モックの日付列) */
export function formatMd(iso: string): string {
const month = Number.parseInt(iso.slice(5, 7), 10);
const day = Number.parseInt(iso.slice(8, 10), 10);
return `${month}/${day}`;
}
/** 2026-08-27T02:00:00+09:00 → 02:00 */
export function formatHm(iso: string): string {
return iso.slice(11, 16);
}
/** JSTの今日を YYYY-MM-DD で返す */
export function todayJst(): string {
return new Intl.DateTimeFormat("en-CA", {
timeZone: "Asia/Tokyo",
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(new Date());
}
/**
* Prisma から読んだ `Date`(DBの DATETIME/TIMESTAMP)を ISO8601(JST, +09:00)文字列にする
* (指示書P 段階2/3)。
*
* `Date` は常にUTC起点の絶対時刻を保持するので、`Intl` 等の環境依存タイムゾーンに頼らず、
* UTCに9時間を足してから UTC の getter で文字列を組み立てる(実行環境のタイムゾーンに
* 依存しない。テストが CI 環境のタイムゾーンで揺れないようにするため)。
*/
export function toJstIso(date: Date): string {
const jst = new Date(date.getTime() + 9 * 60 * 60 * 1000);
const y = jst.getUTCFullYear();
const m = String(jst.getUTCMonth() + 1).padStart(2, "0");
const d = String(jst.getUTCDate()).padStart(2, "0");
const hh = String(jst.getUTCHours()).padStart(2, "0");
const mm = String(jst.getUTCMinutes()).padStart(2, "0");
const ss = String(jst.getUTCSeconds()).padStart(2, "0");
return `${y}-${m}-${d}T${hh}:${mm}:${ss}+09:00`;
}
/**
* Prisma(`@prisma/adapter-mariadb`)から出てきた `Date` を「真の瞬間」に直す。
*
* ★なぜ要るのか★:
* DB の DATETIME には ★日本時間の壁時計値★ が入っている(TeamWorks 第8回 A2。指示書Y)。
* ところがアダプタは ``new Date(`${value}Z`)`` で ★UTC とみなして★ `Date` にする
* (`node_modules/@prisma/adapter-mariadb/dist/index.js` の `mapRow()`)。
* ★この挙動は設定で変更できない★ — 接続文字列の `timezone` も効かない
* (アダプタが `dateStrings: true` でドライバの変換を迂回しているため)。
* 結果、返ってくる `Date` は ★真の瞬間より9時間先★ を指している。
*
* ★補正しないと何が起きるか★:
* 表示が9時間進むだけでなく、`isFutureSchedule()`(`lib/db/clean-log-mapping.ts`)の
* 実時刻との比較が狂い、★深夜2時の清掃が翌朝の確認時刻でも「まだ未来」と判定されて
* 画面から消える★(`CLAUDE.md` §1 の「翌朝確認型」の運用を直撃する)。
*
* ★使い方★: Prisma から出た日時は、既存の処理に渡す前に必ずこれを通す。
* `toJstIso()` と `isFutureSchedule()` は変更しない(入力が正しくなれば直る)。
*/
export function fromDbJst(date: Date): Date {
return new Date(date.getTime() - DB_DATETIME_OFFSET_MINUTES * 60_000);
}
/**
* `fromDbJst()` の逆。「真の瞬間」を、DB に書きたい壁時計値として渡せる `Date` にする。
*
* ★なぜ要るのか★: アダプタの書き込み側(`formatDateTime()`)は `getUTC*` で文字列を組み立てる。
* つまり「DB に書きたい壁時計値を UTC のフィールドに載せた `Date`」を渡す必要がある。
*
* ★消費者★: `scripts/seed-local-db.ts` と、アプリの書き込み `lib/db/robot-error-write.ts`
* (書き込み許可4カラム `status` / `comment` / `responded_at` / `resolved_at` の日時2カラム。D-042 / 指示書AT)。
* ★メインDBに日時を書くときは必ずこれを通すこと★(付け忘れはガードテスト
* `lib/db/__tests__/robot-error-write-sql.test.ts` が検出する)
*/
export function toDbJst(date: Date): Date {
return new Date(date.getTime() + DB_DATETIME_OFFSET_MINUTES * 60_000);
}
/**
* Prisma から出た日時 → ISO8601(JST, +09:00)文字列。
*
* ★`toJstIso(fromDbJst(date))` の合成★ — ★★この2段を1つの式に潰さないこと★★。
* 潰すと「DB のタイムゾーン」という設定値が式から消え、`lib/config` を変えても効かなくなる。
*
* ★性質★: 戻り値の日時の桁は ★DB の生値そのもの★ になる。
* (DB が `2026-09-11 02:00:00` なら `"2026-09-11T02:00:00+09:00"`)
* ★これが「直った」ことの判定基準★ — DB の生値と画面の表示が一致すること。
*/
export function jstIsoFromDb(date: Date): string {
return toJstIso(fromDbJst(date));
}
/** YYYY-MM-DD 同士の日数(両端含む)。不正なら null */
export function inclusiveDayCount(from: string, to: string): number | null {
if (!/^\d{4}-\d{2}-\d{2}$/.test(from) || !/^\d{4}-\d{2}-\d{2}$/.test(to)) {
return null;
}
const start = Date.parse(`${from}T00:00:00+09:00`);
const end = Date.parse(`${to}T00:00:00+09:00`);
if (Number.isNaN(start) || Number.isNaN(end)) return null;
return Math.floor((end - start) / 86_400_000) + 1;
}