// Parses WIAA 2A/3A/4A state finals (places 1-8) from pdftext.js output. Handles three layouts: // A (2026): place line, "#bib" line, name, year, school, mark // B (2025): fixed-width lines " 1 # 669 Name 12 School 10.71 -1.4" // C (2022-2024): School, Yr, Name, place, mark tokens on separate lines // Usage: node parse-multi.js -> JSON rows on stdout const fs = require('fs'); const secs = s => s.split(':').map(Number).reduce((a, b) => a * 60 + b, 0); const ftToM = s => { const [f, i] = s.split('-').map(Number); return (f * 12 + (i || 0)) * 0.0254; }; const EVENTS = { '100 Meter Dash': ['100m', 'time'], '200 Meter Dash': ['200m', 'time'], '400 Meter Dash': ['400m', 'time'], '800 Meter Run': ['800m', 'time'], '1600 Meter Run': ['1600m', 'time'], '3200 Meter Run': ['3200m', 'time'], 'High Jump': ['High jump', 'distance'], 'Pole Vault': ['Pole vault', 'distance'], 'Long Jump': ['Long jump', 'distance'], 'Triple Jump': ['Triple jump', 'distance'], }; const MARK = String.raw`[JX]?(\d+:\d{2}\.\d{2}|\d+\.\d{2}|\d+-\d{2}(?:\.\d{2})?)`; const markRe = new RegExp('^' + MARK + '$'); const [file, year] = process.argv.slice(2); const L = fs.readFileSync(file, 'latin1').split('\n'); const rows = []; for (let i = 0; i < L.length; i++) { const h = L[i].match(/^Event \d+\s+(Boys|Girls) (.+) (\dA)$/); if (!h || !EVENTS[h[2]]) continue; let j = i + 1; while (j < L.length && !/^Event \d+ /.test(L[j])) j++; const b = L.slice(i + 1, j); const t = b.map(x => x.trim()); if (t.includes('Preliminaries') || !t.some(x => x === 'Finals' || /\bFinals\b/.test(x))) continue; const [event, kind] = EVENTS[h[2]]; const found = new Map(); const add = (place, m) => { if (place >= 1 && place <= 8 && !found.has(place)) found.set(place, m.replace(/^[JX]/, '')); }; const lineB = new RegExp(String.raw`^\s*(\d+) #\s*\d+ .*?\s` + MARK + String.raw`\b`); for (let k = 0; k < b.length; k++) { const mb = b[k].match(lineB); if (mb) { add(+mb[1], mb[2]); continue; } if (/^\d+$/.test(b[k]) && b[k + 1] && b[k + 1].startsWith('#')) { // layout A for (let q = k + 4; q < Math.min(k + 8, b.length); q++) if (markRe.test(t[q])) { add(+b[k], t[q]); break; } continue; } if (/^\d+$/.test(b[k]) && b[k + 1] !== undefined && markRe.test(t[k + 1]) && k >= 2 && /^\s+\d+$/.test(b[k - 2])) add(+b[k], t[k + 1]); // layout C } // places can tie (two 4ths); keep each listed athlete by scanning in order is overkill: one mark per place is enough for (const [place, m] of [...found].sort((a, b) => a[0] - b[0])) { const v = kind === 'time' ? secs(m) : ftToM(m); rows.push({ year: +year, classification: h[3], sex: h[1], event, place, mark: m, value: kind === 'time' ? Math.round(v * 100) / 100 : Math.round(v * 1000) / 1000, unit: kind === 'time' ? 'seconds' : 'meters' }); } } console.log(JSON.stringify(rows));