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