238 lines
7.0 KiB
TypeScript
238 lines
7.0 KiB
TypeScript
import {
|
|
addShanghaiDays,
|
|
parseShanghaiYmd,
|
|
shanghaiMonthRange,
|
|
shanghaiWeekRange,
|
|
shanghaiYearMonth,
|
|
shanghaiYmd,
|
|
startOfShanghaiDay,
|
|
} from './shanghai-date';
|
|
|
|
export const DASHBOARD_GRANULARITIES = ['day', 'week', 'month', 'quarter', 'year'] as const;
|
|
export type DashboardGranularity = (typeof DASHBOARD_GRANULARITIES)[number];
|
|
|
|
function pad2(n: number): string {
|
|
return String(n).padStart(2, '0');
|
|
}
|
|
|
|
export function shanghaiQuarterIndex(d: Date): { year: number; quarter: number } {
|
|
const [y, m] = shanghaiYmd(d).split('-').map(Number);
|
|
return { year: y, quarter: Math.ceil(m / 3) };
|
|
}
|
|
|
|
export function shanghaiQuarterRange(
|
|
year: number,
|
|
quarter: number,
|
|
): { start: Date; endExclusive: Date } {
|
|
const startMonth = (quarter - 1) * 3 + 1;
|
|
const endMonth = startMonth + 3;
|
|
const endYear = endMonth > 12 ? year + 1 : year;
|
|
const em = endMonth > 12 ? endMonth - 12 : endMonth;
|
|
return {
|
|
start: new Date(`${year}-${pad2(startMonth)}-01T00:00:00+08:00`),
|
|
endExclusive: new Date(`${endYear}-${pad2(em)}-01T00:00:00+08:00`),
|
|
};
|
|
}
|
|
|
|
export function shanghaiBucketKey(d: Date, grain: DashboardGranularity): string {
|
|
const ymd = shanghaiYmd(d);
|
|
const [y, m] = ymd.split('-').map(Number);
|
|
switch (grain) {
|
|
case 'day':
|
|
return ymd;
|
|
case 'week':
|
|
return shanghaiYmd(shanghaiWeekRange(d).start);
|
|
case 'month':
|
|
return `${y}-${pad2(m)}`;
|
|
case 'quarter':
|
|
return `${y}-Q${Math.ceil(m / 3)}`;
|
|
case 'year':
|
|
return String(y);
|
|
}
|
|
}
|
|
|
|
export function shanghaiBucketLabel(key: string, grain: DashboardGranularity): string {
|
|
switch (grain) {
|
|
case 'day':
|
|
return key.slice(5);
|
|
case 'week':
|
|
return `${key.slice(5)}周`;
|
|
default:
|
|
return key;
|
|
}
|
|
}
|
|
|
|
export type ShanghaiBucket = {
|
|
key: string;
|
|
label: string;
|
|
start: Date;
|
|
endExclusive: Date;
|
|
};
|
|
|
|
export function eachShanghaiBuckets(
|
|
from: Date,
|
|
to: Date,
|
|
grain: DashboardGranularity,
|
|
): ShanghaiBucket[] {
|
|
const startDay = startOfShanghaiDay(from);
|
|
const endDay = startOfShanghaiDay(to);
|
|
const out: ShanghaiBucket[] = [];
|
|
|
|
if (grain === 'day') {
|
|
for (let d = startDay; d.getTime() <= endDay.getTime(); d = addShanghaiDays(d, 1)) {
|
|
const key = shanghaiYmd(d);
|
|
out.push({
|
|
key,
|
|
label: shanghaiBucketLabel(key, grain),
|
|
start: d,
|
|
endExclusive: addShanghaiDays(d, 1),
|
|
});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
if (grain === 'week') {
|
|
let { start } = shanghaiWeekRange(startDay);
|
|
const lastStart = shanghaiWeekRange(endDay).start;
|
|
while (start.getTime() <= lastStart.getTime()) {
|
|
const key = shanghaiYmd(start);
|
|
const endExclusive = addShanghaiDays(start, 7);
|
|
out.push({ key, label: shanghaiBucketLabel(key, grain), start, endExclusive });
|
|
start = endExclusive;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
if (grain === 'month') {
|
|
let { year, month } = shanghaiYearMonth(startDay);
|
|
const end = shanghaiYearMonth(endDay);
|
|
while (year < end.year || (year === end.year && month <= end.month)) {
|
|
const range = shanghaiMonthRange(year, month);
|
|
const key = `${year}-${pad2(month)}`;
|
|
out.push({ key, label: key, start: range.start, endExclusive: range.endExclusive });
|
|
month += 1;
|
|
if (month > 12) {
|
|
month = 1;
|
|
year += 1;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
if (grain === 'quarter') {
|
|
let { year, quarter } = shanghaiQuarterIndex(startDay);
|
|
const end = shanghaiQuarterIndex(endDay);
|
|
while (year < end.year || (year === end.year && quarter <= end.quarter)) {
|
|
const range = shanghaiQuarterRange(year, quarter);
|
|
const key = `${year}-Q${quarter}`;
|
|
out.push({ key, label: key, start: range.start, endExclusive: range.endExclusive });
|
|
quarter += 1;
|
|
if (quarter > 4) {
|
|
quarter = 1;
|
|
year += 1;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
let year = Number(shanghaiYmd(startDay).slice(0, 4));
|
|
const endYear = Number(shanghaiYmd(endDay).slice(0, 4));
|
|
while (year <= endYear) {
|
|
out.push({
|
|
key: String(year),
|
|
label: String(year),
|
|
start: new Date(`${year}-01-01T00:00:00+08:00`),
|
|
endExclusive: new Date(`${year + 1}-01-01T00:00:00+08:00`),
|
|
});
|
|
year += 1;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** 按粒度把日期往前挪一档(日/周/月/季/年),用于环比窗口 */
|
|
export function addShanghaiGrain(d: Date, grain: DashboardGranularity, delta: number): Date {
|
|
const start = startOfShanghaiDay(d);
|
|
switch (grain) {
|
|
case 'day':
|
|
return addShanghaiDays(start, delta);
|
|
case 'week':
|
|
return addShanghaiDays(start, delta * 7);
|
|
case 'month':
|
|
return addShanghaiMonths(start, delta);
|
|
case 'quarter':
|
|
return addShanghaiMonths(start, delta * 3);
|
|
case 'year':
|
|
return addShanghaiMonths(start, delta * 12);
|
|
}
|
|
}
|
|
|
|
function addShanghaiMonths(d: Date, delta: number): Date {
|
|
const [y, m, day] = shanghaiYmd(d).split('-').map(Number);
|
|
const utc = new Date(Date.UTC(y, m - 1 + delta, 1));
|
|
const ty = utc.getUTCFullYear();
|
|
const tm = utc.getUTCMonth() + 1;
|
|
const last = new Date(Date.UTC(ty, tm, 0)).getUTCDate();
|
|
return parseShanghaiYmd(`${ty}-${pad2(tm)}-${pad2(Math.min(day, last))}`);
|
|
}
|
|
|
|
/** 整段起止按粒度回退一档:日窗口昨天~今天 → 前天~昨天 */
|
|
export function previousShanghaiRangeByGrain(
|
|
from: Date,
|
|
to: Date,
|
|
grain: DashboardGranularity,
|
|
): { from: Date; to: Date } {
|
|
return {
|
|
from: addShanghaiGrain(from, grain, -1),
|
|
to: addShanghaiGrain(to, grain, -1),
|
|
};
|
|
}
|
|
|
|
/** 环比:(本期 − 上期) / 上期;上期 0 且本期 0 → null;上期 0 且本期 > 0 → 1 */
|
|
export function periodMomRatio(curr: number, prev: number): number | null {
|
|
if (prev === 0) return curr === 0 ? null : 1;
|
|
return (curr - prev) / prev;
|
|
}
|
|
|
|
/** 默认窗口 = 上一档起点 ~ 今天(日=昨天~今天,周=上周一~今天,以此类推) */
|
|
export function defaultShanghaiRange(
|
|
grain: DashboardGranularity,
|
|
anchor = new Date(),
|
|
): { from: Date; to: Date } {
|
|
const to = startOfShanghaiDay(anchor);
|
|
switch (grain) {
|
|
case 'day':
|
|
return { from: addShanghaiDays(to, -1), to };
|
|
case 'week': {
|
|
const thisMonday = shanghaiWeekRange(to).start;
|
|
return { from: addShanghaiDays(thisMonday, -7), to };
|
|
}
|
|
case 'month': {
|
|
const { year, month } = shanghaiYearMonth(to);
|
|
const prev = month === 1 ? { year: year - 1, month: 12 } : { year, month: month - 1 };
|
|
return { from: shanghaiMonthRange(prev.year, prev.month).start, to };
|
|
}
|
|
case 'quarter': {
|
|
const { year, quarter } = shanghaiQuarterIndex(to);
|
|
let y = year;
|
|
let q = quarter - 1;
|
|
if (q <= 0) {
|
|
q = 4;
|
|
y -= 1;
|
|
}
|
|
return { from: shanghaiQuarterRange(y, q).start, to };
|
|
}
|
|
case 'year': {
|
|
const y = Number(shanghaiYmd(to).slice(0, 4));
|
|
return { from: new Date(`${y - 1}-01-01T00:00:00+08:00`), to };
|
|
}
|
|
}
|
|
}
|
|
|
|
export function defaultShanghaiRangeYmds(
|
|
grain: DashboardGranularity,
|
|
anchor = new Date(),
|
|
): { from: string; to: string } {
|
|
const r = defaultShanghaiRange(grain, anchor);
|
|
return { from: shanghaiYmd(r.from), to: shanghaiYmd(r.to) };
|
|
}
|