Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add O(N)ovember 🍂 #954

Merged
merged 14 commits into from
Nov 8, 2023
1 change: 1 addition & 0 deletions src/lib/public/lc-dailies/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './lc-dailies-api-client';
export * from './onovember';
168 changes: 168 additions & 0 deletions src/lib/public/lc-dailies/onovember.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import type { Season } from 'lc-dailies';

const SEASON_DURATION = 7 * 24 * 60 * 60 * 1_000; // 1 week.
const ONOVEMBER_MONTH = 10; // November.

/**
* onovember is a function that manipulates the LC-Dailies data for the O(N)ovember page.
*/
export function onovember(data: Season[]) {
const seasonsMap = categorizeByMonth(data, ONOVEMBER_MONTH);
const onovembersMap: Record<
string, // Year.
{
totalSubmissions: number;
players: Record<string, { submissionCount: number; username: string }>;
dailies: Record<
string, // Day of month.
{ questionTitle: string; questionURL: string; playerIDs: Record<string, number> }
>;
calendar: {
weekdayName: string;
dayOfMonth: string;
submissionCount: number;
submissionsText?: string;
question?: { title: string; url: string };
}[];
winners: {
usernames: string[];
submissionCount: number;
// TODO: Add total player score.
}[];
}
> = {};
for (const year in seasonsMap) {
onovembersMap[year] = {
totalSubmissions: 0, // Total submissions are tallied after dailies are created.
dailies: {}, // Dailies are created first.
players: {}, // Players are tallied after dailies are created.
winners: [], // Winners are sorted after players are tallied.
calendar: [], // Calendar is created at the end.
};
const seasons = seasonsMap[year];
for (const season of seasons) {
for (const questionID in season.questions) {
const question = season.questions[questionID];
const questionDate = new Date(`${question.date} GMT`);
if (questionDate.getUTCMonth() !== ONOVEMBER_MONTH) {
continue;
}

const dayOfMonth = questionDate.getUTCDate().toString();
onovembersMap[year].dailies[dayOfMonth] = {
questionTitle: question.title,
questionURL: question.url,
playerIDs: {},
};

for (const playerID in season.submissions) {
const playerSubmissions = season.submissions[playerID];
for (const submissionQuestionID in playerSubmissions) {
if (questionID !== submissionQuestionID) {
continue;
}

const submission = playerSubmissions[submissionQuestionID];
if (!submission) {
continue;
}

onovembersMap[year].dailies[dayOfMonth].playerIDs[playerID] = 0;
onovembersMap[year].totalSubmissions++;
if (!onovembersMap[year].players[playerID]) {
onovembersMap[year].players[playerID] = {
username: season.players[playerID].lc_username,
submissionCount: 0,
};
}

onovembersMap[year].players[playerID].submissionCount++;
}
}
}
}

onovembersMap[year].winners = Object.entries(onovembersMap[year].players)
.sort(({ 1: a }, { 1: b }) => b.submissionCount - a.submissionCount)
.reduce((groups, { 1: player }) => {
const group = groups[groups.length - 1];
if (!group || group.submissionCount !== player.submissionCount) {
groups.push({ usernames: [player.username], submissionCount: player.submissionCount });
} else {
group.usernames.push(player.username);
}

return groups;
}, [] as { usernames: string[]; submissionCount: number }[]);

for (let i = 1; i <= 30; i++) {
const dayOfMonth = i.toString();
const daily = onovembersMap[year].dailies[dayOfMonth];
const weekdayName = getWeekdayName(new Date(Date.UTC(+year, ONOVEMBER_MONTH + 1, i)));
if (!daily) {
onovembersMap[year].calendar.push({
dayOfMonth,
weekdayName,
submissionCount: 0,
});
continue;
}

const playerIDs = Object.keys(daily.playerIDs);
const concatenatedUsernames = playerIDs
.sort(
(a, b) =>
onovembersMap[year].players[b].submissionCount -
onovembersMap[year].players[a].submissionCount
)
.map((id) => onovembersMap[year].players[id].username)
.join(', ');
const submissionsText = `${daily.questionTitle} answered by ${playerIDs.length}${
playerIDs.length > 0 ? `, ${concatenatedUsernames}` : ''
}`;
onovembersMap[year].calendar.push({
dayOfMonth,
weekdayName,
submissionCount: playerIDs ? Object.keys(playerIDs).length : 0,
question: {
title: onovembersMap[year].dailies[dayOfMonth].questionTitle,
url: onovembersMap[year].dailies[dayOfMonth].questionURL,
},
submissionsText,
});
}
}

return onovembersMap;
}

/**
* categorizeByMonth categorizes seasons by month. Returns a record of years to seasons.
*/
function categorizeByMonth(seasons: Season[], month: number): Record<number, Season[]> {
const result: Record<number, Season[]> = {};
for (const season of seasons) {
const date = new Date(season.start_date);
const endOfSeasonDate = new Date(date.getTime() + SEASON_DURATION);
if (date.getUTCMonth() === month || endOfSeasonDate.getUTCMonth() === month) {
const year = date.getUTCFullYear();
result[year] ??= [];
const index = result[year].findIndex((s) => s.start_date > season.start_date);
if (index === -1) {
result[year].push(season);
} else {
result[year].splice(index, 0, season);
}
}
}

return result;
}

// function getONovemberWeekdayName(year: number, month: number, dayOfMonth: number) {
// return getWeekdayName(new Date(`${year}-${month}-${dayOfMonth} GMT`));
// }

function getWeekdayName(date: Date) {
return date.toLocaleString('en-US', { weekday: 'short' });
}
7 changes: 3 additions & 4 deletions src/routes/(site)/lc-dailies/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@
time they take to do so. These scores reset on a weekly basis.
</p>

<p>
<a href="/lc-dailies-handbook">acmcsuf.com/lc-dailies-handbook</a>
</p>
<p>Usage instructions: <a href="/lc-dailies-handbook">acmcsuf.com/lc-dailies-handbook</a></p>

<p>See our daily O(N)ovember challenge: <a href="/onovember">acmcsuf.com/onovember</a></p>

<h2>Seasons</h2>

Expand Down Expand Up @@ -63,7 +63,6 @@

p {
margin: 10px 0;
text-indent: 1em;
}

ol {
Expand Down
6 changes: 5 additions & 1 deletion src/routes/(site)/lc-dailies/[season_id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@
</tr>
<tr>
<td>Season ID</td>
<td><code>{data.season?.id}</code></td>
<td
><a href="https://lc-dailies.deno.dev/seasons/{data.season?.id}"
><code>{data.season?.id}</code></a
></td
>
</tr>
<tr>
<td>Start date</td>
Expand Down
143 changes: 143 additions & 0 deletions src/routes/(site)/onovember/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<script lang="ts">
import type { PageData } from './$types';
import { MetaTags } from 'svelte-meta-tags';
import Spacing from '$lib/public/legacy/spacing.svelte';

export let data: PageData;
</script>

<MetaTags
openGraph={{
title: `LC-Dailies | O(N)ovember`,
description:
"Showcase of the scores of the players in the acmCSUF Discord server's LC-Dailies competition during the month of November.",
}}
/>

<svelte:head>
<title>LC-Dailies | O(N)ovember</title>
</svelte:head>

<Spacing --min="100px" --med="100px" --max="100px" />

<main>
<h1 id="title">
LC-Dailies
<small>O(N)ovember 🍂</small>
</h1>

{#each Object.entries(data.onovembers) as [year, onovember] (year)}
<section>
<h2 id={year}>
<a href="/onovember#{year}"
>O(N)ovember {year}
<small
>{onovember.totalSubmissions} total submission{onovember.totalSubmissions !== 1
? 's'
: ''}</small
></a
>
</h2>

<ol>
{#each onovember.winners as { usernames, submissionCount }}
<li>
{#each usernames as username, i}
{#if i > 0}, {/if}
<a href={`https://leetcode.com/${username}`} title={username}>{username}</a>
{/each}
<code>{submissionCount}</code> submissions
</li>
{/each}
</ol>

<div class="onovember-section">
{#each onovember.calendar as { dayOfMonth, weekdayName, submissionCount, submissionsText, question }}
{#if question}
<div
class="onovember-cell"
title={submissionsText}
style:--submission-count={submissionCount}
>
{weekdayName} Nov {dayOfMonth}
<hr />
Answered by
<code>{submissionCount}</code>
<hr />
<a href={question.url}>{question.title}</a>
</div>
{:else}
<div class="onovember-cell" style:--submission-count="0">
Nov {dayOfMonth}
<hr />
Answered by
<code>0</code>
</div>
{/if}
{/each}
</div>
</section>
{/each}
</main>

<style>
main {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}

h1 {
text-align: center;
font-size: 24px;
padding: 2em;
}

h2 {
width: 100%;
text-align: left;
}

small {
font-size: 16px;
background-color: #666;
color: #fff;
padding: 5px 10px;
border-radius: 5px;
margin-left: 5px;
}

.onovember-section {
--onovember-width: 150px;

display: grid;
grid: auto-flow / repeat(auto-fill, minmax(var(--onovember-width), 1fr));
}

.onovember-cell {
background-color: hsl(24, 100%, calc(50% - var(--submission-count, 0) * 5%));
padding: 1em;
margin: 0.5em;
border-radius: 5px;
display: inline-block;
text-align: center;
max-width: var(--onovember-width);
}

ol {
margin: 0 2em 2em 2em;
}

a {
text-decoration: none;
}

a:hover {
text-decoration: underline;
}

code {
font-size: 16px;
}
</style>
9 changes: 9 additions & 0 deletions src/routes/(site)/onovember/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { PageLoadEvent } from './$types';
import { LCDailiesAPIClient, onovember } from '$lib/public/lc-dailies';

export async function load({ fetch }: PageLoadEvent) {
const apiClient = new LCDailiesAPIClient(fetch);
return {
onovembers: onovember(await apiClient.listSeasons()),
};
}
1 change: 0 additions & 1 deletion src/routes/(site)/random/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export async function GET({ url }: RequestEvent) {
const pages = getPages(modules);
const randomPage = pages[~~(Math.random() * pages.length)];
const destination = new URL(randomPage, url);
console.log({ modules, pages });
return Response.redirect(destination, 302);
}

Expand Down