Files
dukang/packages/domain/src/shanghai-date.ts
T

185 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 财务账单日 / 账期一律按中国北京时间(Asia/Shanghai,无夏令时) */
export const SHANGHAI_TIME_ZONE = 'Asia/Shanghai';
function pad2(n: number): string {
return String(n).padStart(2, '0');
}
export function shanghaiYmd(d: Date): string {
return d.toLocaleDateString('sv-SE', { timeZone: SHANGHAI_TIME_ZONE });
}
export function parseShanghaiYmd(ymd: string): Date {
return new Date(`${ymd}T00:00:00+08:00`);
}
export function startOfShanghaiDay(d: Date = new Date()): Date {
return parseShanghaiYmd(shanghaiYmd(d));
}
export function addShanghaiDays(d: Date, days: number): Date {
const [y, m, day] = shanghaiYmd(startOfShanghaiDay(d)).split('-').map(Number);
const utc = new Date(Date.UTC(y, m - 1, day + days));
const ymd = `${utc.getUTCFullYear()}-${pad2(utc.getUTCMonth() + 1)}-${pad2(utc.getUTCDate())}`;
return new Date(`${ymd}T00:00:00+08:00`);
}
/** 门店 T+1:核销窗口 [昨日 00:00, 今日 00:00);出账日 = 今天 */
export function shanghaiT1DayWindow(anchor = new Date()) {
const billDate = startOfShanghaiDay(anchor);
const start = addShanghaiDays(billDate, -1);
return { start, end: billDate, billDate };
}
/**
* 酒厂日滞后窗口(旧:每天出账只收 lagDays 前那一天)。
* 现网酒厂已改为「每 periodDays 天出一期」,见 shanghaiWineryPeriodWindow。
*/
export function shanghaiLaggedIssueWindow(anchor = new Date(), lagDays: number) {
const billDate = startOfShanghaiDay(anchor);
const start = addShanghaiDays(billDate, -lagDays);
const end = addShanghaiDays(start, 1);
return { start, end, billDate };
}
/** 两个北京自然日之间相差天数(b − a;可为负) */
export function shanghaiCalendarDaysBetween(a: Date, b: Date): number {
const sa = startOfShanghaiDay(a).getTime();
const sb = startOfShanghaiDay(b).getTime();
return Math.round((sb - sa) / (24 * 60 * 60 * 1000));
}
/** 是否酒厂出账日:相对 epoch,每 periodDays 天一次(首张 = epoch + periodDays */
export function isWineryIssueDay(anchor: Date, periodDays: number, epochYmd: string): boolean {
const billDate = startOfShanghaiDay(anchor);
const epoch = parseShanghaiYmd(epochYmd);
const diff = shanghaiCalendarDaysBetween(epoch, billDate);
return diff >= periodDays && diff % periodDays === 0;
}
/**
* 酒厂 T+N 期账单窗口:出账日 = anchor 当天;纳入 [出账日 periodDays, 出账日) 内完成的订单。
* 调用方应先用 isWineryIssueDay 判断是否出账日。
*/
export function shanghaiWineryPeriodWindow(anchor = new Date(), periodDays: number) {
const billDate = startOfShanghaiDay(anchor);
const start = addShanghaiDays(billDate, -periodDays);
return { start, end: billDate, billDate };
}
/** 订单完成日 → 所属酒厂出账日(落入下一期出账日) */
export function wineryIssueDateFromCompletedAt(
completedAt: Date,
periodDays: number,
epochYmd: string,
) {
const c = startOfShanghaiDay(completedAt);
const epoch = parseShanghaiYmd(epochYmd);
const daysFromEpoch = shanghaiCalendarDaysBetween(epoch, c);
if (daysFromEpoch < 0) {
return addShanghaiDays(epoch, periodDays);
}
const k = Math.floor(daysFromEpoch / periodDays) + 1;
return addShanghaiDays(epoch, k * periodDays);
}
/** 枚举 [from, to] 内(含)的全部酒厂出账日 */
export function listWineryIssueDays(from: Date, to: Date, periodDays: number, epochYmd: string): Date[] {
const start = startOfShanghaiDay(from);
const end = startOfShanghaiDay(to);
const out: Date[] = [];
for (let d = start; d.getTime() <= end.getTime(); d = addShanghaiDays(d, 1)) {
if (isWineryIssueDay(d, periodDays, epochYmd)) out.push(d);
}
return out;
}
export function shanghaiMonthRange(year: number, month: number): { start: Date; endExclusive: Date } {
const start = new Date(`${year}-${pad2(month)}-01T00:00:00+08:00`);
const endMonth = month === 12 ? 1 : month + 1;
const endYear = month === 12 ? year + 1 : year;
const endExclusive = new Date(`${endYear}-${pad2(endMonth)}-01T00:00:00+08:00`);
return { start, endExclusive };
}
export function shanghaiMonthLastInstant(year: number, month: number): Date {
return new Date(shanghaiMonthRange(year, month).endExclusive.getTime() - 1);
}
export function previousShanghaiMonth(anchor = new Date()): { year: number; month: number } {
const [y, m] = shanghaiYmd(anchor).split('-').map(Number);
if (m === 1) return { year: y - 1, month: 12 };
return { year: y, month: m - 1 };
}
export function shanghaiYearMonth(anchor = new Date()): { year: number; month: number } {
const [y, m] = shanghaiYmd(anchor).split('-').map(Number);
return { year: y, month: m };
}
/** 账期展示:以 periodStart 所在北京月的首末日,避免 UTC 23:59:59 滚到下月 */
export function shanghaiPeriodYmds(periodStart: Date): { periodStart: string; periodEnd: string } {
const [y, m] = shanghaiYmd(periodStart).split('-').map(Number);
const { start, endExclusive } = shanghaiMonthRange(y, m);
return {
periodStart: shanghaiYmd(start),
periodEnd: shanghaiYmd(new Date(endExclusive.getTime() - 1)),
};
}
/** 0=周日 … 6=周六,按北京日历日 */
export function shanghaiWeekday(d: Date): number {
return new Date(`${shanghaiYmd(d)}T12:00:00+08:00`).getUTCDay();
}
/** 北京自然周:周一 00:00 ~ 下周一 00:00(不含) */
export function shanghaiWeekRange(anchor = new Date()): { start: Date; endExclusive: Date } {
const dayStart = startOfShanghaiDay(anchor);
const wd = shanghaiWeekday(dayStart);
const daysFromMonday = wd === 0 ? 6 : wd - 1;
const start = addShanghaiDays(dayStart, -daysFromMonday);
return { start, endExclusive: addShanghaiDays(start, 7) };
}
export function shanghaiWeekLastInstant(anchor = new Date()): Date {
return new Date(shanghaiWeekRange(anchor).endExclusive.getTime() - 1);
}
export function previousShanghaiWeek(anchor = new Date()): { start: Date; endExclusive: Date } {
return shanghaiWeekRange(addShanghaiDays(shanghaiWeekRange(anchor).start, -1));
}
/** 下一个合伙人出账时刻:周一 08:00(若尚未到本周一 08:00 则为当日) */
export function nextPartnerBillIssueAt(anchor = new Date()): Date {
const { start } = shanghaiWeekRange(anchor);
const thisMonday8 = new Date(start.getTime() + 8 * 60 * 60 * 1000);
if (anchor.getTime() < thisMonday8.getTime()) return thisMonday8;
return new Date(addShanghaiDays(start, 7).getTime() + 8 * 60 * 60 * 1000);
}
export function shanghaiPeriodYmdsForWeek(periodStart: Date): { periodStart: string; periodEnd: string } {
const { start } = shanghaiWeekRange(periodStart);
return {
periodStart: shanghaiYmd(start),
periodEnd: shanghaiYmd(addShanghaiDays(start, 6)),
};
}
/** 周账用真实起止日;跨度超过 8 天则按月展示(兼容历史月账) */
export function shanghaiBillPeriodYmds(
periodStart: Date,
periodEnd?: Date,
): { periodStart: string; periodEnd: string } {
if (periodEnd) {
const days = shanghaiCalendarDaysBetween(periodStart, periodEnd);
if (days >= 0 && days <= 8) {
return {
periodStart: shanghaiYmd(periodStart),
periodEnd: shanghaiYmd(periodEnd),
};
}
}
return shanghaiPeriodYmds(periodStart);
}