Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm install
- run: npm run test -- **/solution
- run: npm run test -- solution
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ If you have a suggestion to improve an exercise, an idea for a new exercise, or
- A markdown file with a description of the task, an empty (or mostly empty) JavaScript file, and a set of tests.
- A `solutions` directory that contains an example solution and the same test file with all of the tests unskipped.

To complete an exercise, you will need to go to the exercise directory with `cd exerciseName` in the terminal and run `npm test exerciseName.spec.js`. This should run the test file and show you the output. When you run a test for the first time, it will fail. This is by design! You must open the exercise file and write the code needed to get the test to pass.
To complete an exercise, you will need to go to the exercise directory with `cd <path to file>` in the terminal and run `npm test exerciseName.spec.js`. For example, to go to the first Foundations exercise "helloWorld", you need to `cd foundations/01_helloWorld` then run `npm rest helloWorld`. This should run the test file and show you the output. When you run a test for the first time, it will fail. This is by design! You must open the exercise file and write the code needed to get the test to pass.

1. Some of the exercises have test conditions defined in their spec file as `test.skip` instead of `test`. This is intentional. Once all `test`s pass, you will change the next `test.skip` to `test` and test your code again. You will do this until all conditions are satisfied. **All tests must pass at the same time**, and you should not have any instances of `test.skip` in the spec file when you are finished with an exercise.
1. Once you successfully finish an exercise, check the `solutions` directory within each exercise.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Exercise 13 - Factorial
# Exercise 1 - Factorial

Write a recursive [factorial](https://simple.wikipedia.org/wiki/Factorial) function that takes a non-negative integer, and returns the product of all positive integers less than or equal to the input integer. An input of `0` should return `1`. The function should only accept numbers, so `'4'` should not be accepted as it is a string. All invalid inputs should return `undefined`.

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Exercise 14 - contains
# Exercise 2 - contains

Write a function that searches for a value in a nested object. It returns true if the object contains that value.

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Exercise 15 - totalIntegers
# Exercise 3 - totalIntegers

Write a function that takes in an arbitrarily deep array or object and returns the total number of integers stored inside this array or object.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Exercise 16 - permutations
# Exercise 4 - permutations

Write a function that takes in an empty array or an input array of an consecutive positive integers, starting at 1, and returns an array of all possible permutations of the original array

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Exercise 19 - pascal
# Exercise 5 - pascal

The pascal's triangle is modelled as follows:
- The first row is `1`.
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
50 changes: 38 additions & 12 deletions generators/helpers.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,56 @@
const { readdir } = require("fs/promises");
const { basename, dirname, join } = require("path");

function splitDirectoryName(directoryName) {
const exerciseDirectoryName = directoryName.endsWith("solution")
? basename(dirname(directoryName))
: basename(directoryName);
return {
exerciseNumber: directoryName.match(/\d+/),
exerciseName: directoryName.match(/[a-z]+/i),
exerciseNumber: exerciseDirectoryName.match(/\d+/),
exerciseName: exerciseDirectoryName.match(/[a-z]+/i),
};
}

async function getLatestExerciseDirectory() {
async function getDirsWithExercises(path) {
const ignoredDirs = ["archive", "node_modules", "generators"];
try {
const files = await readdir("./");
const dirs = await readdir(join(process.cwd(), path), {
withFileTypes: true,
});
const exerciseDirs = dirs.filter(
(entry) =>
entry.isDirectory() &&
!entry.name.startsWith(".") &&
!ignoredDirs.includes(entry.name),
);
return exerciseDirs.map((dir) => dir.name);
} catch {
return [];
}
}

async function getLatestExerciseDirectory(path) {
try {
const files = await readdir(join(process.cwd(), path));
return files.findLast((file) => /^\d+_\w+$/.test(file));
} catch (err) {
console.error(err);
} catch {
return "0";
}
}

async function createExerciseDirectoryName(directoryName) {
const latestExerciseDirectory = await getLatestExerciseDirectory();
async function createExerciseDirectoryName(exerciseName, path) {
const latestExerciseDirectory = await getLatestExerciseDirectory(path);
const latestExerciseNumber = parseInt(latestExerciseDirectory.match(/^\d+/));

if (latestExerciseDirectory === `${latestExerciseNumber}_${directoryName}`) {
throw new Error(`Exercise already exists with name "${directoryName}"`);
if (latestExerciseDirectory === `${latestExerciseNumber}_${exerciseName}`) {
throw new Error(`Exercise already exists with name "${exerciseName}"`);
}

return `${latestExerciseNumber + 1}_${directoryName}`;
return `${latestExerciseNumber + 1}_${exerciseName}`;
}

module.exports = { createExerciseDirectoryName, splitDirectoryName };
module.exports = {
getDirsWithExercises,
createExerciseDirectoryName,
splitDirectoryName,
};
4 changes: 2 additions & 2 deletions generators/writeExercise.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ async function writeExercise(exercisePath) {
const { exerciseName } = splitDirectoryName(exercisePath);
const isSolutionFile = exercisePath.includes("/solution");
const exerciseContent = `const ${exerciseName} = function() {
${isSolutionFile ? "// Replace this comment with the solution code" : ""}
${isSolutionFile ? " // Replace this comment with the solution code" : ""}
};

// Do not edit below this line
module.exports = ${exerciseName};
`;
Expand Down
2 changes: 1 addition & 1 deletion generators/writeExerciseSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('${exerciseName}', () => {

expect(${exerciseName}()).toBe('');
});

test${isSolutionFile ? "" : ".skip"}('Second test description', () => {
// Replace this comment with any other necessary code, and update the expect line as necessary

Expand Down
115 changes: 86 additions & 29 deletions plopFile.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,103 @@
const { mkdir } = require("fs/promises");
const { join } = require("path");
const { camelCase } = require("case-anything");
const { createExerciseDirectoryName } = require("./generators/helpers");
const {
createExerciseDirectoryName,
getDirsWithExercises,
} = require("./generators/helpers");
const { writeReadme } = require("./generators/writeReadme");
const { writeExercise } = require("./generators/writeExercise");
const { writeExerciseSpec } = require("./generators/writeExerciseSpec");

/**
* @typedef {import('plop').NodePlopAPI} Plop
* @param {Plop} plop
*/
module.exports = function (plop) {
plop.setActionType("createExercise", async function (answers) {
const { exerciseName } = answers;
if (!exerciseName) {
throw new Error(
`Invalid exerciseName. Expected: valid string. Actual: "${exerciseName}"`
const NEW_DIR_OPTION = "<Make new directory>";

plop.setActionType(
"createExercise",
async function ({ pathForExercise, exerciseName }) {
if (!exerciseName) {
throw new Error(
`Invalid exerciseName. Expected: valid string. Actual: "${exerciseName}"`,
);
} else if (!pathForExercise.length) {
throw new Error(
"The new exercise cannot be placed in the project root",
);
}

const camelExerciseName = camelCase(exerciseName);
const exerciseDirectoryName = await createExerciseDirectoryName(
camelExerciseName,
join(...pathForExercise),
);
}

const camelExerciseName = camelCase(exerciseName);
const exerciseDirectoryName = await createExerciseDirectoryName(
camelExerciseName
);
const basePath = join("./", exerciseDirectoryName);
const solutionPath = join(basePath, "solution");

await mkdir(basePath);
await mkdir(solutionPath);

await writeReadme(basePath);
await writeExercise(basePath);
await writeExercise(solutionPath);
await writeExerciseSpec(basePath);
await writeExerciseSpec(solutionPath);
});
const basePath = join(
process.cwd(),
...pathForExercise,
exerciseDirectoryName,
);
const solutionPath = join(basePath, "solution");

await mkdir(basePath, { recursive: true });
await mkdir(solutionPath);

await writeReadme(basePath);
await writeExercise(basePath);
await writeExercise(solutionPath);
await writeExerciseSpec(basePath);
await writeExerciseSpec(solutionPath);
},
);

plop.setGenerator("Basic", {
description: "Create a basic JavaScript exercise.",
prompts: [
{
prompts: async function (inquirer) {
async function getPathForExercise(dirPath = []) {
const exerciseDirs = await getDirsWithExercises(dirPath.join("/"));

// Will only be empty when entering a new dir on a recursive call
// Recursive call only happens when new dir required which can bypass this question
const { dir } = exerciseDirs.length
? await inquirer.prompt({
type: "list",
name: "dir",
message: "Which directory should this exercise go in?",
choices: [NEW_DIR_OPTION, ...exerciseDirs],
})
: { dir: NEW_DIR_OPTION };

if (dir === NEW_DIR_OPTION) {
const { newDirName } = await inquirer.prompt({
type: "input",
name: "newDirName",
message: "What is the name of the new directory?",
});
dirPath.push(newDirName);
} else {
dirPath.push(dir);
}

const { needMoreDirs } = await inquirer.prompt({
type: "confirm",
name: "needMoreDirs",
message: "Does this exercise need to be nested in a subdirectory?",
});

return needMoreDirs ? await getPathForExercise(dirPath) : dirPath;
}

const pathForExercise = await getPathForExercise();
const { exerciseName } = await inquirer.prompt({
type: "input",
name: "exerciseName",
message: "What is the name of the exercise? (camelCase)",
},
],
message: "What is the name of the new exercise (in camelCase)?",
});

return { pathForExercise, exerciseName };
},
actions: [{ type: "createExercise" }],
});
};