73 lines
4.1 KiB
TypeScript
73 lines
4.1 KiB
TypeScript
import { formatDateTime, formatTime } from "@/shared/utils/date-time";
|
||
|
||
const zonedTime = /(?<![\w/+:.-])(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?)(Z|[+-]\d{2}:\d{2})(?![\w:+/-])/g;
|
||
const utcMarker = String.raw`(?:Z|(?:[((]\s*)?UTC(?:\s*\+?0(?::00)?)?(?:\s*[))])?)(?![\w:+/-])`;
|
||
const utcRange = new RegExp(String.raw`(?<![\w/+:.-])(?:(\d{4}-\d{2}-\d{2})[T ])?(\d{2}:\d{2})\s*[–—~至-]\s*(\d{2}:\d{2})\s*${utcMarker}`, "g");
|
||
const utcPoint = new RegExp(String.raw`(?<![\w/+:.-])(?:(\d{4}-\d{2}-\d{2})[T ])?(\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?)\s*${utcMarker}`, "g");
|
||
|
||
function parseTime(day: string, clock: string, zone = "Z"): number | null {
|
||
const date = new Date(`${day}T00:00:00Z`);
|
||
const [hours, minutes, seconds = "0"] = clock.split(":");
|
||
if (!Number.isFinite(date.getTime()) || date.toISOString().slice(0, 10) !== day
|
||
|| Number(hours) > 23 || Number(minutes) > 59 || Number(seconds) >= 60) return null;
|
||
const value = Date.parse(`${day}T${clock}${zone}`);
|
||
return Number.isFinite(value) ? value : null;
|
||
}
|
||
|
||
/** Display historical report prose without changing stored facts, IDs, URLs or code. */
|
||
export function formatDisplayText(text: string | null | undefined): string {
|
||
if (!text) return text ?? "";
|
||
return text.split(/(https?:\/\/\S+|tjwater:\/\/\S+|`[^`]*`)/g).map((part, index) => {
|
||
if (index % 2) return part;
|
||
return part.replace(utcRange, (original, day: string | undefined, start: string, end: string) => {
|
||
const startAt = parseTime(day ?? "2000-01-01", start);
|
||
let endAt = parseTime(day ?? "2000-01-01", end);
|
||
if (startAt === null || endAt === null) return original;
|
||
if (endAt < startAt) endAt += 24 * 60 * 60_000;
|
||
const dateOptions = { year: "numeric", month: "numeric", day: "numeric" } as const;
|
||
const crossesDay = formatDateTime(startAt, dateOptions) !== formatDateTime(endAt, dateOptions);
|
||
const startLabel = day ? formatDateTime(startAt, { ...dateOptions, hour: "2-digit", minute: "2-digit" }) : formatTime(startAt);
|
||
const endLabel = crossesDay && day
|
||
? formatDateTime(endAt, { ...dateOptions, hour: "2-digit", minute: "2-digit" })
|
||
: `${crossesDay ? "次日 " : ""}${formatTime(endAt)}`;
|
||
return `${startLabel}–${endLabel}(UTC+8)`;
|
||
}).replace(zonedTime, (original, day: string, clock: string, zone: string) => {
|
||
const value = parseTime(day, clock, zone);
|
||
return value === null ? original : `${formatDateTime(value)}(UTC+8)`;
|
||
}).replace(utcPoint, (original, day: string | undefined, clock: string) => {
|
||
const value = parseTime(day ?? "2000-01-01", clock);
|
||
if (value === null) return original;
|
||
return `${day ? formatDateTime(value) : formatTime(value)}(UTC+8)`;
|
||
});
|
||
}).join("");
|
||
}
|
||
|
||
/** Chart labels use the display clock without repeating its zone to readers. */
|
||
export function formatChartDisplayText(value: string): string {
|
||
return formatDisplayText(value)
|
||
.replace(/[((]\s*(?:上海时间|Asia\/Shanghai|UTC\s*\+\s*8(?::00)?)\s*[))]/g, "")
|
||
.replace(/(?:上海时间|Asia\/Shanghai|UTC\s*\+\s*8(?::00)?)(?![\w:+-])\s*/g, "")
|
||
.replace(/[,,]\s*([))])/g, "$1")
|
||
.trim();
|
||
}
|
||
|
||
type ChartDisplayFields = {
|
||
title: string;
|
||
x_axis_name?: string;
|
||
y_axis_name?: string;
|
||
x_data: string[];
|
||
series: Array<{ name: string }>;
|
||
};
|
||
|
||
/** Preserve values and original sample timestamps; convert only visible labels. */
|
||
export function withChartDisplayTimes<T extends ChartDisplayFields>(chart: T): T {
|
||
const utcAxis = /UTC(?:\s*\+?0(?::00)?)?(?![\w+:-])/.test(chart.x_axis_name ?? "");
|
||
const bareTime = /^(?:\d{4}-\d{2}-\d{2}[T ])?\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?$/;
|
||
return { ...chart,
|
||
title: formatChartDisplayText(chart.title),
|
||
...(chart.x_axis_name ? { x_axis_name: formatChartDisplayText(chart.x_axis_name.replace(/UTC(?:\s*\+?0(?::00)?)?(?![\w+:-])/g, "UTC+8")) || "时间" } : {}),
|
||
x_data: chart.x_data.map((label) => formatChartDisplayText(utcAxis && bareTime.test(label) ? `${label} UTC` : label)),
|
||
series: chart.series.map((series) => ({ ...series, name: formatChartDisplayText(series.name) }))
|
||
};
|
||
}
|