#!/usr/bin/env node /** * Busan Airport Limousine Schedule Query Tool * * Usage: * node query.js # to-airport + from-airport for that stop's line * node query.js --to # to-airport only * node query.js --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 '); 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)'); }