const fs = require('fs'); const path = require('path'); const SEASONS_FILE = path.join(__dirname, '..', 'constants', 'seasons'); // Parse DD.MM from the "until eos" parenthetical to an end-of-day UTC unix ts // for the season's year (inferred from the first weekN timestamp). function _endOfDayUtc(year, day, month) { const d = new Date(Date.UTC(year, month - 1, day, 23, 59, 59)); return Math.floor(d.getTime() / 1000); } // Parse the seasons file into: // { '2026-I': {start, end, weeks}, '2026-II': {start, end, weeks}, ... } // where `end` is the last second (UTC) of the date in the "until eos" row. // Status is computed at access time by getSeasons(), not cached here. function parseSeasonsFile(content) { const lines = content.split('\n'); const seasons = {}; let current = null; let firstTs = null; let eosDay = null, eosMonth = null; let weekEntries = []; const seasonHeader = /^\s*(\d{4})-(I{1,3}|IV|VI{0,3}|IX|X)\s*$/; const weekLine = /^(week\s+\d+|until\s+eos)\s+\((\d{2}\.\d{2})\s+[—-]\s+(\d{2}\.\d{2})\)\s+\s+max BR\s+(\d+(?:\.\d+)?)/i; const commit = () => { if (current && firstTs !== null && eosDay !== null) { const year = parseInt(current.split('-')[0], 10); seasons[current] = { start: firstTs, end: _endOfDayUtc(year, eosDay, eosMonth), weeks: weekEntries, }; } current = null; firstTs = null; eosDay = null; eosMonth = null; weekEntries = []; }; for (const line of lines) { const h = line.match(seasonHeader); if (h) { commit(); current = `${h[1]}-${h[2]}`; continue; } if (!current) continue; const w = line.match(weekLine); if (w) { const kind = w[1].toLowerCase(); const startDate = w[2]; const endDate = w[3]; const ts = parseInt(w[4], 10); const maxBr = parseFloat(w[5]); weekEntries.push({ label: kind, start_date: startDate, end_date: endDate, start_ts: ts, max_br: maxBr, }); if (firstTs === null) firstTs = ts; if (kind === 'until eos') { eosDay = parseInt(endDate.split('.')[0], 10); eosMonth = parseInt(endDate.split('.')[1], 10); } } } commit(); return seasons; } // All "week N (…) " timestamps for a given season name, in order. // Used as x-axis faint vline markers by the Python renderer. function parseWeekBoundaries(content, seasonName) { const lines = content.split('\n'); const out = []; let inSeason = false; const seasonHeader = /^\s*(\d{4})-(I{1,3}|IV|VI{0,3}|IX|X)\s*$/; const weekLine = //; for (const line of lines) { const h = line.match(seasonHeader); if (h) { inSeason = `${h[1]}-${h[2]}` === seasonName; continue; } if (!inSeason) continue; const w = line.match(weekLine); if (w) out.push(parseInt(w[1], 10)); } return out; } let _cached = null; function _loadSeasons() { if (_cached) return _cached; const content = fs.readFileSync(SEASONS_FILE, 'utf8'); const parsed = parseSeasonsFile(content); _cached = { content, parsed }; return _cached; } function getSeasons() { const { parsed } = _loadSeasons(); const now = Math.floor(Date.now() / 1000); const out = {}; for (const [name, range] of Object.entries(parsed)) { out[name] = { start: range.start, end: range.end, status: range.end < now ? 'completed' : 'in_progress', }; } return out; } function getSeasonRange(name) { return getSeasons()[name] || null; } function getSeasonDetails() { const { parsed } = _loadSeasons(); const now = Math.floor(Date.now() / 1000); const out = {}; for (const [name, range] of Object.entries(parsed)) { out[name] = { start: range.start, end: range.end, status: range.end < now ? 'completed' : 'in_progress', weeks: range.weeks || [], }; } return out; } function getWeekBoundaries(name) { const { content } = _loadSeasons(); return parseWeekBoundaries(content, name); } module.exports = { getSeasons, getSeasonRange, getSeasonDetails, getWeekBoundaries, parseSeasonsFile, parseWeekBoundaries };