150 lines
6.2 KiB
JavaScript
150 lines
6.2 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Busan Airport Limousine Schedule Query Tool
|
||
*
|
||
* Usage:
|
||
* node query.js <stop-name> # to-airport + from-airport for that stop's line
|
||
* node query.js <stop-name> --to # to-airport only
|
||
* node query.js <stop-name> --from # from-airport only
|
||
* node query.js --from-airport --line 1 # all from-airport departures, line 1
|
||
* node query.js --from-airport --line 2 # all from-airport departures, line 2
|
||
*/
|
||
|
||
import { readFileSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, join } from 'node:path';
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const DATA = join(__dirname, '../data/schedule.jsonl');
|
||
|
||
const trips = readFileSync(DATA, 'utf8')
|
||
.trim().split('\n')
|
||
.map(l => JSON.parse(l));
|
||
|
||
const args = process.argv.slice(2);
|
||
if (args.length === 0) {
|
||
console.error('Usage: node query.js <stop-name>');
|
||
console.error(' node query.js --from-airport --line 1|2');
|
||
process.exit(1);
|
||
}
|
||
|
||
const fromAirportFlag = args.includes('--from-airport');
|
||
const lineFlag = args.includes('--line') ? args[args.indexOf('--line') + 1] : null;
|
||
const toOnly = args.includes('--to');
|
||
const fromOnly = args.includes('--from');
|
||
const stopQuery = args.find(a => !a.startsWith('--') && a !== lineFlag) || null;
|
||
|
||
// ── Helper ──────────────────────────────────────────────────────────────────
|
||
|
||
function pad(s, n) {
|
||
return String(s).padEnd(n);
|
||
}
|
||
|
||
function header(title) {
|
||
console.log(`\n${title}`);
|
||
console.log('─'.repeat(title.length));
|
||
}
|
||
|
||
// ── From-airport by line (--from-airport --line N) ──────────────────────────
|
||
|
||
if (fromAirportFlag && lineFlag) {
|
||
const line = parseInt(lineFlag);
|
||
if (line !== 1 && line !== 2) {
|
||
console.error('--line must be 1 or 2'); process.exit(1);
|
||
}
|
||
// Line 1 = pages 3–4, Line 2 = pages 5–6
|
||
const pages = line === 1 ? [3, 4] : [5, 6];
|
||
const termLabels = { 3: 'Domestic', 4: 'International', 5: 'Domestic', 6: 'International' };
|
||
const dest = line === 1 ? 'Haeundae/Gijang' : 'Seomyeon/Bujeon';
|
||
|
||
header(`[From Airport] Line ${line}: Gimhae Airport → ${dest}`);
|
||
console.log(`${'Airport dep'.padEnd(14)} ${'Est. arrival'.padEnd(14)} Terminal`);
|
||
console.log(`${'─'.repeat(14)} ${'─'.repeat(14)} ─────────────`);
|
||
|
||
for (const page of pages) {
|
||
const pageTrips = trips.filter(t => t.page === page).sort((a, b) => {
|
||
const [ah, am] = a.departure.time.split(':').map(Number);
|
||
const [bh, bm] = b.departure.time.split(':').map(Number);
|
||
return (ah * 60 + am) - (bh * 60 + bm);
|
||
});
|
||
for (const t of pageTrips) {
|
||
console.log(`${pad(t.departure.time, 14)} ${pad(t.arrival.time, 14)} ${termLabels[page]}`);
|
||
}
|
||
}
|
||
console.log('\n(arrival times estimated — see arrival_note in schedule.jsonl)');
|
||
process.exit(0);
|
||
}
|
||
|
||
// ── Stop lookup ──────────────────────────────────────────────────────────────
|
||
|
||
if (!stopQuery) {
|
||
console.error('Provide a stop name or use --from-airport --line 1|2');
|
||
process.exit(1);
|
||
}
|
||
|
||
// Find all to-airport trips where the stop appears (partial match)
|
||
const matchedTrips = trips.filter(t =>
|
||
t.direction === 'to_airport' &&
|
||
t.all_stops.some(s => s.stop.includes(stopQuery))
|
||
);
|
||
|
||
if (matchedTrips.length === 0) {
|
||
console.error(`No stops found matching "${stopQuery}"`);
|
||
console.error('Line 1 stops: 반얀트리 아난티코브 오시리아 장산역 해운대온천사거리 해운대해수욕장 동백섬입구 한화리조트해운대 파크하얏트부산 요트경기장 벡스코 신세계센텀시티 상수도남부사업소');
|
||
console.error('Line 2 stops: 부전시장 부전역 서면/롯데호텔백화점 동의대역 주례역');
|
||
process.exit(1);
|
||
}
|
||
|
||
// Determine which line this stop belongs to
|
||
const line = matchedTrips[0].page === 1 ? 1 : 2;
|
||
const dest = line === 1 ? 'Haeundae/Gijang' : 'Seomyeon/Bujeon';
|
||
|
||
// Find the canonical matched stop name
|
||
const matchedStopName = matchedTrips[0].all_stops.find(s => s.stop.includes(stopQuery)).stop;
|
||
|
||
// ── To-airport section ───────────────────────────────────────────────────────
|
||
|
||
if (!fromOnly) {
|
||
const sorted = [...matchedTrips].sort((a, b) => {
|
||
const sa = a.all_stops.find(s => s.stop.includes(stopQuery));
|
||
const sb = b.all_stops.find(s => s.stop.includes(stopQuery));
|
||
const [ah, am] = sa.time.split(':').map(Number);
|
||
const [bh, bm] = sb.time.split(':').map(Number);
|
||
return (ah * 60 + am) - (bh * 60 + bm);
|
||
});
|
||
|
||
header(`[To Airport] Line ${line}: ${matchedStopName} → Gimhae Airport`);
|
||
console.log(`${'Dep'.padEnd(10)} ${'Airport arr (est)'.padEnd(18)}`);
|
||
console.log(`${'─'.repeat(10)} ${'─'.repeat(18)}`);
|
||
|
||
for (const t of sorted) {
|
||
const stopEntry = t.all_stops.find(s => s.stop.includes(stopQuery));
|
||
console.log(`${pad(stopEntry.time, 10)} ${t.arrival.time}`);
|
||
}
|
||
}
|
||
|
||
// ── From-airport section ─────────────────────────────────────────────────────
|
||
|
||
if (!toOnly) {
|
||
const pages = line === 1 ? [3, 4] : [5, 6];
|
||
const termLabels = { 3: 'Dom', 4: 'Int\'l', 5: 'Dom', 6: 'Int\'l' };
|
||
|
||
header(`[From Airport] Line ${line}: Gimhae Airport → ${dest}`);
|
||
console.log(`${'Airport dep'.padEnd(14)} ${'Est. arrival'.padEnd(14)} Terminal`);
|
||
console.log(`${'─'.repeat(14)} ${'─'.repeat(14)} ────────`);
|
||
|
||
const fromTrips = trips
|
||
.filter(t => pages.includes(t.page))
|
||
.sort((a, b) => {
|
||
const [ah, am] = a.departure.time.split(':').map(Number);
|
||
const [bh, bm] = b.departure.time.split(':').map(Number);
|
||
return (ah * 60 + am) - (bh * 60 + bm);
|
||
});
|
||
|
||
for (const t of fromTrips) {
|
||
console.log(`${pad(t.departure.time, 14)} ${pad(t.arrival.time, 14)} ${termLabels[t.page]}`);
|
||
}
|
||
|
||
console.log('\n(arrival times estimated)');
|
||
}
|