Files
SREBOT/utils/seasons.js
T

136 lines
4.4 KiB
JavaScript

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+<t:(\d+):R>\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 (…) <t:TS:R>" 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 = /<t:(\d+):R>/;
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 };