@@ -0,0 +1,237 @@
|
||||
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) };
|
||||
}
|
||||
Reference in New Issue
Block a user