Files
backend/src/modules/investments/investment-calendar.ts
T

60 lines
1.9 KiB
TypeScript

import { ISO_DATE_LENGTH, ISO_DATE_START } from '../../common/time';
const UTC_MONTH_INDEX_OFFSET = 1;
const NEXT_DAY_OFFSET = 1;
const MOSCOW_UTC_OFFSET_HOURS = 3;
export type DateOnlyRange = { from: string; to: string };
const moscow = 'Europe/Moscow';
const moscowFormatter = new Intl.DateTimeFormat('en-CA', {
timeZone: moscow,
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
export function moscowDate(value: Date): string {
const parts = moscowFormatter.formatToParts(value);
const get = (type: string) => parts.find((part) => part.type === type)?.value;
return `${get('year')}-${get('month')}-${get('day')}`;
}
// Inclusive Moscow calendar dates become a half-open instant range, capped at now.
export function dateOnlyBounds(
range: DateOnlyRange,
now = new Date(),
): { from: Date; to: Date } {
const from = dateParts(range.from);
const end = dateParts(range.to);
const to = new Date(
Date.UTC(end[0], end[1] - UTC_MONTH_INDEX_OFFSET, end[2] + NEXT_DAY_OFFSET, -MOSCOW_UTC_OFFSET_HOURS),
);
return {
from: new Date(
Date.UTC(from[0], from[1] - UTC_MONTH_INDEX_OFFSET, from[2], -MOSCOW_UTC_OFFSET_HOURS),
),
to: to > now ? now : to,
};
}
export function dateParts(value: string): [number, number, number] {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
if (!match) throw new Error('Invalid date');
const parts = [Number(match[1]), Number(match[2]), Number(match[3])] as [
number,
number,
number,
];
if (utcDate(value).toISOString().slice(ISO_DATE_START, ISO_DATE_LENGTH) !== value) {
throw new Error('Invalid date');
}
return parts;
}
export function utcDate(value: string): Date {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
if (!match) return new Date(NaN);
return new Date(
Date.UTC(Number(match[1]), Number(match[2]) - UTC_MONTH_INDEX_OFFSET, Number(match[3])),
);
}