feat: enhance schedule import functionality with support for day sections and restrictions
This commit is contained in:
53
backend/test/fixtures/schedule-import/route24.html
vendored
Normal file
53
backend/test/fixtures/schedule-import/route24.html
vendored
Normal 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>
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user