88 lines
4.8 KiB
JavaScript
88 lines
4.8 KiB
JavaScript
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 };
|
|
}
|