// Builds the anonymized FactHub tables for dataset wa-i638-girls-event-placements // from HeCheated.org's public Washington K-12 results page. // Usage: node build_placements.js // Athlete names, schools and meet names are dropped; each athlete gets a code (A, B, ...) // ordered by number of events in the pamphlet-era snapshot (most first), ties by earliest date. const fs = require('fs'), vm = require('vm'), path = require('path'); const [, , htmlPath, outDir = '.'] = process.argv; const html = fs.readFileSync(htmlPath, 'utf8'); const s = html.indexOf('const data = ['); const e = html.indexOf('];', s); // The page embeds its table as a JavaScript array literal; evaluate it in an empty sandbox. const rows = vm.runInNewContext('(' + html.slice(s + 13, e + 1) + ')', {}, { timeout: 2000 }); const MONTHS = { Jan: '01', Feb: '02', Mar: '03', Apr: '04', May: '05', Jun: '06', Jul: '07', Aug: '08', Sept: '09', Sep: '09', Oct: '10', Nov: '11', Dec: '12' }; const isoDate = d => { const [y, m, dd] = d.split('-'); return `${y}-${MONTHS[m]}-${dd.padStart(2, '0')}`; }; const isFirst = x => /^1st/.test(x.sort); const isTop3 = x => /^(1st|2nd|3rd|Semi)/.test(x.sort); const counted = x => x.sort !== 'DNS'; // Rows added after the pamphlet text was written: the three September 2026 rows. const inSnapshot = x => !(x.year === '2026' && /^2026-Sept/.test(x.date)); const relay = x => /4x|DMR|SMR/.test(x.eventl); const meetGroup = x => { if (/State\/Provincial/.test(x.category)) return 'State championship'; if (/Regional|District|Sectional|Conference\/League Championship|Conference\/League Standings/.test(x.category)) return 'District, regional or league championship'; if (/National/.test(x.category)) return 'National invitational (out of state)'; if (/Dual|Tri-|Quad|Invitational|Conference Meet/.test(x.category)) return 'Regular-season meet'; return 'Other or unclassified'; }; const finishGroup = x => { if (x.sort === 'DNS') return 'Did not start (not counted)'; if (isFirst(x)) return '1st'; if (isTop3(x)) return '2nd-3rd'; const m = String(x.place).match(/^(\d+)/); if (!m) return 'DQ or no place listed'; const n = +m[1]; if (n <= 10) return '4th-10th'; return '11th or lower'; }; // Athlete codes const stats = {}; rows.forEach(x => { const k = x.name; stats[k] = stats[k] || { n: 0, first: isoDate(x.date) }; if (inSnapshot(x) && counted(x)) stats[k].n++; if (isoDate(x.date) < stats[k].first) stats[k].first = isoDate(x.date); }); const order = Object.keys(stats).sort((a, b) => stats[b].n - stats[a].n || stats[a].first.localeCompare(stats[b].first)); const code = {}; order.forEach((k, i) => code[k] = String.fromCharCode(65 + i)); const out = rows .map(x => ({ x, d: isoDate(x.date) })) .sort((a, b) => a.d.localeCompare(b.d) || a.x.eventl.localeCompare(b.x.eventl)) .map(({ x, d }, i) => ({ row_id: i + 1, athlete_code: code[x.name], date: d, year: +x.year, level: x.level, sport: x.sport, meet_category: x.category, meet_group: meetGroup(x), wiaa_state_meet: /State\/Provincial/.test(x.category) && /WIAA/.test(x.competition), event: x.eventl || '(team result)', relay: relay(x), team_sport: x.sport === 'Ultimate Frisbee', place_as_listed: x.place, finish_group: finishGroup(x), counted_event: counted(x), first_place: counted(x) && isFirst(x), top_three: counted(x) && isTop3(x), meet_location_state: x.location, in_pamphlet_count: inSnapshot(x), })); const cols = Object.keys(out[0]); const esc = v => { const t = String(v); return /[",\n]/.test(t) ? '"' + t.replace(/"/g, '""') + '"' : t; }; fs.writeFileSync(path.join(outDir, 'wa_girls_event_entries.csv'), [cols.join(','), ...out.map(r => cols.map(c => esc(r[c])).join(','))].join('\n') + '\n'); fs.writeFileSync(path.join(outDir, 'entries.json'), JSON.stringify(out, null, 1)); console.log('rows', out.length);