diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 298cd80a2..b939053f2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,6 +74,8 @@ If there is no such issue, you may open one. The baseline of work is as follows: 1. Create a `.test.ts` test file. Here add the tests, per canonical data if possible (more on canonical data below). 1. Create a `.meta/proof.ci.ts` file. Place a working implementation, assuming it's renamed to `.ts` 1. Create `.meta/tests.toml`. If the exercise that is being implemented has test data in the [problem specifications repository][problem-specifications], the contents of this file **must** be a list of UUIDs of the tests that are implemented or not implemented. Scroll down to [tools](#tools) to find the configlet application which aids generating this file _interactively_. +1. Create a `.meta/config.json`. Copy the structure from any other `.meta/config.json`. Fill the `blurb`, `source` and `source_url` according to the `metadata.yml` in the [problem specifications repository][problem-specifications]. Add yourself as author. +1. Create a `.docs/instructions.md` file. Copy the instructions from the [problem specifications repository][problem-specifications] 1. Run the tests locally, using `scripts/test`: `ASSIGNMENT=slug yarn babel-node scripts/test`. 1. Run the linter locally, using `scripts/lint`: `ASSIGNMENT=slug yarn babel-node scripts/lint`. 1. Create an entry in `config.json`: a unique _new_ UUID (you can use the `configlet uuid` tool to generate one, scroll down to [tools](#tools) to see how you can get it), give it a difficulty (should be similar to similar exercises), and make sure the _order_ of the file is sane. Currently the file is ordered first on core - non core, then on difficulty low to high, and finally lexographically. diff --git a/config.json b/config.json index 5bbf008ad..2ea60c71c 100644 --- a/config.json +++ b/config.json @@ -964,24 +964,6 @@ "math" ] }, - { - "slug": "crypto-square", - "name": "Crypto Square", - "uuid": "e92b7444-a5c3-4452-82fd-8c70cf14085a", - "practices": [], - "prerequisites": [], - "difficulty": 9, - "topics": [ - "algorithms", - "arrays", - "conditionals", - "loops", - "regular_expressions", - "sorting", - "text_formatting", - "transforming" - ] - }, { "slug": "robot-simulator", "name": "Robot Simulator", @@ -998,21 +980,7 @@ "strings" ] }, - { - "slug": "kindergarten-garden", - "name": "Kindergarten Garden", - "uuid": "d70d3579-999c-452c-9243-98908e24b47e", - "practices": [], - "prerequisites": [], - "difficulty": 7, - "topics": [ - "arrays", - "conditionals", - "loops", - "strings", - "text_formatting" - ] - }, + { "slug": "armstrong-numbers", "name": "Armstrong Numbers", @@ -1117,6 +1085,21 @@ "difficulty": 7, "topics": ["algorithms", "arrays", "games"] }, + { + "slug": "kindergarten-garden", + "name": "Kindergarten Garden", + "uuid": "d70d3579-999c-452c-9243-98908e24b47e", + "practices": [], + "prerequisites": [], + "difficulty": 7, + "topics": [ + "arrays", + "conditionals", + "loops", + "strings", + "text_formatting" + ] + }, { "slug": "queen-attack", "name": "Queen Attack", @@ -1142,6 +1125,33 @@ "prerequisites": [], "difficulty": 8, "topics": ["algorithms", "closures", "reactive_programming"] + }, + { + "slug": "crypto-square", + "name": "Crypto Square", + "uuid": "e92b7444-a5c3-4452-82fd-8c70cf14085a", + "practices": [], + "prerequisites": [], + "difficulty": 9, + "topics": [ + "algorithms", + "arrays", + "conditionals", + "loops", + "regular_expressions", + "sorting", + "text_formatting", + "transforming" + ] + }, + { + "slug": "knapsack", + "name": "Knapsack", + "uuid": "b80ebb8d-9416-4fc8-91ee-3db99411810c", + "practices": [], + "prerequisites": [], + "difficulty": 9, + "topics": ["algorithms"] } ] }, diff --git a/exercises/practice/knapsack/.docs/instructions.md b/exercises/practice/knapsack/.docs/instructions.md new file mode 100644 index 000000000..a41a2bb18 --- /dev/null +++ b/exercises/practice/knapsack/.docs/instructions.md @@ -0,0 +1,37 @@ +# Description + +In this exercise, let's try to solve a classic problem. + +Bob is a thief. After months of careful planning, he finally manages to +crack the security systems of a high-class apartment. + +In front of him are many items, each with a value (v) and weight (w). Bob, +of course, wants to maximize the total value he can get; he would gladly +take all of the items if he could. However, to his horror, he realizes that +the knapsack he carries with him can only hold so much weight (W). + +Given a knapsack with a specific carrying capacity (W), help Bob determine +the maximum value he can get from the items in the house. Note that Bob can +take only one of each item. + +All values given will be strictly positive. Items will be represented as a +list of pairs, `wi` and `vi`, where the first element `wi` is the weight of +the *i*th item and `vi` is the value for that item. + +For example: + +Items: [ +{ "weight": 5, "value": 10 }, +{ "weight": 4, "value": 40 }, +{ "weight": 6, "value": 30 }, +{ "weight": 4, "value": 50 } +] + +Knapsack Limit: 10 + +For the above, the first item has weight 5 and value 10, the second item has +weight 4 and value 40, and so on. + +In this example, Bob should take the second and fourth item to maximize his +value, which, in this case, is 90. He cannot get more than 90 as his +knapsack has a weight limit of 10. diff --git a/exercises/practice/knapsack/.eslintignore b/exercises/practice/knapsack/.eslintignore new file mode 100644 index 000000000..e3e45c396 --- /dev/null +++ b/exercises/practice/knapsack/.eslintignore @@ -0,0 +1,12 @@ +!.meta + +# Protected or generated +.git +.vscode + +# When using npm +node_modules/* + +# Configuration files +babel.config.js +jest.config.js \ No newline at end of file diff --git a/exercises/practice/knapsack/.eslintrc b/exercises/practice/knapsack/.eslintrc new file mode 100644 index 000000000..3fbb9032c --- /dev/null +++ b/exercises/practice/knapsack/.eslintrc @@ -0,0 +1,23 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json", + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 2020, + "sourceType": "module" + }, + "extends": "@exercism/eslint-config-typescript", + "env": { + "jest": true + }, + "overrides": [ + { + "files": [".meta/proof.ci.ts", ".meta/exemplar.ts", "*.test.ts"], + "excludedFiles": ["custom.test.ts"], + "extends": "@exercism/eslint-config-typescript/maintainers" + } + ] +} diff --git a/exercises/practice/knapsack/.meta/config.json b/exercises/practice/knapsack/.meta/config.json new file mode 100644 index 000000000..e0f37863a --- /dev/null +++ b/exercises/practice/knapsack/.meta/config.json @@ -0,0 +1,12 @@ +{ + "blurb": "Given a knapsack that can only carry a certain weight, determine which items to put in the knapsack in order to maximize their combined value.", + "authors": ["JoshiRaez"], + "contributors": ["SleeplessByte"], + "files": { + "solution": ["knapsack.ts"], + "test": ["knapsack.test.ts"], + "example": [".meta/proof.ci.ts"] + }, + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Knapsack_problem" +} diff --git a/exercises/practice/knapsack/.meta/proof.ci.ts b/exercises/practice/knapsack/.meta/proof.ci.ts new file mode 100644 index 000000000..1aa69850c --- /dev/null +++ b/exercises/practice/knapsack/.meta/proof.ci.ts @@ -0,0 +1,86 @@ +type Item = { + weight: number + value: number +} + +export function maximumValue({ + maximumWeight, + items, +}: { + maximumWeight: number + items: Item[] +}): number { + const maxValueForItemsSizeToWeight: number[][] = new Array(items.length + 1) + .fill(undefined) + .map(() => new Array(maximumWeight + 1)) + + return solve(items.length, maximumWeight, items, maxValueForItemsSizeToWeight) +} + +function solve( + setItemsSize: number, + maxWeight: number, + items: Item[], + maxValueForItemsSizeToWeight: number[][] +): number { + return memoizedMax( + setItemsSize, + maxWeight, + items, + maxValueForItemsSizeToWeight + ) +} + +function memoizedMax( + setItemsSize: number, + maxWeight: number, + items: Item[], + maxValueForItemsSizeToWeight: number[][] +): number { + const alreadyKnownMaxValue = + maxValueForItemsSizeToWeight[setItemsSize][maxWeight] + if (alreadyKnownMaxValue || alreadyKnownMaxValue === 0) + return alreadyKnownMaxValue + + const max = calculateMax( + setItemsSize, + maxWeight, + items, + maxValueForItemsSizeToWeight + ) + maxValueForItemsSizeToWeight[setItemsSize][maxWeight] = max + + return max +} + +function calculateMax( + setItemsSize: number, + maxWeight: number, + items: Item[], + maxValueForItemsSizeToWeight: number[][] +): number { + if (!setItemsSize) return 0 + + return Math.max( + ...Array.from(Array(maxWeight + 1).keys()).map( + (checkingWeight) => + memoizedMax( + setItemsSize - 1, + checkingWeight, + items, + maxValueForItemsSizeToWeight + ) + + valueOfItemIfFits(maxWeight, checkingWeight, items[setItemsSize - 1]) + ) + ) +} + +function valueOfItemIfFits( + maxWeight: number, + alreadyInBagWeight: number, + itemToFit: Item +): number { + if (alreadyInBagWeight + itemToFit.weight <= maxWeight) return itemToFit.value + + return 0 +} diff --git a/exercises/practice/knapsack/.meta/tests.toml b/exercises/practice/knapsack/.meta/tests.toml new file mode 100644 index 000000000..a902dc9f2 --- /dev/null +++ b/exercises/practice/knapsack/.meta/tests.toml @@ -0,0 +1,27 @@ +[a4d7d2f0-ad8a-460c-86f3-88ba709d41a7] +description = "No items" +include = true + +[1d39e98c-6249-4a8b-912f-87cb12e506b0] +description = "One item, but too heavy" +include = true + +[833ea310-6323-44f2-9d27-a278740ffbd8] +description = "Five items. Can\'t be greedy by weight" +include = true + +[277cdc52-f835-4c7d-872b-bff17bab2456] +description = "Five items. Can\'t be greedy by value" +include = true + +[81d8e679-442b-4f7a-8a59-7278083916c9] +description = "Example knapsack" +include = true + +[f23a2449-d67c-4c26-bf3e-cde020f27ecc] +description = "8 items" +include = true + +[7c682ae9-c385-4241-a197-d2fa02c81a11] +description = "15 items" +include = true diff --git a/exercises/practice/knapsack/babel.config.js b/exercises/practice/knapsack/babel.config.js new file mode 100644 index 000000000..eca257951 --- /dev/null +++ b/exercises/practice/knapsack/babel.config.js @@ -0,0 +1,20 @@ +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + useBuiltIns: 'entry', + corejs: '3.17', + }, + ], + '@babel/preset-typescript', + ], + plugins: [ + '@babel/proposal-class-properties', + '@babel/proposal-object-rest-spread', + '@babel/plugin-syntax-bigint', + ], +} diff --git a/exercises/practice/knapsack/jest.config.js b/exercises/practice/knapsack/jest.config.js new file mode 100644 index 000000000..d6b552072 --- /dev/null +++ b/exercises/practice/knapsack/jest.config.js @@ -0,0 +1,19 @@ +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', + }, +} diff --git a/exercises/practice/knapsack/knapsack.test.ts b/exercises/practice/knapsack/knapsack.test.ts new file mode 100644 index 000000000..c20a23c16 --- /dev/null +++ b/exercises/practice/knapsack/knapsack.test.ts @@ -0,0 +1,108 @@ +import { maximumValue } from './knapsack' + +describe('Check if max carriable value is returned', () => { + it('No items', () => { + const input = { + maximumWeight: 100, + items: [], + } + const expected = 0 + expect(maximumValue(input)).toEqual(expected) + }) + + it('One item, but too heavy', () => { + const input = { + maximumWeight: 10, + items: [{ weight: 100, value: 1 }], + } + const expected = 0 + expect(maximumValue(input)).toEqual(expected) + }) + + it("Five items. Can't be greedy by weight", () => { + const input = { + maximumWeight: 10, + items: [ + { weight: 2, value: 5 }, + { weight: 2, value: 5 }, + { weight: 2, value: 5 }, + { weight: 2, value: 5 }, + { weight: 10, value: 21 }, + ], + } + const expected = 21 + expect(maximumValue(input)).toEqual(expected) + }) + + it("Five items. Can't be greedy by value", () => { + const input = { + maximumWeight: 10, + items: [ + { weight: 2, value: 20 }, + { weight: 2, value: 20 }, + { weight: 2, value: 20 }, + { weight: 2, value: 20 }, + { weight: 10, value: 50 }, + ], + } + const expected = 80 + expect(maximumValue(input)).toEqual(expected) + }) + + it('Example knapsack', () => { + const input = { + maximumWeight: 10, + items: [ + { weight: 5, value: 10 }, + { weight: 4, value: 40 }, + { weight: 6, value: 30 }, + { weight: 4, value: 50 }, + ], + } + const expected = 90 + expect(maximumValue(input)).toEqual(expected) + }) + + it('8 items', () => { + const input = { + maximumWeight: 104, + items: [ + { weight: 25, value: 350 }, + { weight: 35, value: 400 }, + { weight: 45, value: 450 }, + { weight: 5, value: 20 }, + { weight: 25, value: 70 }, + { weight: 3, value: 8 }, + { weight: 2, value: 5 }, + { weight: 2, value: 5 }, + ], + } + const expected = 900 + expect(maximumValue(input)).toEqual(expected) + }) + + it('15 items', () => { + const input = { + maximumWeight: 750, + items: [ + { weight: 70, value: 135 }, + { weight: 73, value: 139 }, + { weight: 77, value: 149 }, + { weight: 80, value: 150 }, + { weight: 82, value: 156 }, + { weight: 87, value: 163 }, + { weight: 90, value: 173 }, + { weight: 94, value: 184 }, + { weight: 98, value: 192 }, + { weight: 106, value: 201 }, + { weight: 110, value: 210 }, + { weight: 113, value: 214 }, + { weight: 115, value: 221 }, + { weight: 118, value: 229 }, + { weight: 120, value: 240 }, + ], + } + const expected = 1458 + expect(maximumValue(input)).toEqual(expected) + }) +}) diff --git a/exercises/practice/knapsack/knapsack.ts b/exercises/practice/knapsack/knapsack.ts new file mode 100644 index 000000000..c03219464 --- /dev/null +++ b/exercises/practice/knapsack/knapsack.ts @@ -0,0 +1,14 @@ +type Item = { + weight: number + value: number +} + +export function maximumValue({ + maximumWeight, + items, +}: { + maximumWeight: number + items: Item[] +}): number { + throw new Error('Remove this statement and implement this function') +} diff --git a/exercises/practice/knapsack/package.json b/exercises/practice/knapsack/package.json new file mode 100644 index 000000000..f7b52452a --- /dev/null +++ b/exercises/practice/knapsack/package.json @@ -0,0 +1,33 @@ +{ + "name": "@exercism/typescript-knapsack", + "version": "1.0.0", + "description": "Exercism practice exercise on knapsack", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/exercism/typescript" + }, + "devDependencies": { + "@babel/core": "^7.15.5", + "@babel/plugin-proposal-class-properties": "^7.14.5", + "@babel/plugin-proposal-object-rest-spread": "^7.14.7", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/preset-env": "^7.15.4", + "@babel/preset-typescript": "^7.15.0", + "@types/jest": "^26.0.24", + "@types/node": "^14.17.14", + "@exercism/eslint-config-typescript": "^0.3.0", + "babel-jest": "^26.6.3", + "core-js": "^3.17.2", + "eslint": "^7.32.0", + "eslint-plugin-import": "^2.24.2", + "jest": "^26.6.3", + "typescript": "^4.4.2" + }, + "scripts": { + "test": "yarn lint:types && jest --no-cache", + "lint": "yarn lint:types && yarn lint:ci", + "lint:types": "yarn tsc --noEmit -p .", + "lint:ci": "eslint . --ext .tsx,.ts" + } +} diff --git a/exercises/practice/knapsack/tsconfig.json b/exercises/practice/knapsack/tsconfig.json new file mode 100644 index 000000000..7314e3544 --- /dev/null +++ b/exercises/practice/knapsack/tsconfig.json @@ -0,0 +1,19 @@ +{ + "display": "Configuration for Node LTS", + "compilerOptions": { + "lib": ["es2020"], + "module": "commonjs", + "target": "es2020", + + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + + // Because we'll be using babel + // Ensure that Babel can safely transpile files in the TypeScript project + "isolatedModules": true + }, + "include": ["*", ".meta/*"], + "exclude": ["node_modules"] +}