Files
Jyotisha/frontend/src/lib/report-fact-table-values.ts
T

54 lines
3.3 KiB
TypeScript

import { findForbiddenContent } from "./personal-report-contract";
import { PLANET_ZH, SIGN_ORDER, SIGN_ZH, type PlanetName, type SignName } from "./chart-view-labels";
export const record = (value: unknown): Record<string, unknown> => value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
export const at = (value: unknown, path: string): unknown => path.split(".").reduce((next, key) => record(next)[key], value);
export const entries = (value: unknown) => Object.entries(record(value)).filter(([key]) => /^[A-Za-z0-9_-]+$/.test(key));
export const list = (value: unknown): unknown[] => Array.isArray(value) ? value : [];
export const decimal = (value: unknown): string | null => typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : null;
export const yesNo = (value: unknown): string | null => typeof value === "boolean" ? value ? "是" : "否" : null;
/** Display-only allowlist: unknown/internal labels never become prose. */
export const readable = (value: unknown): string | null => typeof value === "string" && value.length > 0 && value.length <= 300
&& !/[_<>\[\]{}\/\\]|https?:|\b\w+\.\w+\b/.test(value) && !findForbiddenContent(value).length ? value : null;
export const date = (value: unknown): string | null => typeof value === "string" && /^\d{4}-\d{2}-\d{2}(?:T|$)/.test(value) ? value.slice(0, 10) : null;
export function planet(value: unknown, chinese?: unknown): string | null {
const label = readable(chinese);
if (label && /[㐀-鿿]/.test(label)) return label;
return typeof value === "string" ? PLANET_ZH[value as PlanetName] ?? (value === "Lagna" ? "上升" : null) : null;
}
export function sign(value: unknown, chinese?: unknown, index?: unknown): string | null {
const label = readable(chinese);
if (label && /[㐀-鿿]/.test(label)) return label;
if (typeof value === "string") {
const mapped = SIGN_ZH[value as SignName];
if (mapped) return mapped;
if (Object.values(SIGN_ZH).some(name => value === name || value === `${name}座`)) return value;
}
return typeof index === "number" && Number.isInteger(index) && index >= 0 && index < 12 ? SIGN_ZH[SIGN_ORDER[index]] : null;
}
export const position = (value: unknown) => {
const raw = record(value);
return [sign(raw.sign, raw.sign_cn, raw.sign_idx), decimal(raw.degree_in_sign ?? raw.sign_degree)] as const;
};
export const isBlocked = (value: unknown) => {
const raw = record(value);
return raw.status === "blocked" || raw.status === "missing_in_local" || raw.claimStatus === "blocked" || raw.claim_status === "blocked";
};
/** English edition: engine keys are already English names; `Lagna` reads as Ascendant. */
export function planetEn(value: unknown): string | null {
if (typeof value !== "string") return null;
if (value === "Lagna") return "Ascendant";
return value in PLANET_ZH ? value : null;
}
/** English sign name from an engine key, a Chinese sign name, or a 0-based index. */
export function signEn(value: unknown, index?: unknown): string | null {
if (typeof value === "string") {
if (value in SIGN_ZH) return value;
const match = SIGN_ORDER.find(name => value === SIGN_ZH[name] || value === `${SIGN_ZH[name]}座`);
if (match) return match;
}
return typeof index === "number" && Number.isInteger(index) && index >= 0 && index < 12 ? SIGN_ORDER[index] : null;
}