Skip to content

Commit

Permalink
📝 AdventOfPBT event Day 1 (#5453)
Browse files Browse the repository at this point in the history
  • Loading branch information
dubzzz authored Dec 1, 2024
1 parent abfe7a6 commit f6ff4d8
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 0 deletions.
43 changes: 43 additions & 0 deletions website/blog/2024-12-01-advent-of-pbt-day-1/AdventOfTheDay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import adventBuggy from './buggy.mjs';
import { buildAdventOfTheDay } from './AdventOfTheDayBuilder';

const { AdventPlaygroundOfTheDay, FormOfTheDay } = buildAdventOfTheDay({
day: 1,
buildBuggyAdvent: adventBuggy,
referenceAdvent: sortLetters,
parser,
placeholderForm: 'Anne=15\nPaul=38\nElena=81',
functionName: 'sortLetters',
signature: 'function sortLetters(letters: Letter[]): Letter[];',
signatureExtras: ['type Letter = { name: string; age: number };'],
});

export { AdventPlaygroundOfTheDay, FormOfTheDay };

// Reference implementation

type Letter = { name: string; age: number };

function sortLetters(letters: Letter[]): Letter[] {
const clonedLetters = [...letters];
return clonedLetters.sort((la, lb) =>
la.age !== lb.age ? la.age - lb.age : la.name > lb.name ? 1 : la.name < lb.name ? -1 : 0,
);
}

// Inputs parser

const lineRegex = /^([a-z]+)=(\d+)$/;
function parser(answer: string): unknown[] | undefined {
const parsedAnswer: Letter[] = [];
for (const line of answer.trim().split('\n')) {
const m = lineRegex.exec(line);
if (m === null) {
throw new Error(
'Each line of the answer should follow the pattern: name=age, with name only made of characters in a-z and of length minimum one and age being exclusively between 7 and 77',
);
}
parsedAnswer.push({ name: m[1], age: Number(m[2]) });
}
return [parsedAnswer];
}
147 changes: 147 additions & 0 deletions website/blog/2024-12-01-advent-of-pbt-day-1/AdventOfTheDayBuilder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React, { useState } from 'react';
import { isEqual, set } from 'lodash';
import Admonition from '@theme/Admonition';
import AdventPlayground from './AdventPlayground';

const answerFieldName = 'answer';

type Options = {
day: number;
buildBuggyAdvent: () => (...args: unknown[]) => unknown;
referenceAdvent: (...args: unknown[]) => unknown;
postAdvent?: (adventOutput: unknown) => unknown;
parser: (answer: string) => unknown[] | undefined;
placeholderForm: string;
functionName: string;
signature: string;
signatureExtras?: string[];
};

export function buildAdventOfTheDay(options: Options) {
const {
day,
buildBuggyAdvent,
referenceAdvent,
postAdvent = (v) => v,
parser,
placeholderForm,
functionName,
signature,
signatureExtras,
} = options;

const snippetLinesWithHeadingSpaces = String(buildBuggyAdvent).split('\n').slice(1, -1);
const spacesCountToDrop = /^( +)/.exec(snippetLinesWithHeadingSpaces[0])[1].length;
const spacesToDrop = ' '.repeat(spacesCountToDrop);
const snippet = snippetLinesWithHeadingSpaces
.map((line) => (line.startsWith(spacesToDrop) ? line.substring(spacesToDrop.length) : line))
.map((line) => line.replace(/^return /, 'export default '))
.join('\n');

function AdventPlaygroundOfTheDay() {
return (
<AdventPlayground
functionName={functionName}
signature={signature}
signatureExtras={signatureExtras}
snippet={snippet}
day={day}
/>
);
}

let lastError = null;
const storageKey = `aopbt24-day${day}`;

function retrievePastAnswerIfSolved(): string | null {
try {
const pastAnswer = localStorage.getItem(storageKey);
if (pastAnswer === null) {
return null;
}
const inputs = parser(pastAnswer);
if (inputs === undefined) {
return null;
}
const buggyAdvent = buildBuggyAdvent();
if (isEqual(postAdvent(buggyAdvent(...inputs)), postAdvent(referenceAdvent(...inputs)))) {
return null;
}
return pastAnswer;
} catch (err) {
return null;
}
}

function onSubmit(event: React.SyntheticEvent<HTMLFormElement>) {
event.preventDefault();
try {
const answer = extractAnswerFromForm(event);
const inputs = parser(answer);
if (inputs === undefined) {
lastError = 'Malformed inputs provided!';
return;
}
const buggyAdvent = buildBuggyAdvent();
if (isEqual(postAdvent(buggyAdvent(...inputs)), postAdvent(referenceAdvent(...inputs)))) {
lastError = 'The input you provided seems to be working well: Santa is looking for a bug!';
return;
}
lastError = null;
localStorage.setItem(storageKey, answer);
} catch (err) {
lastError = `Malformed inputs provided!\n${(err as Error).message}`;
}
}

function FormOfTheDay() {
const [, setId] = useState(Symbol());
const pastAnswer = lastError === null ? retrievePastAnswerIfSolved() : null;

return (
<>
{pastAnswer !== null && (
<Admonition type="tip" icon="🎉" title="Congratulations">
<p>You solved this puzzle!</p>
</Admonition>
)}
{lastError !== null && (
<Admonition type="danger">
<p>{lastError}</p>
</Admonition>
)}
<form
onSubmit={(e) => {
onSubmit(e);
setId(Symbol());
}}
>
<textarea
name={answerFieldName}
style={{ width: '100%', ...(pastAnswer !== null ? { backgroundColor: 'lightgreen' } : {}) }}
rows={5}
defaultValue={pastAnswer ?? undefined}
placeholder={`Example of answer:\n${placeholderForm}`}
></textarea>
<br />
<button type="submit">Submit</button>
</form>
</>
);
}

return { AdventPlaygroundOfTheDay, FormOfTheDay };
}

function extractAnswerFromForm(event: React.SyntheticEvent<HTMLFormElement>): string {
const form = event.currentTarget;
const formElements = form.elements;
if (!(answerFieldName in formElements)) {
throw new Error(`No ${JSON.stringify(answerFieldName)} field attached to the form`);
}
const answer = (formElements[answerFieldName] as { value?: unknown }).value;
if (typeof answer !== 'string') {
throw new Error(`${JSON.stringify(answerFieldName)} field attached to the form must be of type string`);
}
return answer;
}
96 changes: 96 additions & 0 deletions website/blog/2024-12-01-advent-of-pbt-day-1/AdventPlayground.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from 'react';
import {
SandpackProvider,
SandpackLayout,
SandpackCodeEditor,
SandpackTests,
UnstyledOpenInCodeSandboxButton,
} from '@codesandbox/sandpack-react';
import { atomDark } from '@codesandbox/sandpack-themes';
import Admonition from '@theme/Admonition';

type Props = {
functionName: string;
signature: string;
signatureExtras?: string[];
snippet: string;
day: number;
};

export default function AdventPlayground(props: Props) {
const { functionName, signature, signatureExtras, snippet, day } = props;
const styleCodeEditor = { height: 400 };
const styleTests = { height: 200 };

const adventSpecLines = [
`import fc from 'fast-check';`,
`import ${functionName} from './advent.js';`,
``,
...signatureExtras.map((extra) => `// declare ${extra}`),
`// declare ${signature}`,
`test('helping Santa', () => {`,
` fc.assert(fc.property(fc.constant('noop'), (noop) => {`,
` }));`,
`})`,
];

return (
<>
<SandpackProvider
theme={atomDark}
files={{
[`/advent.js`]: {
code: snippet,
readOnly: true,
active: false,
hidden: true,
},
[`/advent.spec.ts`]: {
code: adventSpecLines.join('\n'),
readOnly: false,
active: true,
hidden: false,
},
'package.json': {
code: JSON.stringify({ main: `src/advent.js` }),
readOnly: true,
active: false,
hidden: true,
},
}}
customSetup={{
entry: `/advent.js`,
dependencies: {
'fast-check': 'latest',
},
}}
>
<SandpackLayout>
<SandpackCodeEditor style={styleCodeEditor} />
</SandpackLayout>
<SandpackLayout>
<SandpackTests verbose style={styleTests} />
</SandpackLayout>
<p>
<UnstyledOpenInCodeSandboxButton>
Open in CodeSandbox for more options: including typings...
</UnstyledOpenInCodeSandboxButton>
</p>
</SandpackProvider>
<Admonition type="note">
<p>
Can’t access the online playground? Prefer to run it locally?
<br />
No problem! You can download the source file{' '}
<a
download={`advent-day-${String(day).padStart(2, '0')}.mjs`}
href={`data:application/octet-stream;base64,${btoa(snippet)}`}
>
here
</a>
.
</p>
</Admonition>
</>
);
}
14 changes: 14 additions & 0 deletions website/blog/2024-12-01-advent-of-pbt-day-1/buggy.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// @ts-check

export default function advent() {
/** @typedef {{name: string; age: number;}} Letter */

/**
* @param {Letter[]} letters
* @returns {Letter[]}
*/
return function sortLetters(letters) {
const clonedLetters = [...letters];
return clonedLetters.sort((la, lb) => la.age - lb.age || la.name.codePointAt(0) - lb.name.codePointAt(0));
};
}
40 changes: 40 additions & 0 deletions website/blog/2024-12-01-advent-of-pbt-day-1/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: Advent of PBT 2024 · Day 1
authors: [dubzzz]
tags: [advent-of-pbt, advent-of-pbt-2024]
---

import {AdventPlaygroundOfTheDay,FormOfTheDay} from './AdventOfTheDay';

Christmas is at risk! In their rush to meet tight deadlines, Santa’s elves accidentally introduced bugs into critical algorithms. If these issues aren’t discovered in time, Christmas could be delayed for everyone worldwide!

Your mission is to troubleshoot these black-box algorithms using the power of fast-check.

The clock is ticking. Santa just pinged you with your first challenge: he’s struggling to answer children in the proper and efficient order. Something seems to be going wrong—can you uncover the issue and save Christmas? 🎄🔧

<!--truncate-->

## Letters to Santa

Each year, Santa receives billions of letters from children and adults all over the globe, each with their own unique wish lists and messages. To ensure timely responses before the big day, Santa has spent years refining his process. This year, his carefully designed system is entering its final stage.

Santa has decided to prioritize his answers based on the following criteria:

1. The younger the sender, the faster the answer.
2. If two senders are the same age, sort their letters alphabetically by name using the `<` operator.

To implement this system, Santa asked his elves for help. He instructed them that the input would be an array of letters, each represented as a string in the format `name=age`. The name would consist only of lowercase letters (`a-z`), with at least one character, and the age would be a number between 7 and 77. The elves’ task was to return a sorted version of this array based on the specified criteria.

## Hands on

The elves completed the task, but Santa is worried they may have made mistakes.

Using the property-based testing features provided by fast-check, your mission is to uncover a set of inputs (letters) that break the elves’ implementation.

Santa has entrusted you with the code created by the elves (though you can’t see it directly). You’re his last hope — can you find the flaws and save Christmas? 🎄🔧

<AdventPlaygroundOfTheDay />

## Your answer

<FormOfTheDay />
3 changes: 3 additions & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@docusaurus/preset-classic": "3.6.3",
"@docusaurus/remark-plugin-npm2yarn": "3.6.3",
"clsx": "^2.1.1",
"lodash": "^4.17.21",
"prism-react-renderer": "^2.4.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
Expand All @@ -33,7 +34,9 @@
"@docusaurus/types": "3.6.3",
"@jest/globals": "^29.7.0",
"@mdx-js/react": "^3.1.0",
"@types/lodash": "^4.17.13",
"@types/node": "^20.14.15",
"@types/react": "^18.3.12",
"fast-check": "workspace:*",
"jest": "^29.7.0",
"jimp": "^1.6.0",
Expand Down
3 changes: 3 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -20784,11 +20784,14 @@ __metadata:
"@docusaurus/types": "npm:3.6.3"
"@jest/globals": "npm:^29.7.0"
"@mdx-js/react": "npm:^3.1.0"
"@types/lodash": "npm:^4.17.13"
"@types/node": "npm:^20.14.15"
"@types/react": "npm:^18.3.12"
clsx: "npm:^2.1.1"
fast-check: "workspace:*"
jest: "npm:^29.7.0"
jimp: "npm:^1.6.0"
lodash: "npm:^4.17.21"
prism-react-renderer: "npm:^2.4.0"
react: "npm:^18.3.1"
react-dom: "npm:^18.3.1"
Expand Down

0 comments on commit f6ff4d8

Please sign in to comment.