const DATE_YMD_RE = /^(\d{4})-(\d{2})-(\d{2})$/; function pad2(n: number): string { return String(n).padStart(2, "0"); } /** 本地日历 `YYYY-MM-DD` */ export function toDateYmdValue(date: Date): string { return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`; } function parseDateYmd(value: string): Date | null { const match = DATE_YMD_RE.exec(value.trim()); if (!match) { return null; } const [, y, mo, d] = match; const date = new Date(Number(y), Number(mo) - 1, Number(d)); return Number.isNaN(date.getTime()) ? null : date; } function formatUtcDateTimeFromMs(ms: number): string { const date = new Date(ms); return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}`; } /** 本地自然日 00:00:00 → UTC `YYYY-MM-DD HH:mm:ss`(开账 API) */ export function localDateYmdToUtcPeriodStart(ymd: string): string { const date = parseDateYmd(ymd); if (date === null) { return ymd; } const ms = new Date( date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0, ).getTime(); return formatUtcDateTimeFromMs(ms); } /** 本地自然日 23:59:59 → UTC `YYYY-MM-DD HH:mm:ss`(开账 API) */ export function localDateYmdToUtcPeriodEnd(ymd: string): string { const date = parseDateYmd(ymd); if (date === null) { return ymd; } const ms = new Date( date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999, ).getTime(); return formatUtcDateTimeFromMs(ms); } export function localDateRangeToUtcPeriodBounds( startYmd: string, endYmd: string, ): { period_start: string; period_end: string } { return { period_start: localDateYmdToUtcPeriodStart(startYmd), period_end: localDateYmdToUtcPeriodEnd(endYmd), }; } /** UTC 账期时刻 → 本地日历 `YYYY-MM-DD`(列表展示) */ export function utcPeriodInstantToLocalYmd(iso: string | null | undefined): string { if (iso == null || iso === "") { return "—"; } const ms = Date.parse(iso); if (Number.isNaN(ms)) { return iso.slice(0, 10); } return toDateYmdValue(new Date(ms)); } /** UTC 存储日 `Y-m-d` → 本地日历标记用 `yyyy-MM-dd` */ export function utcStorageDateToLocalMarkYmd(utcYmd: string): string { const match = DATE_YMD_RE.exec(utcYmd.trim()); if (!match) { return utcYmd; } const [, y, mo, d] = match; const ms = Date.UTC(Number(y), Number(mo) - 1, Number(d), 12, 0, 0, 0); return toDateYmdValue(new Date(ms)); } export function utcStorageDatesToLocalMarks(dates: string[]): string[] { return dates.map((day) => utcStorageDateToLocalMarkYmd(day)); } /** 开账建议:UTC 存储日 → 本地表单 `yyyy-MM-dd` */ export function utcStorageDateToLocalFormYmd(utcYmd: string): string { return utcStorageDateToLocalMarkYmd(utcYmd); } export function formatSettlementPeriodSpan( periodStart: string | undefined, periodEnd: string | undefined, ): string { const start = utcPeriodInstantToLocalYmd(periodStart); const end = utcPeriodInstantToLocalYmd(periodEnd); return `${start} ~ ${end}`; } export function isSettlementLocalDateRangeValid(startYmd: string, endYmd: string): boolean { const start = parseDateYmd(startYmd); const end = parseDateYmd(endYmd); if (start === null || end === null) { return false; } return start.getTime() <= end.getTime(); } function eachLocalYmdInRange(startYmd: string, endYmd: string): string[] { const start = parseDateYmd(startYmd); const end = parseDateYmd(endYmd); if (start === null || end === null || start.getTime() > end.getTime()) { return []; } const days: string[] = []; const cursor = new Date(start.getFullYear(), start.getMonth(), start.getDate()); const endMs = new Date(end.getFullYear(), end.getMonth(), end.getDate()).getTime(); while (cursor.getTime() <= endMs) { days.push(toDateYmdValue(cursor)); cursor.setDate(cursor.getDate() + 1); } return days; } /** 本地账期范围是否与已有账期日期重叠 */ export function settlementRangeOverlapsOccupiedDates( startYmd: string, endYmd: string, occupiedLocalYmds: string[], ): boolean { if (occupiedLocalYmds.length === 0) { return false; } const occupied = new Set(occupiedLocalYmds); return eachLocalYmdInRange(startYmd, endYmd).some((day) => occupied.has(day)); }