feat: enhance schedule import functionality with support for day sections and restrictions

This commit is contained in:
2026-09-16 23:16:35 +03:00
parent 595ad6456b
commit d32c15c85e
7 changed files with 174 additions and 14 deletions

View File

@@ -0,0 +1,53 @@
<h1 class="entry-title">Маршрут №24</h1><div class="entry-content">
<table>
<caption> <a href="http://ap2polotsk.of.by/ap2/polotsk-gorod/">◄ к списку маршрутов</a> </caption>
<tbody>
<tr>
<td colspan="2" bgcolor="#00B7FF"><center><b>Понедельник – четверг</b></center></td>
</tr>
<tr>
<td>Автовокзал</td>
<td><span class="notranslate">06:00 | 06:14 | 07:00 | 07:47 | 07:53 | 10:00 | 12:00 | 14:00 | 16:02 | 17:00 | 22:05</span></td>
</tr>
<tr>
<td>Полоцк – Стекловолокно</td>
<td><span class="notranslate">06:35 | 07:20 | 07:30 | 08:12 | 08:19 | 08:25 | 10:30 | 12:30 | 15:35 | 16:30 | 17:25 | 23:30</span></td>
</tr>
</tbody>
</table>
<table>
<tbody>
<tr>
<td colspan="2" bgcolor="#00B7FF"><center><b>Пятница</b></center></td>
</tr>
<tr>
<td>Автовокзал</td>
<td><span class="notranslate">06:00 | 06:14 | 07:00 | 07:47 | 07:53 | 10:00 | 12:00 | 14:00 | 15:45 | 16:02 | 22:05</span></td>
</tr>
<tr>
<td>Полоцк – Стекловолокно</td>
<td><span class="notranslate">06:35 | 07:20 | 07:30 | 08:12 | 08:19 | 08:25 | 10:30 | 12:30 | 15:35 | 16:15 | 16:30 | 23:30</span></td>
</tr>
</tbody>
</table>
<table>
<tbody>
<tr>
<td colspan="2" bgcolor="#FFF56E"><center><b>Выходной день</b></center></td>
</tr>
<tr>
<td>Автовокзал</td>
<td><span class="notranslate">06:00 | 14:05 | 22:05</span></td>
</tr>
<tr>
<td>Полоцк – Стекловолокно</td>
<td><span class="notranslate">07:30 | 15:30 | 23:30</span></td>
</tr>
</tbody>
</table>
<div style="text-indent: 40px;" align="justify"><b>Остановочные пункты:</b></div>
<p>Прямое направление: «Автовокзал», «Железнодорожный вокзал», «Экономический колледж», «Олимпиец», «Спортклуб», «Энергосбыт», «Заполотье», «Экимань», «Курган Бессмертия», «Вильнюсское шоссе», «Ксты», «Полоцкгаз», «Полоцк – Стекловолокно».<br>
Обратное направление: «Полоцк – Стекловолокно», «Полоцкгаз», «Ксты», «Вильнюсское шоссе», «ТРЦ Манеж», «Курган Бессмертия», «Экимань», «Заполотье», «Энергосбыт», «Спортклуб», «Олимпиец», «Экономический колледж», «Железнодорожный вокзал», «Автовокзал»</p>
<div><center><b>Схема движения</b></center></div>
<p><a href="http://ap2polotsk.of.by/ap2/wp-content/uploads/2026/07/24-shema.png"><img decoding="async" class="aligncenter" src="http://ap2polotsk.of.by/ap2/wp-content/uploads/2026/07/24-shema.png"></a></p>
</div>

View File

@@ -146,6 +146,27 @@ test('PostgreSQL import lifecycle, concurrency, conflicts, rollback and news rev
assert.equal(responseBody.trips.some(t => t.departureTime === '05:23:00'), false);
assert.equal(Number((await pool.query("SELECT count(*) FROM schedules WHERE day_type='holiday'")).rows[0].count), 2);
// Route 24 uses separate Mon–Thu and Friday tables; persist their restrictions together.
sourceHtml = await fixture('route24');
const source24 = parseSchedule(sourceHtml, 'https://ap2polotsk.of.by/ap2/m-24/');
const ids24 = [];
for (const direction of source24.directions) {
const id = (await pool.query("INSERT INTO routes(route_number,name,type) VALUES('24',$1,'bus') RETURNING id", [direction.label])).rows[0].id;
ids24.push(id);
for (const [index, name] of [direction.stops[0], direction.stops.at(-1)].entries()) {
const stop = (await pool.query("INSERT INTO stops(name,location) VALUES($1,ST_GeogFromText('POINT(28.77 55.48)')) RETURNING id", [name])).rows[0].id;
await pool.query('INSERT INTO route_stops(route_id,stop_id,sequence) VALUES($1,$2,$3)', [id,stop,index+1]);
}
}
const inspection24 = await service.inspect({ routeIds: ids24, sourceUrl: 'https://ap2polotsk.of.by/ap2/m-24/', userId:1 });
const preview24 = await service.preview({ inspectionId:inspection24.id, mappings:inspection24.mappings, validFrom, userId:1 });
await service.apply(preview24.id);
const saved24 = (await pool.query("SELECT trips FROM schedules WHERE route_id=$1 AND day_type='weekday'", [ids24[0]])).rows[0].trips;
assert.equal(saved24.filter(t => t.departureTime === '06:00:00').length, 1);
assert.deepEqual(saved24.find(t => t.departureTime === '17:00:00').daysOfWeek, [1,2,3,4]);
assert.deepEqual(saved24.find(t => t.departureTime === '15:45:00').daysOfWeek, [5]);
assert.equal(Number((await pool.query('SELECT count(*) FROM schedules WHERE route_id=ANY($1::int[])', [ids24])).rows[0].count), 6);
let news = await service.news(true);
assert.equal(news.length, 3);
await service.reviewNews({ url: news[0].url, contentHash: news[0].content_hash });

View File

@@ -162,3 +162,40 @@ test('weekend day restrictions split correctly and contradictory day sections ar
const invalid = parseSchedule(fixture('route9').replace('по пятницам', 'по субботам'), annotated.url);
assert.throws(() => buildDirectionPlan(invalid, 'Швейная фабрика', state, '2026-09-16', '2026-09-16'), /противоречит/);
});
test('route 24 splits Monday–Thursday, Friday and weekends without duplicate daily departures', () => {
const source24 = parseSchedule(fixture('route24'), 'https://ap2polotsk.of.by/ap2/m-24/');
assert.deepEqual(source24.warnings, []);
assert.equal(source24.directions.length, 2);
assert.deepEqual(departureStops(source24), ['Автовокзал', 'Полоцк – Стекловолокно']);
assert.deepEqual(source24.rows.map(row => row.daysOfWeek), [[1,2,3,4], [1,2,3,4], [5], [5], [], []]);
for (const direction of source24.directions) {
const snapshot24 = { route: { id: 24, route_number: '24' }, schedules: [], stops: direction.stops.map((name,i) => ({ name, stop_id: i + 1 })) };
const built = buildDirectionPlan(source24, direction.stops[0], snapshot24, '2026-09-16', '2026-09-16');
const weekdayTrips = built.changes.find(change => change.dayType === 'weekday').after;
assert.equal(new Set(weekdayTrips.map(trip => trip.departureTime)).size, weekdayTrips.length);
for (let day = 1; day <= 7; day++) {
const dayType = day <= 5 ? 'weekday' : day === 6 ? 'saturday' : 'sunday';
const active = built.changes.find(change => change.dayType === dayType).after
.filter(trip => (!trip.daysOfWeek.length || trip.daysOfWeek.includes(day)) && !trip.exceptDaysOfWeek.includes(day))
.map(trip => trip.departureTime).sort();
const row = source24.rows.find(row => row.stopName === direction.stops[0] && row.days.includes(dayType)
&& (!row.daysOfWeek.length || row.daysOfWeek.includes(day)));
assert.deepEqual(active, [...row.times].sort(), `${direction.label}, ISO day ${day}`);
}
const again = buildDirectionPlan(source24, direction.stops[0], { ...snapshot24, schedules: built.changes.map((change,i) => ({ id:i+1, day_type:change.dayType, trips:change.after, valid_from:'2026-09-16' })) }, '2026-09-16', '2026-09-16');
assert.equal(again.hasChanges, false);
}
});
test('section restrictions intersect trip notes and overlapping sections are rejected', () => {
const parsed = html => parseSchedule(html, 'https://ap2polotsk.of.by/ap2/m-24/');
const state = { route:{id:24,route_number:'24'}, schedules:[], stops:[{stop_id:1,name:'Автовокзал'},{stop_id:2,name:'Полоцк – Стекловолокно'}] };
const build = html => buildDirectionPlan(parsed(html), 'Автовокзал', state, '2026-09-16', '2026-09-16');
const narrowed = build(fixture('route24').replace('17:00', '17:00 кроме среды'));
assert.deepEqual(narrowed.changes[0].after.find(t => t.departureTime === '17:00:00').daysOfWeek, [1,2,4]);
assert.throws(() => build(fixture('route24').replace('17:00','17:00 по пятницам')), /противоречит/);
assert.throws(() => build(fixture('route24').replace('Понедельник – четверг','Понедельник – пятница')), /повторяющийся/);
for (const label of ['Понедельник-четверг', 'Понедельник — четверг']) assert.deepEqual(parsed(fixture('route24').replace('Понедельник – четверг',label)).warnings, []);
assert.ok(parsed(fixture('route24').replace('Понедельник – четверг','Четверг – понедельник')).warnings.length);
});