// Builds dataset wa-girls-mile-record-vs-boys: curated tables + saved-analysis results. // Input: finals1600.json (from parse1600.js, WIAA 2A/3A/4A 1600m finals 2022-2026). // Output: data/*.csv and data/analyses.json const fs = require('fs'), path = require('path'); const here = __dirname, out = path.join(here, 'data'); const d = JSON.parse(fs.readFileSync(path.join(here, 'finals1600.json'), 'utf8')); const r1 = x => Math.round(x * 10) / 10, r2 = x => Math.round(x * 100) / 100; const fmt = s => Math.floor(s / 60) + ':' + (s % 60).toFixed(2).padStart(5, '0'); const csv = rows => { const c = Object.keys(rows[0]); const e = v => { const s = v === null ? '' : String(v); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; }; return c.join(',') + '\n' + rows.map(r => c.map(k => e(r[k])).join(',')).join('\n') + '\n'; }; const YEARS = [2022, 2023, 2024, 2025, 2026], CLS = ['2A', '3A', '4A']; // Table 1: every finalist, no names (many are minors). const finalists = d.map(r => ({ year: r.year, classification: r.cls, sex: r.sex, place: r.place, mark: r.mark, seconds: r2(r.sec), grade: +r.grade })) .sort((a, b) => a.year - b.year || a.classification.localeCompare(b.classification) || a.sex.localeCompare(b.sex) || a.place - b.place); // Table 2: girls' benchmarks. const WA = 273.29, US = 261.81; const benchmarks = [ { id: 'wa-girls-record', label: "Washington high-school girls' 1600m best", athlete: 'Alexa Efraimson', school: 'Camas HS (WA)', date: '2014-05-29', meet: 'WIAA 4A state championships, Tacoma', distance: '1600m', mark: '4:33.29', seconds1600: WA, basis: 'Full 1600m race (WIAA official result)', sourceId: 'wiaa-2014-results' }, { id: 'us-girls-mile-record', label: "U.S. high-school girls' mile record", athlete: 'Jane Hedengren', school: 'Timpview HS (UT)', date: '2025-06-05', meet: 'HOKA Festival of Miles, St. Louis', distance: 'Mile (1609.34m)', mark: '4:23.50', seconds1600: null, basis: 'Full mile race', sourceId: 'runners-world-hedengren' }, { id: 'us-girls-1600-split', label: "U.S. high-school girls' 1600m best (split inside the record mile)", athlete: 'Jane Hedengren', school: 'Timpview HS (UT)', date: '2025-06-05', meet: 'HOKA Festival of Miles, St. Louis', distance: '1600m (en-route split)', mark: '4:21.81', seconds1600: US, basis: '1600m split recorded during the 4:23.50 mile', sourceId: 'us-hs-records-wiki' }, { id: 'wa-prior-best-2005', label: "Washington girls' 1600m best before 2014", athlete: 'Brie Felnagle', school: 'Bellarmine Prep (WA)', date: '2005', meet: '2005 season (as reported in 2013)', distance: '1600m', mark: '4:38.09', seconds1600: 278.09, basis: 'Reported by The Columbian, May 23, 2013; year only', sourceId: 'columbian-2013' }, ]; // Table 3: Washington girls' 1600m season leaders (Athletic.net), no names. const leaders = [ [2014, '4:33.29', 11, 'WIAA 4A, 3A & 2A State Championships', 52117], [2015, '4:46.33', 10, 'Metro League Championships', 62268], [2016, '4:45.77', 11, 'Sea-King District Championships', 73225], [2017, '4:44.31', 10, 'Nike/Jesuit Twilight Relays', 82405], [2018, '4:50.76', 12, 'WIAA 2A, 3A, 4A State Championships', 91656], [2019, '4:43.60', 10, 'WIAA 2A, 3A, 4A State Championships', 100498], [2021, '4:48.27', 11, 'SPSL Championships', 118613], [2022, '4:48.74', 10, 'Oregon Relays', 127738], [2023, '4:41.37', 11, 'KingCo 2A, 3A & 4A Championships', 137101], [2024, '4:43.80', 12, 'Oregon Relays', 146511], [2025, '4:40.35', 12, 'Nike Outdoor Nationals', 159649], [2026, '4:46.44', 12, 'Brooks PR Invitational', 170658], ].map(([year, mark, grade, meet, listId]) => ({ year, leaderMark: mark, seconds: r2(+mark.split(':')[0] * 60 + +mark.split(':')[1]), grade, meet, listUrl: `https://www.athletic.net/TrackAndField/rankings/list/${listId}/f` })); const B = finalists.filter(r => r.sex === 'Boys'), G = finalists.filter(r => r.sex === 'Girls'); const A = {}; // A1 where each benchmark would place in each boys' final A.placement = YEARS.flatMap(y => CLS.map(c => { const f = B.filter(r => r.year === y && r.classification === c); const s = f.map(r => r.seconds).sort((a, b) => a - b); return { year: y, classification: c, boysFinalists: f.length, waRecordPlace: s.filter(x => x < WA).length + 1, usBestPlace: s.filter(x => x < US).length + 1, fieldSizeWithEntrant: f.length + 1, boysWinner: fmt(s[0]), boys8th: fmt(s[7]), boysLast: fmt(s[s.length - 1]) }; })); // A2 summary counts const cnt = s => B.filter(r => r.seconds < s).length; A.summary = { boysFinalists: B.length, finals: 15, fasterThanWaRecord: cnt(WA), fasterThanUsBest: cnt(US), finalsWaRecordLast: A.placement.filter(p => p.waRecordPlace === p.fieldSizeWithEntrant).length, finalsWaRecordInBottom5: A.placement.filter(p => p.fieldSizeWithEntrant - p.waRecordPlace <= 4).length, bestWaRecordPlace: Math.min(...A.placement.map(p => p.waRecordPlace)), worstWaRecordPlace: Math.max(...A.placement.map(p => p.waRecordPlace)), finalsWaRecordTop8: A.placement.filter(p => p.waRecordPlace <= 8).length, bestUsPlace: Math.min(...A.placement.map(p => p.usBestPlace)), worstUsPlace: Math.max(...A.placement.map(p => p.usBestPlace)), finalsUsTop8: A.placement.filter(p => p.usBestPlace <= 8).length, boysSlowerThanWaRecord: B.filter(r => r.seconds >= WA).map(r => ({ year: r.year, classification: r.classification, place: r.place, mark: r.mark })) }; // A3 chart rows: every boy finalist + benchmark per year + girls' champion per year A.chartRows = [ ...B.map(r => ({ year: String(r.year), seconds: r.seconds, series: 'Boy state finalist (2A, 3A, 4A)' })), ...YEARS.map(y => ({ year: String(y), seconds: WA, series: "Washington girls' best ever, 4:33.29 (2014)" })), ...YEARS.map(y => ({ year: String(y), seconds: US, series: "U.S. girls' best ever, 4:21.81 (2025)" })), ...YEARS.map(y => ({ year: String(y), seconds: Math.min(...G.filter(r => r.year === y).map(r => r.seconds)), series: "Fastest girls' state champion that year" })), ]; // A4 top 10 by year A.top10ByYear = YEARS.flatMap(y => { const t = s => finalists.filter(r => r.year === y && r.sex === s).sort((a, b) => a.seconds - b.seconds).slice(0, 10); const av = a => r2(a.reduce((x, r) => x + r.seconds, 0) / a.length); const b = av(t('Boys')), g = av(t('Girls')); return [{ year: String(y), sex: 'Boys', top10AvgSeconds: b, top10AvgMark: fmt(b), gapPct: r1(100 * (g / b - 1)) }, { year: String(y), sex: 'Girls', top10AvgSeconds: g, top10AvgMark: fmt(g), gapPct: r1(100 * (g / b - 1)) }]; }); // A5 2026 rank-by-rank const t26 = s => finalists.filter(r => r.year === 2026 && r.sex === s).sort((a, b) => a.seconds - b.seconds).slice(0, 10); A.top10_2026 = t26('Boys').map((b, i) => { const g = t26('Girls')[i]; return { rank: i + 1, boysMark: b.mark, boysClassification: b.classification, girlsMark: g.mark, girlsClassification: g.classification, gapPct: r1(100 * (g.seconds / b.seconds - 1)), gapSeconds: r2(g.seconds - b.seconds) }; }); // A6 season leaders vs record A.leadersVsRecord = { seasons: leaders.length, seasonsAfter2014: leaders.filter(l => l.year > 2014).length, fastestSince2014: fmt(Math.min(...leaders.filter(l => l.year > 2014).map(l => l.seconds))), anyFasterThanRecord: leaders.some(l => l.year > 2014 && l.seconds < WA) }; // A7 field sizes (participation check) A.fieldSizes = YEARS.flatMap(y => CLS.map(c => ({ year: y, classification: c, boysFinalists: B.filter(r => r.year === y && r.classification === c).length, girlsFinalists: G.filter(r => r.year === y && r.classification === c).length }))); // A8 girls' champions vs boys A.champions = YEARS.map(y => { const g = Math.min(...G.filter(r => r.year === y).map(r => r.seconds)); const f = B.filter(r => r.year === y); return { year: y, fastestGirlsChampion: fmt(g), boysFinalists: f.length, boysFaster: f.filter(r => r.seconds < g).length }; }); fs.mkdirSync(out, { recursive: true }); fs.writeFileSync(path.join(out, 'wa-1600-finalists-2022-2026.csv'), csv(finalists)); fs.writeFileSync(path.join(out, 'girls-1600-benchmarks.csv'), csv(benchmarks)); fs.writeFileSync(path.join(out, 'wa-girls-1600-season-leaders.csv'), csv(leaders)); fs.writeFileSync(path.join(out, 'analyses.json'), JSON.stringify(A, null, 1)); console.log(JSON.stringify(A.summary), '\n', JSON.stringify(A.leadersVsRecord), '\n', JSON.stringify(A.champions)); console.log(A.placement.map(p => p.year + p.classification + ' WA ' + p.waRecordPlace + '/' + p.fieldSizeWithEntrant + ' US ' + p.usBestPlace).join('; ')); console.log(finalists.length, B.length, G.length);