151 lines
3.8 KiB
TypeScript
151 lines
3.8 KiB
TypeScript
export interface Range {
|
|
end: string;
|
|
start: string;
|
|
}
|
|
|
|
export type ViewRange =
|
|
| "1D"
|
|
| "1W"
|
|
| "last7"
|
|
| "1M"
|
|
| "last30"
|
|
| "MTD"
|
|
| "3M"
|
|
| "last90"
|
|
| "QTD"
|
|
| "6M"
|
|
| "1Y"
|
|
| "YTD"
|
|
| "last365";
|
|
|
|
const DYNAMIC = new Set<ViewRange>([
|
|
"last7",
|
|
"last30",
|
|
"last90",
|
|
"last365",
|
|
"MTD",
|
|
"QTD",
|
|
"YTD",
|
|
]);
|
|
|
|
const pad = (n: number) => String(n).padStart(2, "0");
|
|
|
|
export const iso = (d: Date) =>
|
|
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
|
|
export function parse(value: string): Date {
|
|
const [y, m, d] = value.slice(0, 10).split("-").map(Number);
|
|
return new Date(y, m - 1, d);
|
|
}
|
|
|
|
export const today = () => iso(new Date());
|
|
|
|
export function addDays(value: string, days: number): string {
|
|
const d = parse(value);
|
|
d.setDate(d.getDate() + days);
|
|
return iso(d);
|
|
}
|
|
|
|
export function addMonths(value: string, months: number): string {
|
|
const d = parse(value);
|
|
const day = d.getDate();
|
|
d.setDate(1);
|
|
d.setMonth(d.getMonth() + months);
|
|
const last = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
|
|
d.setDate(Math.min(day, last));
|
|
return iso(d);
|
|
}
|
|
|
|
export const isDynamic = (view: ViewRange) => DYNAMIC.has(view);
|
|
|
|
function startOf(value: string, view: ViewRange): string {
|
|
const d = parse(value);
|
|
switch (view) {
|
|
case "1D":
|
|
return value;
|
|
case "1W": {
|
|
const shift = (d.getDay() + 6) % 7;
|
|
return addDays(value, -shift);
|
|
}
|
|
case "3M":
|
|
return iso(
|
|
new Date(d.getFullYear(), Math.floor(d.getMonth() / 3) * 3, 1)
|
|
);
|
|
case "6M":
|
|
return iso(new Date(d.getFullYear(), d.getMonth() < 6 ? 0 : 6, 1));
|
|
case "1Y":
|
|
return iso(new Date(d.getFullYear(), 0, 1));
|
|
default:
|
|
return iso(new Date(d.getFullYear(), d.getMonth(), 1));
|
|
}
|
|
}
|
|
|
|
const LENGTH_MONTHS: Partial<Record<ViewRange, number>> = {
|
|
"1M": 1,
|
|
"1Y": 12,
|
|
"3M": 3,
|
|
"6M": 6,
|
|
};
|
|
|
|
export function periodOf(value: string, view: ViewRange): Range {
|
|
const now = today();
|
|
switch (view) {
|
|
case "last7":
|
|
return { end: now, start: addDays(now, -7) };
|
|
case "last30":
|
|
return { end: now, start: addDays(now, -30) };
|
|
case "last90":
|
|
return { end: now, start: addDays(now, -90) };
|
|
case "last365":
|
|
return { end: now, start: addDays(now, -365) };
|
|
case "MTD":
|
|
return { end: now, start: startOf(now, "1M") };
|
|
case "QTD":
|
|
return { end: now, start: startOf(now, "3M") };
|
|
case "YTD":
|
|
return { end: now, start: startOf(now, "1Y") };
|
|
case "1D":
|
|
return { end: value, start: value };
|
|
case "1W": {
|
|
const start = startOf(value, "1W");
|
|
return { end: addDays(start, 6), start };
|
|
}
|
|
default: {
|
|
const start = startOf(value, view);
|
|
const months = LENGTH_MONTHS[view] ?? 1;
|
|
return { end: addDays(addMonths(start, months), -1), start };
|
|
}
|
|
}
|
|
}
|
|
|
|
export function shiftPeriod(range: Range, view: ViewRange, dir: 1 | -1): Range {
|
|
const fixed = isDynamic(view) ? "1M" : view;
|
|
const aligned = periodOf(range.start, fixed);
|
|
if (aligned.start === range.start && aligned.end === range.end) {
|
|
const anchor = dir === 1 ? addDays(range.end, 1) : addDays(range.start, -1);
|
|
return periodOf(anchor, fixed);
|
|
}
|
|
const days = daysBetween(range.start, range.end) + 1;
|
|
return {
|
|
end: addDays(range.end, dir * days),
|
|
start: addDays(range.start, dir * days),
|
|
};
|
|
}
|
|
|
|
export function daysBetween(a: string, b: string): number {
|
|
return Math.round((parse(b).getTime() - parse(a).getTime()) / 86_400_000);
|
|
}
|
|
|
|
export function eachDay(range: Range): string[] {
|
|
const days: string[] = [];
|
|
for (let d = range.start; d <= range.end; d = addDays(d, 1)) {
|
|
days.push(d);
|
|
}
|
|
return days;
|
|
}
|
|
|
|
export function contains(range: Range, value: string): boolean {
|
|
const day = value.slice(0, 10);
|
|
return day >= range.start && day <= range.end;
|
|
}
|