export function isCalendarDate(value: string): boolean { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value) if (!match) return false const year = Number(match[1]), month = Number(match[2]), day = Number(match[3]) if (year < 1 || month < 1 || month > 12 || day < 1) return false const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) return day <= [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1] } export function isStatisticsDate(value: string): boolean { if (isCalendarDate(value)) return true const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,3})?(Z|[+-]\d{2}:\d{2})$/.exec(value) return Boolean(match && isCalendarDate(match[1]) && Number(match[2]) < 24 && Number(match[3]) < 60 && Number(match[4]) < 60 && Number.isFinite(Date.parse(value))) } export function readStatisticsRange( params: URLSearchParams, fallback: { from: string; to: string }, now = Date.now(), ): { range: { from: string; to: string }; error: string | null } { const from = params.get('from'), to = params.get('to') if (!params.has('from') && !params.has('to')) return { range: fallback, error: null } const invalid = { range: fallback, error: 'Период в ссылке некорректен. Укажите две существующие даты или ISO-время с часовым поясом; начало должно быть раньше конца периода и текущего времени.' } if (params.getAll('from').length !== 1 || params.getAll('to').length !== 1 || !from || !to || !isStatisticsDate(from) || !isStatisticsDate(to)) return invalid const start = Date.parse(from.includes('T') ? from : from + 'T00:00:00Z') // Backend treats date-only `to` as an inclusive UTC day and caps it at now. const end = Date.parse(to.includes('T') ? to : to + 'T00:00:00Z') + (to.includes('T') ? 0 : 86400000) if (start >= Math.min(end, now)) return invalid return { range: { from, to }, error: null } }