mirror of
https://github.com/theoludwig/programming-challenges.git
synced 2025-05-18 12:02:53 +02:00
🎉 Initial commit
This commit is contained in:
84
scripts/create-challenge.ts
Normal file
84
scripts/create-challenge.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import path from 'path'
|
||||
import * as fsWithCallbacks from 'fs'
|
||||
import chalk from 'chalk'
|
||||
import makeDir from 'make-dir'
|
||||
import inquirer from 'inquirer'
|
||||
import { replaceInFile } from 'replace-in-file'
|
||||
import validateProjectName from 'validate-npm-package-name'
|
||||
import copyDirPromise from './utils/copyDirPromise'
|
||||
import date from 'date-and-time'
|
||||
;(async () => {
|
||||
const fs = fsWithCallbacks.promises
|
||||
const QUESTIONS = [
|
||||
{
|
||||
name: 'challengeName',
|
||||
type: 'input',
|
||||
message: 'Challenge name:'
|
||||
},
|
||||
{
|
||||
name: 'userGitHub',
|
||||
type: 'input',
|
||||
message: 'Your GitHub name:'
|
||||
}
|
||||
]
|
||||
const answers = await inquirer.prompt(QUESTIONS)
|
||||
const { challengeName, userGitHub } = answers as {
|
||||
[key: string]: string
|
||||
}
|
||||
console.log()
|
||||
|
||||
if (!challengeName || challengeName === '') {
|
||||
console.log(chalk.cyan('Please specify the challenge name you want to create.'))
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const validChallengeName = validateProjectName(challengeName)
|
||||
if (!validChallengeName.validForNewPackages) {
|
||||
console.log(`
|
||||
Invalid challenge name: ${chalk.red(challengeName)}
|
||||
${validChallengeName.errors != undefined &&
|
||||
validChallengeName.errors[0]}
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const challengePath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'challenges',
|
||||
challengeName
|
||||
)
|
||||
const templatePath = path.resolve(__dirname, 'templates', 'challenge')
|
||||
|
||||
// Challenge valid ?
|
||||
if (fsWithCallbacks.existsSync(challengePath)) {
|
||||
console.log(`The challenge already exists: ${chalk.red(challengeName)}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const createdChallengeTemplatePath = await makeDir(challengePath)
|
||||
const solutionsFolderPath = path.join(createdChallengeTemplatePath, 'solutions')
|
||||
await copyDirPromise(templatePath, createdChallengeTemplatePath)
|
||||
await makeDir(solutionsFolderPath)
|
||||
await fs.writeFile(path.join(solutionsFolderPath, '.gitkeep'), '')
|
||||
|
||||
// Replace {{ challengeName }} in README.md
|
||||
const readmePath = path.join(createdChallengeTemplatePath, 'README.md')
|
||||
await replaceInFile({
|
||||
files: [readmePath],
|
||||
from: /{{ challengeName }}/g,
|
||||
to: challengeName
|
||||
})
|
||||
|
||||
// Replace {{ challengeInfo }} in README.md
|
||||
await replaceInFile({
|
||||
files: [readmePath],
|
||||
from: /{{ challengeInfo }}/g,
|
||||
to: `Created${(userGitHub !== '') ? ` by @${userGitHub}` : ''} at ${date.format(new Date(), 'D MMMM Y', true)}.`
|
||||
})
|
||||
|
||||
console.log(`
|
||||
${chalk.green('Success:')} "${challengeName}" challenge created.
|
||||
${chalk.cyan('You can now edit README.md and input-output.json files.')}
|
||||
`)
|
||||
})()
|
113
scripts/create-solution.ts
Normal file
113
scripts/create-solution.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import path from 'path'
|
||||
import * as fsWithCallbacks from 'fs'
|
||||
import chalk from 'chalk'
|
||||
import inquirer from 'inquirer'
|
||||
import { replaceInFile } from 'replace-in-file'
|
||||
import makeDir from 'make-dir'
|
||||
import date from 'date-and-time'
|
||||
import validateProjectName from 'validate-npm-package-name'
|
||||
import copyDirPromise from './utils/copyDirPromise'
|
||||
;(async () => {
|
||||
const fs = fsWithCallbacks.promises
|
||||
const challengesPath = path.resolve(__dirname, '..', 'challenges')
|
||||
const challengesAvailable = await fs.readdir(challengesPath)
|
||||
const languagesAvailable: {
|
||||
name: string
|
||||
extension: string
|
||||
launch: string
|
||||
}[] = require('./languages-wrapper/_languages.json')
|
||||
|
||||
const QUESTIONS = [
|
||||
{
|
||||
name: 'challengeName',
|
||||
type: 'list',
|
||||
message: 'Select a challenge:',
|
||||
choices: challengesAvailable
|
||||
},
|
||||
{
|
||||
name: 'programmingLanguage',
|
||||
type: 'list',
|
||||
message: 'Select a programming language:',
|
||||
choices: languagesAvailable.map(language => ({ name: language.name, value: language }))
|
||||
},
|
||||
{
|
||||
name: 'solutionName',
|
||||
type: 'input',
|
||||
message: 'Solution name:'
|
||||
},
|
||||
{
|
||||
name: 'userGitHub',
|
||||
type: 'input',
|
||||
message: 'Your GitHub name:'
|
||||
}
|
||||
]
|
||||
|
||||
const answers = await inquirer.prompt(QUESTIONS)
|
||||
console.log()
|
||||
const { challengeName, solutionName, userGitHub } = answers as {
|
||||
[key: string]: string
|
||||
}
|
||||
const { programmingLanguage } = answers as {
|
||||
programmingLanguage: {
|
||||
extension: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
const validSolutionName = validateProjectName(solutionName)
|
||||
if (!validSolutionName.validForNewPackages) {
|
||||
console.log(`
|
||||
Invalid solution name: ${chalk.red(solutionName)}
|
||||
${validSolutionName.errors != undefined &&
|
||||
validSolutionName.errors[0]}
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const solutionPath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'challenges',
|
||||
challengeName,
|
||||
'solutions',
|
||||
solutionName
|
||||
)
|
||||
const templatePath = path.resolve(__dirname, 'templates', 'solutions')
|
||||
const templateSolutionPath = path.resolve(__dirname, 'languages-wrapper', 'templates')
|
||||
|
||||
// Solution valid ?
|
||||
if (fsWithCallbacks.existsSync(solutionPath)) {
|
||||
console.log(`The solution already exists: ${chalk.red(solutionName)}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const createdSolutionTemplatePath = await makeDir(solutionPath)
|
||||
await copyDirPromise(templatePath, createdSolutionTemplatePath)
|
||||
|
||||
const languageSolutionTemplate = path.join(templateSolutionPath, `solution${programmingLanguage.extension}`)
|
||||
await fs.copyFile(languageSolutionTemplate, path.join(createdSolutionTemplatePath, `solution${programmingLanguage.extension}`))
|
||||
|
||||
// Replace {{ solutionName }} in README.md
|
||||
const readmePath = path.join(createdSolutionTemplatePath, 'README.md')
|
||||
await replaceInFile({
|
||||
files: [readmePath],
|
||||
from: /{{ solutionName }}/g,
|
||||
to: `${solutionName} - ${challengeName}`
|
||||
})
|
||||
|
||||
// Replace {{ solutionInfo }} in README.md
|
||||
const createdByString = `Created${(userGitHub !== '') ? ` by @${userGitHub}` : ''} at ${date.format(new Date(), 'D MMMM Y', true)}.`
|
||||
await replaceInFile({
|
||||
files: [readmePath],
|
||||
from: /{{ solutionInfo }}/g,
|
||||
to: 'Programming language : ' + programmingLanguage.name + '\n' + createdByString
|
||||
})
|
||||
|
||||
console.log(`
|
||||
${chalk.green('Success:')} "${solutionName}" created.
|
||||
${chalk.cyan(`Edit your solution${programmingLanguage.extension} file and try to solve "${challengeName}" challenge (see README.md).`)}
|
||||
|
||||
Don't forget to test your solution attempt :
|
||||
${chalk.green(`npm run test ${challengeName} ${solutionName}`)}
|
||||
`)
|
||||
})()
|
17
scripts/languages-wrapper/_languages.json
Normal file
17
scripts/languages-wrapper/_languages.json
Normal file
@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"name": "Python",
|
||||
"extension": ".py",
|
||||
"launch": "python"
|
||||
},
|
||||
{
|
||||
"name": "JavaScript",
|
||||
"extension": ".js",
|
||||
"launch": "node"
|
||||
},
|
||||
{
|
||||
"name": "TypeScript",
|
||||
"extension": ".ts",
|
||||
"launch": "ts-node"
|
||||
}
|
||||
]
|
16
scripts/languages-wrapper/execute.js
Normal file
16
scripts/languages-wrapper/execute.js
Normal file
@ -0,0 +1,16 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs').promises
|
||||
const solution = require('./solution')
|
||||
|
||||
const inputPath = path.join(__dirname, 'input.json')
|
||||
const outputPath = path.join(__dirname, 'output.json')
|
||||
|
||||
const main = async () => {
|
||||
const inputFile = await fs.readFile(inputPath)
|
||||
const inputJSON = JSON.parse(inputFile)
|
||||
|
||||
const result = solution.apply(null, inputJSON)
|
||||
await fs.writeFile(outputPath, JSON.stringify(result))
|
||||
}
|
||||
|
||||
main()
|
13
scripts/languages-wrapper/execute.py
Normal file
13
scripts/languages-wrapper/execute.py
Normal file
@ -0,0 +1,13 @@
|
||||
import os
|
||||
import json
|
||||
from solution import solution
|
||||
|
||||
current_directory = os.path.dirname(__file__)
|
||||
input_path = os.path.join(current_directory, "input.json")
|
||||
output_path = os.path.join(current_directory, "output.json")
|
||||
|
||||
with open(input_path, "r") as file_content:
|
||||
input_json = json.load(file_content)
|
||||
|
||||
with open(output_path, "w") as file_content:
|
||||
json.dump(solution(*input_json), file_content)
|
18
scripts/languages-wrapper/execute.ts
Normal file
18
scripts/languages-wrapper/execute.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import path from 'path'
|
||||
import * as fsWithCallbacks from 'fs'
|
||||
// @ts-ignore
|
||||
import solution from './solution'
|
||||
|
||||
const fs = fsWithCallbacks.promises
|
||||
const inputPath = path.join(__dirname, 'input.json')
|
||||
const outputPath = path.join(__dirname, 'output.json')
|
||||
|
||||
const main = async () => {
|
||||
const inputFile = await fs.readFile(inputPath)
|
||||
const inputJSON = JSON.parse(inputFile.toString())
|
||||
|
||||
const result = solution.apply(null, inputJSON)
|
||||
await fs.writeFile(outputPath, JSON.stringify(result))
|
||||
}
|
||||
|
||||
main()
|
5
scripts/languages-wrapper/templates/solution.js
Normal file
5
scripts/languages-wrapper/templates/solution.js
Normal file
@ -0,0 +1,5 @@
|
||||
function solution () {
|
||||
|
||||
}
|
||||
|
||||
module.exports = solution
|
2
scripts/languages-wrapper/templates/solution.py
Normal file
2
scripts/languages-wrapper/templates/solution.py
Normal file
@ -0,0 +1,2 @@
|
||||
def solution():
|
||||
pass
|
5
scripts/languages-wrapper/templates/solution.ts
Normal file
5
scripts/languages-wrapper/templates/solution.ts
Normal file
@ -0,0 +1,5 @@
|
||||
function solution () {
|
||||
|
||||
}
|
||||
|
||||
export default solution
|
11
scripts/templates/challenge/README.md
Normal file
11
scripts/templates/challenge/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# {{ challengeName }}
|
||||
|
||||
{{ challengeInfo }}
|
||||
|
||||
## Instructions :
|
||||
|
||||
Description of the challenge...
|
||||
|
||||
## Examples :
|
||||
|
||||
See the `input-output.json` file for examples of input/output.
|
6
scripts/templates/challenge/input-output.json
Normal file
6
scripts/templates/challenge/input-output.json
Normal file
@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"input": [],
|
||||
"output": null
|
||||
}
|
||||
]
|
3
scripts/templates/solutions/README.md
Normal file
3
scripts/templates/solutions/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# {{ solutionName }}
|
||||
|
||||
{{ solutionInfo }}
|
195
scripts/test.ts
Normal file
195
scripts/test.ts
Normal file
@ -0,0 +1,195 @@
|
||||
import util from 'util'
|
||||
import path from 'path'
|
||||
import * as fsWithCallbacks from 'fs'
|
||||
import childProcess from 'child_process'
|
||||
import { performance } from 'perf_hooks'
|
||||
import chalk from 'chalk'
|
||||
import deleteAllFilesExceptOne from './utils/deleteAllFilesExceptOne'
|
||||
import emoji from 'node-emoji'
|
||||
import prettyMilliseconds from 'pretty-ms'
|
||||
import { table } from 'table'
|
||||
;(async () => {
|
||||
const fs = fsWithCallbacks.promises
|
||||
const exec = util.promisify(childProcess.exec)
|
||||
const args = process.argv.slice(2)
|
||||
const [challengeName, solutionName] = args
|
||||
|
||||
if (!challengeName || !solutionName) {
|
||||
console.log(`
|
||||
Please specify the challenge and solution name:
|
||||
${chalk.cyan(`npm run test [challenge-name] [solution-name]`)}
|
||||
|
||||
For example:
|
||||
${chalk.cyan('npm run test hello-world python-hello')}
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const challengePath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'challenges',
|
||||
challengeName
|
||||
)
|
||||
const solutionFolderPath = path.resolve(
|
||||
challengePath,
|
||||
'solutions',
|
||||
solutionName
|
||||
)
|
||||
|
||||
// Challenge valid ?
|
||||
try {
|
||||
await fs.access(challengePath)
|
||||
} catch {
|
||||
console.log(`The challenge was not found: ${chalk.red(challengeName)}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Solution valid ?
|
||||
try {
|
||||
await fs.access(solutionFolderPath)
|
||||
} catch {
|
||||
console.log(`The solution was not found: ${chalk.red(solutionName)}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Determinate the language to execute
|
||||
const solutionFilesName = await fs.readdir(solutionFolderPath)
|
||||
let solutionFilePath
|
||||
for (const solutionFileName of solutionFilesName) {
|
||||
const fileName = solutionFileName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.')
|
||||
if (fileName === 'solution') {
|
||||
solutionFilePath = solutionFileName
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!solutionFilePath) {
|
||||
console.log(`The ${chalk.red('solution')} file was not found.`)
|
||||
process.exit(0)
|
||||
}
|
||||
const languages: {
|
||||
name: string
|
||||
extension: string
|
||||
launch: string
|
||||
}[] = require('./languages-wrapper/_languages.json')
|
||||
const extensionSolution = path.extname(solutionFilePath)
|
||||
const languageToExecute = languages.find(
|
||||
language => language.extension === extensionSolution
|
||||
)
|
||||
if (!languageToExecute) {
|
||||
console.log(`Sadly, this ${chalk.red('language')} is not supported yet.`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Copy 'solution' and 'execute' files in temp
|
||||
const inputOutputJSON: { input: any[]; output: any }[] = require(path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'challenges',
|
||||
challengeName,
|
||||
'input-output.json'
|
||||
))
|
||||
const tempPath = path.join(__dirname, '..', 'temp')
|
||||
const executeFile = `execute${languageToExecute.extension}`
|
||||
const executeLanguagePath = path.resolve(
|
||||
__dirname,
|
||||
'languages-wrapper',
|
||||
executeFile
|
||||
)
|
||||
const executeLanguageTempPath = path.join(tempPath, executeFile)
|
||||
const inputPath = path.join(tempPath, 'input.json')
|
||||
const outputPath = path.join(tempPath, 'output.json')
|
||||
await fs.copyFile(
|
||||
path.resolve(solutionFolderPath, solutionFilePath),
|
||||
path.join(tempPath, solutionFilePath)
|
||||
)
|
||||
await fs.copyFile(executeLanguagePath, executeLanguageTempPath)
|
||||
|
||||
// Console.log & Tests
|
||||
const totalCorrect = {
|
||||
total: 0,
|
||||
correct: 0
|
||||
}
|
||||
const tableResult = [
|
||||
[
|
||||
chalk.cyan('Result'),
|
||||
chalk.cyan('Input'),
|
||||
chalk.cyan('Output'),
|
||||
chalk.cyan('Expected output')
|
||||
]
|
||||
]
|
||||
const startTest = performance.now()
|
||||
|
||||
// Loop I/O
|
||||
for (const { input, output } of inputOutputJSON) {
|
||||
// Write input.json
|
||||
const inputStringify = JSON.stringify(input)
|
||||
await fs.writeFile(inputPath, inputStringify)
|
||||
|
||||
// Execute script (create output.json)
|
||||
try {
|
||||
await exec(`${languageToExecute.launch} ${executeLanguageTempPath}`)
|
||||
} catch (error) {
|
||||
console.log(chalk.bgRedBright.black(error.stderr))
|
||||
await deleteAllFilesExceptOne(tempPath, '.gitignore')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Read output.json
|
||||
const data = await fs.readFile(outputPath)
|
||||
const outputJSON = JSON.parse(data.toString())
|
||||
|
||||
// Tests
|
||||
totalCorrect.total += 1
|
||||
const outputJSONStringify = JSON.stringify(outputJSON)
|
||||
const outputStringify = JSON.stringify(output)
|
||||
const isCorrect = outputJSONStringify === outputStringify
|
||||
|
||||
if (isCorrect) {
|
||||
tableResult.push([
|
||||
emoji.get('white_check_mark'),
|
||||
inputStringify,
|
||||
outputJSONStringify,
|
||||
outputStringify
|
||||
])
|
||||
totalCorrect.correct += 1
|
||||
} else {
|
||||
tableResult.push([
|
||||
emoji.get('x'),
|
||||
inputStringify,
|
||||
outputJSONStringify,
|
||||
outputStringify
|
||||
])
|
||||
}
|
||||
|
||||
// Delete I/O file
|
||||
await fs.unlink(inputPath)
|
||||
await fs.unlink(outputPath)
|
||||
}
|
||||
|
||||
const endTest = performance.now()
|
||||
|
||||
console.log(
|
||||
table(tableResult, {
|
||||
columns: {
|
||||
0: { width: 6, alignment: 'center' },
|
||||
1: { width: 20, wrapWord: true },
|
||||
2: { width: 30, wrapWord: true },
|
||||
3: { width: 30, wrapWord: true }
|
||||
}
|
||||
})
|
||||
)
|
||||
console.log(`
|
||||
Challenge : ${challengeName}
|
||||
Solution : ${solutionName}
|
||||
Tests : ${chalk.green(`${totalCorrect.correct} passed`)}, ${
|
||||
totalCorrect.total
|
||||
} total
|
||||
Time : ${chalk.yellow(prettyMilliseconds(endTest - startTest))}
|
||||
`)
|
||||
|
||||
await deleteAllFilesExceptOne(tempPath, '.gitignore')
|
||||
})()
|
26
scripts/utils/copyDirPromise.ts
Normal file
26
scripts/utils/copyDirPromise.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
function copyDirPromise (source: string, destination: string) {
|
||||
return new Promise(next => {
|
||||
const filesToCreate = fs.readdirSync(source)
|
||||
filesToCreate.forEach(async file => {
|
||||
const originalFilePath = path.join(source, file)
|
||||
const stats = fs.statSync(originalFilePath)
|
||||
if (stats.isFile()) {
|
||||
if (file === '.npmignore') file = '.gitignore'
|
||||
const writePath = path.join(destination, file)
|
||||
fs.copyFileSync(originalFilePath, writePath)
|
||||
} else if (stats.isDirectory()) {
|
||||
fs.mkdirSync(path.join(destination, file))
|
||||
await copyDirPromise(
|
||||
path.join(source, file),
|
||||
path.join(destination, file)
|
||||
)
|
||||
}
|
||||
})
|
||||
next()
|
||||
})
|
||||
}
|
||||
|
||||
export default copyDirPromise
|
18
scripts/utils/deleteAllFilesExceptOne.ts
Normal file
18
scripts/utils/deleteAllFilesExceptOne.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import path from 'path'
|
||||
import * as fsWithCallbacks from 'fs'
|
||||
const fs = fsWithCallbacks.promises
|
||||
|
||||
async function deleteAllFilesExceptOne (directoryPath: string, fileNameToNotDelete: string) {
|
||||
const fileNames = await fs.readdir(path.resolve(directoryPath))
|
||||
for (const name of fileNames) {
|
||||
const fileNamePath = path.resolve(directoryPath, name)
|
||||
const stats = await fs.stat(fileNamePath)
|
||||
if (stats.isDirectory()) {
|
||||
await fs.rmdir(fileNamePath, { recursive: true })
|
||||
} else if (name !== fileNameToNotDelete) {
|
||||
await fs.unlink(fileNamePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default deleteAllFilesExceptOne
|
Reference in New Issue
Block a user