import { ApiError } from './errors.mjs'; export function validDate(value) { if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false; const time = Date.parse(`${value}T12:00:00Z`); return Number.isFinite(time) && new Date(time).toISOString().slice(0, 10) === value; } export function previousDate(value) { return new Date(Date.parse(`${value}T12:00:00Z`) - 86400000).toISOString().slice(0, 10); } export function tripIdentity(trip, serviceDate) { const departureTime = String(trip.departureTime ?? '').split(':'); const time = `${departureTime[0]?.padStart(2, '0')}:${departureTime[1]}:${departureTime[2] || '00'}`; return Buffer.from(JSON.stringify([serviceDate, time, trip.startStopId || null, trip.endStopId || null, trip.viaStopIds || [], trip.daysOfWeek || [], trip.exceptDaysOfWeek || [], trip.note || ''])).toString('base64url'); } function stopMap(rawStops) { if (!Array.isArray(rawStops)) throw new ApiError(502); return new Map(rawStops.map(stop => [Number(stop.stop_id), stop])); } function tripArrival(trip, target, stops, serviceDate) { const start = trip.startStopId ? stops.get(Number(trip.startStopId)) : undefined; const end = trip.endStopId ? stops.get(Number(trip.endStopId)) : undefined; if ((trip.startStopId && !start) || (trip.endStopId && !end)) throw new ApiError(502); const sequence = Number(target.sequence); const reverse = start && end && Number(start.sequence) > Number(end.sequence); if (reverse ? sequence > Number(start.sequence) || sequence < Number(end.sequence) : (start && sequence < Number(start.sequence)) || (end && sequence > Number(end.sequence))) return null; const time = typeof trip.departureTime === 'string' && /^([01]?\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?$/.exec(trip.departureTime); const targetOffset = Number(target.time_offset_minutes ?? 0); const startOffset = Number(start?.time_offset_minutes ?? 0); const offset = start && Number(start.sequence) > sequence ? startOffset - targetOffset : targetOffset - startOffset; if (!time || !Number.isFinite(sequence) || !Number.isFinite(offset) || offset < 0 || offset >= 1440) throw new ApiError(502); const scheduledAt = new Date(Date.parse(`${serviceDate}T00:00:00+03:00`) + (Number(time[1]) * 3600 + Number(time[2]) * 60 + Number(time[3] ?? 0) + offset * 60) * 1000).toISOString(); const local = new Date(Date.parse(scheduledAt) + 10800000).toISOString(); return { scheduledTime: local.slice(11, 16), scheduledAt }; } export function journeyStops(rawStops, departure) { const map = stopMap(rawStops); const result = rawStops.flatMap(stop => { if (!Number.isSafeInteger(Number(stop.stop_id)) || Number(stop.stop_id) <= 0 || typeof stop.name !== 'string' || !stop.name.trim()) throw new ApiError(502); const arrival = tripArrival(departure.trip, stop, map, departure.serviceDate); return arrival ? [{ id: Number(stop.stop_id), sequence: Number(stop.sequence), name: stop.name.trim(), ...arrival }] : []; }); return result.sort((a, b) => Date.parse(a.scheduledAt) - Date.parse(b.scheduledAt) || a.sequence - b.sequence); } // RideBus times are local to Belarus (UTC+3). Preserve the actual date when // a trip reaches a stop after midnight instead of wrapping its clock time. export function routeDepartures(route, rawStops, rawSchedule, stopId, serviceDate) { const stops = stopMap(rawStops); const target = rawStops.find(stop => Number(stop.stop_id) === stopId); if (!target) throw new ApiError(502); const schedule = rawSchedule?.schedule; if (!schedule || typeof schedule !== 'object') throw new ApiError(502); let trips = schedule.trips; if (typeof trips === 'string') { try { trips = JSON.parse(trips); } catch { throw new ApiError(502); } } if (!Array.isArray(trips) || trips.length === 0) { if (!Array.isArray(schedule.departure_times)) throw new ApiError(502); trips = schedule.departure_times.map(departureTime => ({ departureTime })); } const weekday = new Date(`${serviceDate}T12:00:00Z`).getUTCDay() || 7; const departures = []; let partial = false; for (const trip of trips) { if (!trip || typeof trip !== 'object') { partial = true; continue; } const included = Array.isArray(trip.daysOfWeek) ? trip.daysOfWeek.map(Number) : []; const excluded = Array.isArray(trip.exceptDaysOfWeek) ? trip.exceptDaysOfWeek.map(Number) : []; if ((included.length && !included.includes(weekday)) || excluded.includes(weekday)) continue; try { const arrival = tripArrival(trip, target, stops, serviceDate); if (arrival) departures.push({ routeId: route.id, routeNumber: route.number, direction: route.name, ...arrival, note: typeof trip.note === 'string' ? trip.note.trim() : '', tripKey: tripIdentity(trip, serviceDate), serviceDate, trip, }); } catch { partial = true; } } return { departures, partial }; }