diff --git a/config.json b/config.json index d89693a72..24a73eed4 100644 --- a/config.json +++ b/config.json @@ -1173,6 +1173,14 @@ "recursion" ] }, + { + "slug": "satellite", + "name": "Satellite", + "uuid": "da0fa224-6095-4382-b869-f4e3953e09f6", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "sublist", "name": "Sublist", diff --git a/exercises/practice/satellite/.docs/instructions.md b/exercises/practice/satellite/.docs/instructions.md new file mode 100644 index 000000000..fbbf14f43 --- /dev/null +++ b/exercises/practice/satellite/.docs/instructions.md @@ -0,0 +1,27 @@ +# Instructions + +Imagine you need to transmit a binary tree to a satellite approaching Alpha Centauri and you have limited bandwidth. +Since the tree has no repeating items it can be uniquely represented by its [pre-order and in-order traversals][wiki]. + +Write the software for the satellite to rebuild the tree from the traversals. + +A pre-order traversal reads the value of the current node before (hence "pre") reading the left subtree in pre-order. +Afterwards the right subtree is read in pre-order. + +An in-order traversal reads the left subtree in-order then the current node and finally the right subtree in-order. +So in order from left to right. + +For example the pre-order traversal of this tree is [a, i, x, f, r]. +The in-order traversal of this tree is [i, a, f, x, r] + +```text + a + / \ +i x + / \ + f r +``` + +Note: the first item in the pre-order traversal is always the root. + +[wiki]: https://en.wikipedia.org/wiki/Tree_traversal diff --git a/exercises/practice/satellite/.meta/config.json b/exercises/practice/satellite/.meta/config.json new file mode 100644 index 000000000..eefc27103 --- /dev/null +++ b/exercises/practice/satellite/.meta/config.json @@ -0,0 +1,25 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "satellite.ts" + ], + "test": [ + "satellite.test.ts" + ], + "example": [ + ".meta/proof.ci.ts" + ] + }, + "custom": { + "version.tests.compatibility": "jest-29", + "flag.tests.task-per-describe": false, + "flag.tests.may-run-long": false, + "flag.tests.includes-optional": false, + "flag.tests.jest": true, + "flag.tests.tstyche": false + }, + "blurb": "Rebuild binary trees from pre-order and in-order traversals." +} diff --git a/exercises/practice/satellite/.meta/proof.ci.ts b/exercises/practice/satellite/.meta/proof.ci.ts new file mode 100644 index 000000000..1c45192cf --- /dev/null +++ b/exercises/practice/satellite/.meta/proof.ci.ts @@ -0,0 +1,56 @@ +export type Tree = { + v?: string + l?: Tree + r?: Tree +} + +const hasUniqueItems = (items: string[]): boolean => + new Set(items).size === items.length + +const hasSameItems = (left: string[], right: string[]): boolean => { + const rightItems = new Set(right) + return left.every((item) => rightItems.has(item)) +} + +const validateTraversals = (preorder: string[], inorder: string[]): void => { + if (preorder.length !== inorder.length) { + throw new Error('traversals must have the same length') + } + + if (!hasUniqueItems(preorder) || !hasUniqueItems(inorder)) { + throw new Error('traversals must contain unique items') + } + + if (!hasSameItems(preorder, inorder)) { + throw new Error('traversals must have the same elements') + } +} + +const buildTree = (preorder: string[], inorder: string[]): Tree => { + if (preorder.length === 0) { + return {} + } + + const [root, ...remainingPreorder] = preorder + const rootIndex = inorder.indexOf(root) + const leftInorder = inorder.slice(0, rootIndex) + const rightInorder = inorder.slice(rootIndex + 1) + const leftSize = leftInorder.length + const leftPreorder = remainingPreorder.slice(0, leftSize) + const rightPreorder = remainingPreorder.slice(leftSize) + + return { + v: root, + l: buildTree(leftPreorder, leftInorder), + r: buildTree(rightPreorder, rightInorder), + } +} + +export const treeFromTraversals = ( + preorder: string[], + inorder: string[] +): Tree => { + validateTraversals(preorder, inorder) + + return buildTree(preorder, inorder) +} diff --git a/exercises/practice/satellite/.meta/tests.toml b/exercises/practice/satellite/.meta/tests.toml new file mode 100644 index 000000000..d0ed5b6ac --- /dev/null +++ b/exercises/practice/satellite/.meta/tests.toml @@ -0,0 +1,37 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[8df3fa26-811a-4165-9286-ff9ac0850d19] +description = "Empty tree" + +[f945ccfc-05e3-47d7-825b-0270559d43ad] +description = "Tree with one item" + +[a0121d5f-37b0-48dd-9c64-cba4c4464135] +description = "Tree with many items" + +[6074041f-4891-4d81-a128-401050c2a3b0] +description = "Reject traversals of different length" + +[27916ce4-45f3-4d8b-8528-496fedc157ca] +description = "Reject inconsistent traversals of same length" + +[d86a3d72-76a9-43b5-9d3a-e64cb1216035] +description = "Reject traversals with repeated items" + +[af31ae02-7e5b-4452-a990-bccb3fca9148] +description = "A degenerate binary tree" + +[ee54463d-a719-4aae-ade4-190d30ce7320] +description = "Another degenerate binary tree" + +[87123c08-c155-4486-90a4-e2f75b0f3e8f] +description = "Tree with many more items" diff --git a/exercises/practice/satellite/.vscode/extensions.json b/exercises/practice/satellite/.vscode/extensions.json new file mode 100644 index 000000000..daaa5ee2e --- /dev/null +++ b/exercises/practice/satellite/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "arcanis.vscode-zipfs", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ] +} diff --git a/exercises/practice/satellite/.vscode/settings.json b/exercises/practice/satellite/.vscode/settings.json new file mode 100644 index 000000000..761fb422a --- /dev/null +++ b/exercises/practice/satellite/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": ["exercism"], + "search.exclude": { + "**/.yarn": true, + "**/.pnp.*": true + } +} diff --git a/exercises/practice/satellite/.yarnrc.yml b/exercises/practice/satellite/.yarnrc.yml new file mode 100644 index 000000000..23e4a6d3d --- /dev/null +++ b/exercises/practice/satellite/.yarnrc.yml @@ -0,0 +1,3 @@ +compressionLevel: mixed + +enableGlobalCache: true diff --git a/exercises/practice/satellite/babel.config.cjs b/exercises/practice/satellite/babel.config.cjs new file mode 100644 index 000000000..164552797 --- /dev/null +++ b/exercises/practice/satellite/babel.config.cjs @@ -0,0 +1,5 @@ +module.exports = { + // eslint-disable-next-line @typescript-eslint/no-require-imports + presets: [[require('@exercism/babel-preset-typescript'), { corejs: '3.38' }]], + plugins: [], +} diff --git a/exercises/practice/satellite/eslint.config.mjs b/exercises/practice/satellite/eslint.config.mjs new file mode 100644 index 000000000..1be39c53f --- /dev/null +++ b/exercises/practice/satellite/eslint.config.mjs @@ -0,0 +1,26 @@ +// @ts-check + +import tsEslint from 'typescript-eslint' +import config from '@exercism/eslint-config-typescript' +import maintainersConfig from '@exercism/eslint-config-typescript/maintainers.mjs' + +export default [ + ...tsEslint.config(...config, { + files: ['.meta/proof.ci.ts', '.meta/exemplar.ts', '*.test.ts'], + extends: maintainersConfig, + }), + { + ignores: [ + // # Protected or generated + '.git/**/*', + '.vscode/**/*', + + //# When using npm + 'node_modules/**/*', + + // # Configuration files + 'babel.config.cjs', + 'jest.config.cjs', + ], + }, +] diff --git a/exercises/practice/satellite/jest.config.cjs b/exercises/practice/satellite/jest.config.cjs new file mode 100644 index 000000000..0aba1a59e --- /dev/null +++ b/exercises/practice/satellite/jest.config.cjs @@ -0,0 +1,22 @@ +module.exports = { + verbose: true, + projects: [''], + testMatch: [ + '**/__tests__/**/*.[jt]s?(x)', + '**/test/**/*.[jt]s?(x)', + '**/?(*.)+(spec|test).[jt]s?(x)', + ], + testPathIgnorePatterns: [ + '/(?:production_)?node_modules/', + '.d.ts$', + '/test/fixtures', + '/test/helpers', + '__mocks__', + ], + transform: { + '^.+\\.[jt]sx?$': 'babel-jest', + }, + moduleNameMapper: { + '^(\\.\\/.+)\\.js$': '$1', + }, +} diff --git a/exercises/practice/satellite/package.json b/exercises/practice/satellite/package.json new file mode 100644 index 000000000..7e21c6db4 --- /dev/null +++ b/exercises/practice/satellite/package.json @@ -0,0 +1,38 @@ +{ + "name": "@exercism/typescript-satellite", + "version": "1.0.0", + "description": "Exercism exercises in Typescript.", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/exercism/typescript" + }, + "type": "module", + "engines": { + "node": "^18.16.0 || >=20.0.0" + }, + "devDependencies": { + "@exercism/babel-preset-typescript": "^0.6.0", + "@exercism/eslint-config-typescript": "^0.8.0", + "@jest/globals": "^29.7.0", + "@types/node": "~22.7.6", + "babel-jest": "^29.7.0", + "core-js": "~3.38.1", + "eslint": "^9.12.0", + "expect": "^29.7.0", + "jest": "^29.7.0", + "prettier": "^3.3.3", + "tstyche": "^2.1.1", + "typescript": "~5.6.3", + "typescript-eslint": "^8.10.0" + }, + "scripts": { + "test": "corepack yarn node test-runner.mjs", + "test:types": "corepack yarn tstyche", + "test:implementation": "corepack yarn jest --no-cache --passWithNoTests", + "lint": "corepack yarn lint:types && corepack yarn lint:ci", + "lint:types": "corepack yarn tsc --noEmit -p .", + "lint:ci": "corepack yarn eslint . --ext .tsx,.ts" + }, + "packageManager": "yarn@4.5.1" +} diff --git a/exercises/practice/satellite/satellite.test.ts b/exercises/practice/satellite/satellite.test.ts new file mode 100644 index 000000000..a83ca67ed --- /dev/null +++ b/exercises/practice/satellite/satellite.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, it, xit } from '@jest/globals' +import { treeFromTraversals } from './satellite.ts' + +describe('Satellite', () => { + it('Empty tree', () => { + const preorder: string[] = [] + const inorder: string[] = [] + expect(treeFromTraversals(preorder, inorder)).toEqual({}) + }) + + xit('Tree with one item', () => { + const preorder = ['a'] + const inorder = ['a'] + const expected = { v: 'a', l: {}, r: {} } + expect(treeFromTraversals(preorder, inorder)).toEqual(expected) + }) + + xit('Tree with many items', () => { + const preorder = ['a', 'i', 'x', 'f', 'r'] + const inorder = ['i', 'a', 'f', 'x', 'r'] + const expected = { + v: 'a', + l: { v: 'i', l: {}, r: {} }, + r: { + v: 'x', + l: { v: 'f', l: {}, r: {} }, + r: { v: 'r', l: {}, r: {} }, + }, + } + expect(treeFromTraversals(preorder, inorder)).toEqual(expected) + }) + + xit('Reject traversals of different length', () => { + const preorder = ['a', 'b'] + const inorder = ['b', 'a', 'r'] + expect(() => treeFromTraversals(preorder, inorder)).toThrow( + 'traversals must have the same length' + ) + }) + + xit('Reject inconsistent traversals of same length', () => { + const preorder = ['x', 'y', 'z'] + const inorder = ['a', 'b', 'c'] + expect(() => treeFromTraversals(preorder, inorder)).toThrow( + 'traversals must have the same elements' + ) + }) + + xit('Reject traversals with repeated items', () => { + const preorder = ['a', 'b', 'a'] + const inorder = ['b', 'a', 'a'] + expect(() => treeFromTraversals(preorder, inorder)).toThrow( + 'traversals must contain unique items' + ) + }) + + xit('A degenerate binary tree', () => { + const preorder = ['a', 'b', 'c', 'd'] + const inorder = ['d', 'c', 'b', 'a'] + const expected = { + v: 'a', + l: { + v: 'b', + l: { + v: 'c', + l: { v: 'd', l: {}, r: {} }, + r: {}, + }, + r: {}, + }, + r: {}, + } + expect(treeFromTraversals(preorder, inorder)).toEqual(expected) + }) + + xit('Another degenerate binary tree', () => { + const preorder = ['a', 'b', 'c', 'd'] + const inorder = ['a', 'b', 'c', 'd'] + const expected = { + v: 'a', + l: {}, + r: { + v: 'b', + l: {}, + r: { + v: 'c', + l: {}, + r: { v: 'd', l: {}, r: {} }, + }, + }, + } + expect(treeFromTraversals(preorder, inorder)).toEqual(expected) + }) + + xit('Tree with many more items', () => { + const preorder = ['a', 'b', 'd', 'g', 'h', 'c', 'e', 'f', 'i'] + const inorder = ['g', 'd', 'h', 'b', 'a', 'e', 'c', 'i', 'f'] + const expected = { + v: 'a', + l: { + v: 'b', + l: { + v: 'd', + l: { v: 'g', l: {}, r: {} }, + r: { v: 'h', l: {}, r: {} }, + }, + r: {}, + }, + r: { + v: 'c', + l: { v: 'e', l: {}, r: {} }, + r: { + v: 'f', + l: { v: 'i', l: {}, r: {} }, + r: {}, + }, + }, + } + expect(treeFromTraversals(preorder, inorder)).toEqual(expected) + }) +}) diff --git a/exercises/practice/satellite/satellite.ts b/exercises/practice/satellite/satellite.ts new file mode 100644 index 000000000..ec919ea45 --- /dev/null +++ b/exercises/practice/satellite/satellite.ts @@ -0,0 +1,6 @@ +export const treeFromTraversals = ( + preorder: unknown[], + inorder: unknown[] +): unknown => { + throw new Error('Remove this line and implement the function') +} diff --git a/exercises/practice/satellite/test-runner.mjs b/exercises/practice/satellite/test-runner.mjs new file mode 100644 index 000000000..44b205fc2 --- /dev/null +++ b/exercises/practice/satellite/test-runner.mjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +/** + * 👋🏽 Hello there reader, + * + * It looks like you are working on this solution using the Exercism CLI and + * not the online editor. That's great! The file you are looking at executes + * the various steps the online test-runner also takes. + * + * @see https://github.com/exercism/typescript-test-runner + * + * TypeScript track exercises generally consist of at least two out of three + * types of tests to run. + * + * 1. tsc, the TypeScript compiler. This tests if the TypeScript code is valid + * 2. tstyche, static analysis tests to see if the types used are expected + * 3. jest, runtime implementation tests to see if the solution is correct + * + * If one of these three fails, this script terminates with -1, -2, or -3 + * respectively. If it succeeds, it terminates with exit code 0. + * + * @note you need corepack (bundled with node LTS) enabled in order for this + * test runner to work as expected. Follow the installation and test + * instructions if you see errors about corepack or pnp. + */ + +import { execSync } from 'node:child_process' +import { existsSync, readFileSync } from 'node:fs' +import { exit } from 'node:process' +import { URL } from 'node:url' + +/** + * Before executing any tests, the test runner attempts to find the + * exercise config.json file which has metadata about which types of tests + * to run for this solution. + */ +const metaDirectory = new URL('./.meta/', import.meta.url) +const exercismDirectory = new URL('./.exercism/', import.meta.url) +const configDirectory = existsSync(metaDirectory) + ? metaDirectory + : existsSync(exercismDirectory) + ? exercismDirectory + : null + +if (configDirectory === null) { + throw new Error( + 'Expected .meta or .exercism directory to exist, but I cannot find it.' + ) +} + +const configFile = new URL('./config.json', configDirectory) +if (!existsSync(configFile)) { + throw new Error('Expected config.json to exist at ' + configFile.toString()) +} + +// Experimental: import config from './config.json' with { type: 'json' } +/** @type {import('./config.json') } */ +const config = JSON.parse(readFileSync(configFile)) + +const jest = !config.custom || config.custom['flag.tests.jest'] +const tstyche = config.custom?.['flag.tests.tstyche'] +console.log( + `[tests] tsc: ✅, tstyche: ${tstyche ? '✅' : '❌'}, jest: ${jest ? '✅' : '❌'}, ` +) + +/** + * 1. tsc: the typescript compiler + */ +try { + console.log('[tests] tsc (compile)') + execSync('corepack yarn lint:types', { + stdio: 'inherit', + cwd: process.cwd(), + }) +} catch { + exit(-1) +} + +/** + * 2. tstyche: type tests + */ +if (tstyche) { + try { + console.log('[tests] tstyche (type tests)') + execSync('corepack yarn test:types', { + stdio: 'inherit', + cwd: process.cwd(), + }) + } catch { + exit(-2) + } +} + +/** + * 3. jest: implementation tests + */ +if (jest) { + try { + console.log('[tests] tstyche (implementation tests)') + execSync('corepack yarn test:implementation', { + stdio: 'inherit', + cwd: process.cwd(), + }) + } catch { + exit(-3) + } +} + +/** + * Done! 🥳 + */ diff --git a/exercises/practice/satellite/tsconfig.json b/exercises/practice/satellite/tsconfig.json new file mode 100644 index 000000000..574616245 --- /dev/null +++ b/exercises/practice/satellite/tsconfig.json @@ -0,0 +1,38 @@ +{ + "display": "Configuration for Exercism TypeScript Exercises", + "compilerOptions": { + // Allows you to use the newest syntax, and have access to console.log + // https://www.typescriptlang.org/tsconfig#lib + "lib": ["ES2020", "dom"], + // Make sure typescript is configured to output ESM + // https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#how-can-i-make-my-typescript-project-output-esm + "module": "Node16", + // Since this project is using babel, TypeScript may target something very + // high, and babel will make sure it runs on your local Node version. + // https://babeljs.io/docs/en/ + "target": "ES2020", // ESLint doesn't support this yet: "es2022", + + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + + // Because jest-resolve isn't like node resolve, the absolute path must be .ts + "allowImportingTsExtensions": true, + "noEmit": true, + + // Because we'll be using babel: ensure that Babel can safely transpile + // files in the TypeScript project. + // + // https://babeljs.io/docs/en/babel-plugin-transform-typescript/#caveats + "isolatedModules": true + }, + "include": [ + "*.ts", + "*.tsx", + ".meta/*.ts", + ".meta/*.tsx", + "__typetests__/*.tst.ts" + ], + "exclude": ["node_modules"] +} diff --git a/exercises/practice/satellite/yarn.lock b/exercises/practice/satellite/yarn.lock new file mode 100644 index 000000000..e69de29bb