diff --git a/config.json b/config.json index de2c9a1dc0..84b9928c3d 100644 --- a/config.json +++ b/config.json @@ -1105,6 +1105,19 @@ "Exception handling" ] }, + { + "uuid" : "cfa5741c-9fe9-4cb5-a322-d77ba8145f4b", + "slug" : "change", + "core" : false, + "unlocked_by": "prime-factors", + "difficulty" : 8, + "topics": [ + "Algorithms", + "Mathematics", + "Performance", + "Searching" + ] + }, { "uuid": "0e43944b-0a68-5680-ef11-70999d2df897c894476", "slug": "twelve-days", diff --git a/exercises/change/README.md b/exercises/change/README.md new file mode 100644 index 0000000000..4e59553055 --- /dev/null +++ b/exercises/change/README.md @@ -0,0 +1,44 @@ +# Change + +Correctly determine the change to be given using the least number of coins. + +The solution will need to accept a value of change to be given and an array of +coin denominations. The program returns the array of coin denominations to +produce the correct amount of change. For example, if change for 37 cents +is required from coins with the denominations of 1, 5, 10 and 25 then the +result is an array with the values: 1, 1, 10 and 25. + +## Setup + +Go through the setup instructions for ECMAScript to +install the necessary dependencies: + +http://exercism.io/languages/ecmascript + +## Requirements + +Install assignment dependencies: + +```bash +$ npm install +``` + +## Making the Test Suite Pass + +Execute the tests with: + +```bash +$ npm test +``` + +In the test suite, all tests but the first have been skipped. + +Once you get a test passing, you can enable the next one by +changing `xtest` to `test`. + +## Source + +Unknown + +## Submitting Incomplete Solutions +It's possible to submit an incomplete solution so you can see how others have completed the exercise. diff --git a/exercises/change/change.spec.js b/exercises/change/change.spec.js new file mode 100644 index 0000000000..0966fff29a --- /dev/null +++ b/exercises/change/change.spec.js @@ -0,0 +1,80 @@ +import Change from './change'; + +describe('Change', () => { + test('test change for 1 cent', () => { + const change = new Change(); + const result = change.calculate([1, 5, 10, 25], 1); + expect(result).toEqual([1]); + }); + + xtest('test single coin change', () => { + const change = new Change(); + const result = change.calculate([1, 5, 10, 25, 100], 25); + expect(result).toEqual([25]); + }); + + xtest('test multiple coin change', () => { + const change = new Change(); + const result = change.calculate([1, 5, 10, 25, 100], 15); + expect(result).toEqual([5, 10]); + }); + + xtest('test change with Lilliputian Coins where a greedy algorithm fails', () => { + // https://en.wikipedia.org/wiki/Change-making_problem#Greedy_method + const change = new Change(); + const result = change.calculate([1, 4, 15, 20, 50], 23); + expect(result).toEqual([4, 4, 15]); + }); + + xtest('test change with Lower Elbonia Coins where a greedy algorithm fails', () => { + // https://en.wikipedia.org/wiki/Change-making_problem#Greedy_method + const change = new Change(); + const result = change.calculate([1, 5, 10, 21, 25], 63); + expect(result).toEqual([21, 21, 21]); + }); + + xtest('test large amount of change', () => { + const change = new Change(); + const result = change.calculate([1, 2, 5, 10, 20, 50, 100], 999); + expect(result).toEqual([2, 2, 5, 20, 20, 50, 100, 100, 100, 100, 100, 100, 100, 100, 100]); + }); + + xtest('test possible change without unit coins available', () => { + const change = new Change(); + const result = change.calculate([2, 5, 10, 20, 50], 21); + expect(result).toEqual([2, 2, 2, 5, 10]); + }); + + xtest('test another possible change without unit coins available', () => { + const change = new Change(); + const result = change.calculate([4, 5], 27); + expect(result).toEqual([4, 4, 4, 5, 5, 5]); + }); + + xtest('test no coins make 0 change', () => { + const change = new Change(); + const result = change.calculate([1, 5, 10, 21, 25], 0); + expect(result).toEqual([]); + }); + + xtest('error testing for change smaller than the smallest of coins', () => { + const change = new Change(); + const message = 'The total 3 cannot be represented in the given currency.'; + const test = () => { change.calculate([5, 10], 3); }; + expect(test).toThrowError(Error, message); + }); + + xtest('error testing if no combination can add up to target', () => { + const change = new Change(); + const message = 'The total 94 cannot be represented in the given currency.'; + const test = () => { change.calculate([5, 10], 94); }; + expect(test).toThrowError(Error, message); + }); + + xtest('negative change is rejected', () => { + const change = new Change(); + const message = 'Negative totals are not allowed.'; + const test = () => { change.calculate([1, 2, 5], -5); }; + expect(test).toThrowError(Error, message); + }); +}); diff --git a/exercises/change/example.js b/exercises/change/example.js new file mode 100644 index 0000000000..b306810012 --- /dev/null +++ b/exercises/change/example.js @@ -0,0 +1,120 @@ +// data structure to hold each candidate solution that is generated +class Candidate { + constructor() { + this.wasSearched = false; + this.coins = []; + } + + searched() { + this.wasSearched = true; + } + + isSearched() { + return this.wasSearched; + } + + getCoins() { + return this.coins; + } + + addCoin(coin) { + const sortNum = (a, b) => a - b; + + this.coins.push(coin); + this.coins.sort(sortNum); + } + + getCoinCount() { + return this.coins.length; + } + + getSum() { + const getSum = (total, num) => total + num; + return this.coins.reduce(getSum); + } +} + +export default class Change { + constructor() { + this.candidates = []; + } + calculate(coinArray, target) { + const { candidates } = this; + // fill the array with 0 to start + candidates[target] = 0; + candidates.fill(0); + + const isNumber = element => typeof (element) === 'number'; + + // save a new candidate to the candidates array + const saveCandidate = (candidate) => { + const sum = candidate.getSum(); + + if (sum <= target) { + if (!isNumber(candidates[sum])) { + if (candidates[sum].getCoinCount() > candidate.getCoinCount()) { + candidates[sum] = candidate; + } + } else { + candidates[sum] = candidate; + } + } + }; + + // initialize the candidate array with the given coins only + const initialize = () => { + coinArray.forEach((coin) => { + const candidate = new Candidate(); + candidate.addCoin(coin); + saveCandidate(candidate); + }); + }; + + // is everthing searched? + const isDone = () => candidates.every( + candidate => isNumber(candidate) || candidate.isSearched()); + + // get the next unsearched member of the candidate array + const getNext = () => candidates.find( + candidate => !isNumber(candidate) && !candidate.isSearched()); + + // for the candidate, generate another candate for each of the possible coins + const branch = (current) => { + coinArray.forEach((coin) => { + // make a new Candidate for coin type + const candidate = new Candidate(); + // copy the curent coins into it and add the new coin type + current.getCoins().forEach((currentCoin) => { + candidate.addCoin(currentCoin); + }); + candidate.addCoin(coin); + saveCandidate(candidate); + }); + }; + + // validation checks up front + if (target === 0) return []; + + if (target < 0) { + throw new Error('Negative totals are not allowed.'); + } + + if (target < Math.min.apply(null, coinArray)) { + throw new Error(`The total ${target} cannot be represented in the given currency.`); + } + + + initialize(); + + // process the arrange until everything is searched + while (!isDone()) { + const candidate = getNext(); + branch(candidate); + candidate.searched(); + } + + // print the result + if (!isNumber(candidates[target])) return candidates[target].getCoins(); + throw new Error(`The total ${target} cannot be represented in the given currency.`); + } +} diff --git a/exercises/change/package.json b/exercises/change/package.json new file mode 100644 index 0000000000..3e31078838 --- /dev/null +++ b/exercises/change/package.json @@ -0,0 +1,69 @@ +{ + "name": "xecmascript", + "version": "0.0.0", + "description": "Exercism exercises in ECMAScript 6.", + "author": "Katrina Owen", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/exercism/xecmascript" + }, + "devDependencies": { + "babel-jest": "^20.0.3", + "babel-plugin-transform-builtin-extend": "^1.1.2", + "babel-preset-env": "^1.4.0", + "eslint": "^3.19.0", + "eslint-config-airbnb": "^15.0.1", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-jsx-a11y": "^5.0.1", + "eslint-plugin-react": "^7.0.1", + "jest": "^20.0.4" + }, + "jest": { + "modulePathIgnorePatterns": [ + "package.json" + ] + }, + "babel": { + "presets": [ + "env" + ], + "plugins": [ + [ + "babel-plugin-transform-builtin-extend", + { + "globals": [ + "Error" + ] + } + ], + ["transform-regenerator"] + ] + }, + "scripts": { + "test": "jest --no-cache ./*", + "watch": "jest --no-cache --watch ./*", + "lint": "eslint .", + "lint-test": "eslint . && jest --no-cache ./* " + }, + "eslintConfig": { + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "env": { + "es6": true, + "node": true, + "jest": true + }, + "extends": "airbnb", + "rules": { + "import/no-unresolved": "off", + "import/extensions": "off" + } + }, + "licenses": [ + "MIT" + ], + "dependencies": {} +}