/**
* 制御盤から受け取ったログの一覧の絞り込み・並べ替え・ページ分け(指示書AY 段階3-2。D8)。
* 期待値の正は指示書AY §3 表1(`__tests__/query.test.ts`)。
*
* ★純粋関数★(`CLAUDE.md` §9.4)。一覧は制御盤からもらい、D-Watch はここで絞るだけ。
* ★`modified_at` は `Date.parse()` の瞬間で比べる・並べる★(文字列で比べるとオフセット違いで順序が狂う。4-17)。
* 入力の `modified_at` は検証済み(オフセット付き・存在する日付。`lib/ds-logs/validate.ts`)である前提。
*/
import { toJstIso } from "@/lib/format/date";
import type { DsLogFile, DsLogSort, DsLogType } from "@/lib/types";
export interface DeviceLogQuery {
/** `null` = 全種類。★`types` に無いキーは0件★(D23) */
types: readonly string[] | null;
/** 瞬間(ms)。`from ≤ t` */
from: number | null;
/** 瞬間(ms)。`t < to` */
to: number | null;
sort: DsLogSort;
/** 1〜 */
page: number;
perPage: number;
}
/** 名前の昇順。★`localeCompare` を使わない★(実行環境のロケールで順序を変えない) */
function compareName(a: string, b: string): number {
return a < b ? -1 : a > b ? 1 : 0;
}
/**
* 表1。★入力の配列を書き換えない★。
* - `date_desc`: 新しい順 → 同時刻は name 昇順
* - `type_date_desc`: 制御盤の `types` 配列の順(★クエリの順ではない★)→ 新しい順 → name 昇順
* 出力の `modified_at` は `toJstIso()` で JST に揃える(表1 Q17。`CLAUDE.md` §9.3)。
*/
export function queryDeviceLogs(
types: readonly DsLogType[],
items: readonly DsLogFile[],
query: DeviceLogQuery
): { items: DsLogFile[]; total: number } {
const wanted = query.types === null ? null : new Set(query.types);
const typeOrder = new Map(types.map((t, index) => [t.key, index]));
const filtered = items
.map((item) => ({ item, at: Date.parse(item.modified_at) }))
.filter(({ item, at }) => {
if (wanted !== null && !wanted.has(item.type)) return false;
if (query.from !== null && at < query.from) return false;
if (query.to !== null && at >= query.to) return false;
return true;
});
filtered.sort((a, b) => {
if (query.sort === "type_date_desc") {
const byType =
(typeOrder.get(a.item.type) ?? Number.MAX_SAFE_INTEGER) -
(typeOrder.get(b.item.type) ?? Number.MAX_SAFE_INTEGER);
if (byType !== 0) return byType;
}
if (a.at !== b.at) return b.at - a.at;
return compareName(a.item.name, b.item.name);
});
const start = (query.page - 1) * query.perPage;
const page = filtered.slice(start, start + query.perPage).map(({ item, at }) => ({
...item,
modified_at: toJstIso(new Date(at)),
}));
return { items: page, total: filtered.length };
}
/**
* モーダルの `types` クエリ(指示書BJ 表Q・J10)。
* `null` = パラメータを付けない(省略=全種類、または種類未初期化・種類0件)。
* ★全種類は「件数同じかつ known を全部含む」★(件数だけで誤認しない。罠5)。
*/
export function deviceLogTypesParam(
knownTypeKeys: readonly string[],
selected: ReadonlySet<string> | null
): string | null {
if (selected === null) return null;
if (knownTypeKeys.length === 0) return null;
if (
selected.size === knownTypeKeys.length &&
knownTypeKeys.every((key) => selected.has(key))
) {
return null;
}
const knownSelected = knownTypeKeys.filter((key) => selected.has(key));
const unknown = [...selected].filter((key) => !knownTypeKeys.includes(key));
return [...knownSelected, ...unknown].join(",");
}