Add local time handling for ETA calculations and tests

This commit is contained in:
2026-07-10 23:23:36 +03:00
parent edf3c1052b
commit 93b266f26d
3 changed files with 94 additions and 23 deletions

View File

@@ -1,11 +1,13 @@
const pool = require('../config/database');
const logger = require('../config/logger');
const { getLocalDateTime, getIsoWeekday } = require('../utils/etaTime');
class EtaController {
// Calculate ETA for a stop on a route
async calculateEta(req, res) {
const { stopId, routeId } = req.body;
const currentTime = new Date();
const localNow = this.getLocalDateTime(currentTime);
if (!stopId || !routeId) {
return res.status(400).json({
@@ -17,7 +19,8 @@ class EtaController {
try {
// Get schedule type for today (holiday-aware)
const typeResult = await pool.query(
'SELECT get_schedule_type(CURRENT_DATE) as schedule_type'
'SELECT get_schedule_type($1::date) as schedule_type',
[localNow.date]
);
let scheduleType = typeResult.rows[0].schedule_type;
@@ -27,10 +30,10 @@ class EtaController {
FROM schedules
WHERE route_id = $1
AND day_type = $2
AND (valid_from IS NULL OR valid_from <= CURRENT_DATE)
AND (valid_until IS NULL OR valid_until >= CURRENT_DATE)
AND (valid_from IS NULL OR valid_from <= $3::date)
AND (valid_until IS NULL OR valid_until >= $3::date)
LIMIT 1
`, [routeId, scheduleType]);
`, [routeId, scheduleType, localNow.date]);
// If holiday and no specific holiday schedule, fall back to sunday
if (scheduleResult.rows.length === 0 && scheduleType === 'holiday') {
@@ -40,10 +43,10 @@ class EtaController {
FROM schedules
WHERE route_id = $1
AND day_type = 'sunday'
AND (valid_from IS NULL OR valid_from <= CURRENT_DATE)
AND (valid_until IS NULL OR valid_until >= CURRENT_DATE)
AND (valid_from IS NULL OR valid_from <= $2::date)
AND (valid_until IS NULL OR valid_until >= $2::date)
LIMIT 1
`, [routeId]);
`, [routeId, localNow.date]);
}
if (scheduleResult.rows.length === 0) {
@@ -80,7 +83,7 @@ class EtaController {
));
// Calculate ETAs
const currentTimeStr = currentTime.toTimeString().split(' ')[0];
const currentTimeStr = localNow.time;
const etas = [];
for (const trip of activeTrips) {
@@ -90,7 +93,7 @@ class EtaController {
const delay = await this.getHistoricalDelay(
routeId,
stopId,
currentTime.getHours(),
localNow.hour,
scheduleType === 'holiday' ? 'sunday' : scheduleType
);
@@ -175,11 +178,13 @@ class EtaController {
// Helper: Calculate ETA for a specific route (internal use)
async calculateEtaForRoute(stopId, routeId) {
const currentTime = new Date();
const localNow = this.getLocalDateTime(currentTime);
let dayType = this.getDayType(currentTime);
try {
const typeResult = await pool.query(
'SELECT get_schedule_type(CURRENT_DATE) as schedule_type'
'SELECT get_schedule_type($1::date) as schedule_type',
[localNow.date]
);
dayType = typeResult.rows[0].schedule_type;
} catch (error) {
@@ -190,10 +195,10 @@ class EtaController {
SELECT departure_times, trips
FROM schedules
WHERE route_id = $1 AND day_type = $2
AND (valid_from IS NULL OR valid_from <= CURRENT_DATE)
AND (valid_until IS NULL OR valid_until >= CURRENT_DATE)
AND (valid_from IS NULL OR valid_from <= $3::date)
AND (valid_until IS NULL OR valid_until >= $3::date)
LIMIT 1
`, [routeId, dayType]);
`, [routeId, dayType, localNow.date]);
if (scheduleResult.rows.length === 0 && dayType === 'holiday') {
dayType = 'sunday';
@@ -201,10 +206,10 @@ class EtaController {
SELECT departure_times, trips
FROM schedules
WHERE route_id = $1 AND day_type = 'sunday'
AND (valid_from IS NULL OR valid_from <= CURRENT_DATE)
AND (valid_until IS NULL OR valid_until >= CURRENT_DATE)
AND (valid_from IS NULL OR valid_from <= $2::date)
AND (valid_until IS NULL OR valid_until >= $2::date)
LIMIT 1
`, [routeId]);
`, [routeId, localNow.date]);
}
if (scheduleResult.rows.length === 0) return [];
@@ -227,7 +232,7 @@ class EtaController {
this.tripServesStop(trip, targetStop, stopsById)
));
const currentTimeStr = currentTime.toTimeString().split(' ')[0];
const currentTimeStr = localNow.time;
const etas = [];
for (const trip of activeTrips) {
@@ -237,7 +242,7 @@ class EtaController {
const delay = await this.getHistoricalDelay(
routeId,
stopId,
currentTime.getHours(),
localNow.hour,
dayType
);
@@ -278,12 +283,17 @@ class EtaController {
// Helper: Determine day type
getDayType(date) {
const day = date.getDay();
if (day === 0) return 'sunday';
if (day === 6) return 'saturday';
const isoWeekday = this.getIsoWeekday(date);
if (isoWeekday === 7) return 'sunday';
if (isoWeekday === 6) return 'saturday';
return 'weekday';
}
// All schedule and ETA comparisons use Polotsk local civil time.
getLocalDateTime(date = new Date()) {
return getLocalDateTime(date);
}
// Helper: Add minutes to time string
addMinutesToTime(timeStr, minutes) {
const [hours, mins, secs] = timeStr.split(':').map(Number);
@@ -380,8 +390,7 @@ class EtaController {
}
getIsoWeekday(date) {
const day = date.getDay();
return day === 0 ? 7 : day;
return getIsoWeekday(date);
}
publicTripInfo(trip) {

View File

@@ -0,0 +1,46 @@
const ETA_TIME_ZONE = 'Europe/Minsk';
function getLocalDateTime(date = new Date()) {
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone: ETA_TIME_ZONE,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hourCycle: 'h23'
}).formatToParts(date).reduce((result, part) => {
if (part.type !== 'literal') result[part.type] = part.value;
return result;
}, {});
return {
date: `${parts.year}-${parts.month}-${parts.day}`,
time: `${parts.hour}:${parts.minute}:${parts.second}`,
hour: Number(parts.hour)
};
}
function getIsoWeekday(date = new Date()) {
const weekday = new Intl.DateTimeFormat('en-US', {
timeZone: ETA_TIME_ZONE,
weekday: 'short'
}).format(date);
return {
Mon: 1,
Tue: 2,
Wed: 3,
Thu: 4,
Fri: 5,
Sat: 6,
Sun: 7
}[weekday];
}
module.exports = {
ETA_TIME_ZONE,
getIsoWeekday,
getLocalDateTime
};

View File

@@ -0,0 +1,16 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { ETA_TIME_ZONE, getIsoWeekday, getLocalDateTime } = require('../src/utils/etaTime');
test('ETA uses Europe/Minsk local date and time instead of UTC', () => {
const utcDate = new Date('2026-07-10T21:30:15.000Z');
assert.equal(ETA_TIME_ZONE, 'Europe/Minsk');
assert.deepEqual(getLocalDateTime(utcDate), {
date: '2026-07-11',
time: '00:30:15',
hour: 0
});
assert.equal(getIsoWeekday(utcDate), 6);
});