diff --git a/scripts/backfill-streaks.js b/scripts/backfill-streaks.js new file mode 100644 index 00000000..8456441a --- /dev/null +++ b/scripts/backfill-streaks.js @@ -0,0 +1,85 @@ +"use strict"; + +const fs = require("fs").promises; +const fsSync = require("fs"); +const path = require("path"); + +async function calculateStreaks(history) { + if (!history || history.length === 0) { + return { currentStreak: 0, longestStreak: 0, lastUpdated: null }; + } + + const sortedHistory = [...history].sort( + (a, b) => new Date(a.date) - new Date(b.date), + ); + + let currentStreak = 0; + let longestStreak = 0; + let lastActiveDate = null; + let previousTotal = -1; + + for (const entry of sortedHistory) { + const currentTotal = + (entry.easy || 0) + (entry.medium || 0) + (entry.hard || 0); + const entryDate = new Date(entry.date); + + if (currentTotal > previousTotal) { + if (!lastActiveDate) { + currentStreak = 1; + } else { + const diffDays = Math.round( + (entryDate - lastActiveDate) / (1000 * 60 * 60 * 24), + ); + + if (diffDays === 1) { + currentStreak++; + } else if (diffDays > 1) { + currentStreak = 1; + } + } + + longestStreak = Math.max(longestStreak, currentStreak); + lastActiveDate = entryDate; + } + + previousTotal = currentTotal; + } + + return { + currentStreak, + longestStreak, + lastUpdated: lastActiveDate + ? lastActiveDate.toISOString().split("T")[0] + : null, + }; +} + +async function runBackfill() { + const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, "..", "data"); + const userDataDir = path.join(DATA_DIR, "user-data"); + + try { + const files = await fs.readdir(userDataDir); + const jsonFiles = files.filter((f) => f.endsWith(".json")); + + for (const file of jsonFiles) { + const filePath = path.join(userDataDir, file); + const userData = JSON.parse(await fs.readFile(filePath, "utf8")); + + const streaks = await calculateStreaks(userData.history); + + userData.streak = { + current: streaks.currentStreak, + longest: streaks.longestStreak, + lastUpdated: streaks.lastUpdated, + }; + + await fs.writeFile(filePath, JSON.stringify(userData, null, 2)); + } + } catch (err) { + console.error(`Backfill failed: ${err.message}`); + process.exit(1); + } +} + +runBackfill(); diff --git a/scripts/sync-leaderboard.js b/scripts/sync-leaderboard.js index 0d85d551..1309a165 100644 --- a/scripts/sync-leaderboard.js +++ b/scripts/sync-leaderboard.js @@ -95,6 +95,47 @@ async function updateUserDataAsync(user, DATA_DIR, ranksObj = null) { if (ranksObj) { userData.leaderboardRanks = ranksObj; } + + const today = new Date().toISOString().split("T")[0]; + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + const yesterdayStr = yesterday.toISOString().split("T")[0]; + + if (!userData.streak) { + userData.streak = { current: 0, longest: 0, lastUpdated: null }; + } + + const totalSolvedToday = + (user.data.easySolved || 0) + + (user.data.mediumSolved || 0) + + (user.data.hardSolved || 0); + + const totalSolvedYesterday = + history.length > 1 + ? history[history.length - 2].easy + + history[history.length - 2].medium + + history[history.length - 2].hard + : 0; + + if ( + userData.streak.lastUpdated && + userData.streak.lastUpdated < yesterdayStr + ) { + userData.streak.current = 0; + } + + if ( + totalSolvedToday > totalSolvedYesterday && + userData.streak.lastUpdated !== today + ) { + userData.streak.current += 1; + userData.streak.lastUpdated = today; + + if (userData.streak.current > (userData.streak.longest || 0)) { + userData.streak.longest = userData.streak.current; + } + } + await atomicWrite(userDataPath, userData); }