diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 54aeff6dac747d8cc0a79cccdf2071f439a44d80..d973ee1854dc6f37789e28b60079c2a4a4640873 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,3 +21,4 @@ include: - local: "/puissance-4/.gitlab-ci.yml" - local: "/trip-service-kata-typescript/.gitlab-ci.yml" - local: "/roman-calculator/.gitlab-ci.yml" + - local: "/h2g2/.gitlab-ci.yml" diff --git a/README.md b/README.md index 8cd766f2a555ae960c8f4dc979ea9a28a612022b..fcfc29ee791ffa641894ae6a604502ede24a1274 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,4 @@ Ce dépôt Git a pour but de partager les différents ateliers pouvant être ré | [Trip service (TypeScript)](/trip-service-kata-typescript) | Kata | Moyenne | | [Roman calculator](/roman-calculator) | Kata | Moyenne | | [Atomic Design I and II](https://gitlab.ippon.fr/arey/pattern-library) | Front | Moyenne | +| [H2G2 (TypeScript)](/h2g2) | Kata | Moyenne | diff --git a/h2g2/.eslintrc.js b/h2g2/.eslintrc.js new file mode 100644 index 0000000000000000000000000000000000000000..fd818a475e66b5f69bb8806570d63f8ca46ab19f --- /dev/null +++ b/h2g2/.eslintrc.js @@ -0,0 +1,71 @@ +module.exports = { + root: true, + env: { + node: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:import/errors', + 'plugin:import/warnings', + 'plugin:import/typescript', + ], + plugins: ['prettier', '@typescript-eslint'], + rules: { + quotes: ['error', 'single', { avoidEscape: true }], + 'comma-dangle': [ + 'error', + { + arrays: 'always-multiline', + objects: 'always-multiline', + imports: 'always-multiline', + exports: 'always-multiline', + functions: 'never', + }, + ], + 'import/order': [ + 'error', + { + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + 'newlines-between': 'always', + }, + ], + 'prettier/prettier': [ + 'error', + { + singleQuote: true, + trailingComma: 'es5', + printWidth: 140, + }, + ], + }, + settings: { + 'import/resolver': { + typescript: { + alwaysTryTypes: true, + }, + }, + 'import/core-modules': ['sinon'], + }, + parserOptions: { + parser: '@typescript-eslint/parser', + }, + overrides: [ + { + files: ['**/__tests__/*.{j,t}s?(x)', '**/*.spec.{j,t}s?(x)'], + env: { + jest: true, + }, + }, + { + files: ['**/*.ts', '**/*.tsx'], + rules: { + 'no-unused-vars': ['off'], + 'no-undef': ['off'], + }, + }, + ], +}; diff --git a/h2g2/.gitignore b/h2g2/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..291a1654e4fa05c2658e11d7b343d8ce7e562a0b --- /dev/null +++ b/h2g2/.gitignore @@ -0,0 +1,3 @@ +/node_modules +/.idea + diff --git a/h2g2/.gitlab-ci.yml b/h2g2/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..761b791f9a1192e8f12f3bf46d362a1f4d19663f --- /dev/null +++ b/h2g2/.gitlab-ci.yml @@ -0,0 +1,11 @@ +h2g2: + variables: + PROJECT_FOLDER: "h2g2" + extends: .node14 + only: + refs: + - master + - merge_requests + changes: + - ".gitlab-common-ci.yml" + - "h2g2/**/*" diff --git a/h2g2/Potter.md b/h2g2/Potter.md new file mode 100644 index 0000000000000000000000000000000000000000..73814f0dfff09485fb6ba3362e739681cf75024e --- /dev/null +++ b/h2g2/Potter.md @@ -0,0 +1,106 @@ +# Potter + +If you want to try this Kata for yourself or at your dojo meeting, read +the problem description and see if the Kata appeals to you. The rest is +commentary and helpful clues for if you get stuck solving it. I would +recommend trying the Kata for yourself before reading too much of it. + +## Problem Description + +Once upon a time there was a series of 5 books about a very English hero +called Harry. (At least when this Kata was invented, there were only 5. +Since then they have multiplied) Children all over the world thought he +was fantastic, and, of course, so did the publisher. So in a gesture of +immense generosity to mankind, (and to increase sales) they set up the +following pricing model to take advantage of Harry's magical powers. + +One copy of any of the five books costs 8 EUR. If, however, you buy two +different books from the series, you get a 5% discount on those two +books. If you buy 3 different books, you get a 10% discount. With 4 +different books, you get a 20% discount. If you go the whole hog, and +buy all 5, you get a huge 25% discount. + +Note that if you buy, say, four books, of which 3 are different titles, +you get a 10% discount on the 3 that form part of a set, but the fourth +book still costs 8 EUR. + +Potter mania is sweeping the country and parents of teenagers everywhere +are queueing up with shopping baskets overflowing with Potter books. +Your mission is to write a piece of code to calculate the price of any +conceivable shopping basket, giving as big a discount as possible. + +For example, how much does this basket of books cost? + +* 2 copies of the first book +* 2 copies of the second book +* 2 copies of the third book +* 1 copy of the fourth book +* 1 copy of the fifth book + +answer : + + (4 * 8) - 20% [first book, second book, third book, fourth book] + + (4 * 8) - 20% [first book, second book, third book, fifth book] + = 25.6 * 2 + = 51.20 + +## Clues + +You’ll find that this Kata is easy at the start. You can get going with +tests for baskets of 0 books, 1 book, 2 identical books, 2 different +books… and it is not too difficult to work in small steps and gradually +introduce complexity. + +However, the twist becomes apparent when you sit down and work out how +much you think the sample basket above should cost. It isn’t +`5 * 8 * 0.75 + 3 *8 * 0.90`. It is in fact `4 * 8 * 0.8 + 4 * 8 * 0.8`. So the trick +with this Kata is not that the acceptance test you’ve been given is +wrong. The trick is that you have to write some code that is intelligent +enough to notice that two sets of four books is cheaper than a set of +five and a set of three. + +You will have to introduce a certain amount of clever optimization +algorithm. But not too much! This problem does not require a fully +fledged general purpose optimizer. Try to solve just this problem well +in order to share it for everyone or even in the ??? . Trust that you +can generalize and improve your solution if and when new requirements +come along. + +- This application has nice application for + +## Suggested Test Cases + +(Originally posted at xp-france) + + def testBasics + assert_equal(0, price([])) + assert_equal(8, price([1])) + assert_equal(8, price([2])) + assert_equal(8, price([3])) + assert_equal(8, price([4])) + assert_equal(8 * 3, price([1, 1, 1])) + end + + def testSimpleDiscounts + assert_equal(8 * 2 * 0.95, price([0, 1])) + assert_equal(8 * 3 * 0.9, price([0, 2, 4])) + assert_equal(8 * 4 * 0.8, price([0, 1, 2, 4])) + assert_equal(8 * 5 * 0.75, price([0, 1, 2, 3, 4])) + end + + def testSeveralDiscounts + assert_equal(8 + (8 * 2 * 0.95), price([0, 0, 1])) + assert_equal(2 * (8 * 2 * 0.95), price([0, 0, 1, 1])) + assert_equal((8 * 4 * 0.8) + (8 * 2 * 0.95), price([0, 0, 1, 2, 2, 3])) + assert_equal(8 + (8 * 5 * 0.75), price([0, 1, 1, 2, 3, 4])) + end + + def testEdgeCases + assert_equal(2 * (8 * 4 * 0.8), price([0, 0, 1, 1, 2, 2, 3, 4])) + assert_equal(3 * (8 * 5 * 0.75) + 2 * (8 * 4 * 0.8), + price([0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + 2, 2, 2, 2, + 3, 3, 3, 3, 3, + 4, 4, 4, 4])) + end diff --git a/h2g2/README.md b/h2g2/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b7e90ca06053b059137225784c1e5586eaff1059 --- /dev/null +++ b/h2g2/README.md @@ -0,0 +1,9 @@ +# H2G2 + +Kata Potter, mais on l'a renommé H2G2 parce que la série Harry Potter comporte 7 livres et non 5 alors que la trilogie en cinq volumes de Douglas Adams en comporte 5, en même temps c'est ce qui est écrit dans l'intitulé. + +- **Auteurs** : Léa COSTON et Anthony REY +- **Date** : 06/10/2020 +- **Langage** : TypeScript +- **Niveau** : Moyen +- **Replay** : [H2G2 Kata - Léa et Anthony](https://www.youtube.com/watch?v=XB2c8fj_Aus) diff --git a/h2g2/jest.config.js b/h2g2/jest.config.js new file mode 100644 index 0000000000000000000000000000000000000000..c4fc7e4220dfb1bb7c0c1c72c0db0de400344234 --- /dev/null +++ b/h2g2/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + roots: ['<rootDir>/src'], + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + testEnvironment: 'node', +}; diff --git a/h2g2/package.json b/h2g2/package.json new file mode 100644 index 0000000000000000000000000000000000000000..2c2831633b19dc07dea41960cf36029114e4e914 --- /dev/null +++ b/h2g2/package.json @@ -0,0 +1,28 @@ +{ + "name": "h2g2", + "version": "1.0.0", + "description": "Code retreat", + "main": "src/index.ts", + "dependencies": { + "@types/jest": "^26.0.15", + "@typescript-eslint/eslint-plugin": "^4.6.1", + "@typescript-eslint/parser": "^4.6.1", + "eslint": "^7.13.0", + "eslint-import-resolver-typescript": "^2.3.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-prettier": "^3.1.4", + "jest": "^26.6.3", + "prettier": "^2.1.2", + "ts-jest": "^26.4.3", + "typescript": "^4.0.5" + }, + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:watch:all": "jest --watchAll", + "eslint:ci": "eslint './**/*.{ts,js}'", + "eslint": "eslint './**/*.{ts,js}' --fix" + }, + "author": "Anthony REY", + "license": "MIT" +} diff --git a/h2g2/src/.gitkeep b/h2g2/src/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/h2g2/src/H2G2.spec.ts b/h2g2/src/H2G2.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1be092f9faf1d6b5cc47b5c6df69b0f72a21a5b3 --- /dev/null +++ b/h2g2/src/H2G2.spec.ts @@ -0,0 +1,121 @@ +const BOOK_PRICE = 8; +const DISCOUNT_TWO_BOOKS = 0.95; +const DISCOUNT_THREE_BOOKS = 0.9; +const DISCOUNT_FOUR_BOOKS = 0.8; +const DISCOUNT_FIVE_BOOKS = 0.75; + +const DISCOUNT = new Map([ + [2, DISCOUNT_TWO_BOOKS], + [3, DISCOUNT_THREE_BOOKS], + [4, DISCOUNT_FOUR_BOOKS], + [5, DISCOUNT_FIVE_BOOKS], +]); + +enum Book { + FIRST = 'FIRST', + SECOND = 'SECOND', + THIRD = 'THIRD', + FOURTH = 'FOURTH', + FIFTH = 'FIFTH', +} + +const discountFor = (distinct: number): number => DISCOUNT.get(distinct) || 1; + +const maxSerie = (occurences: Map<Book, number>) => + Array.from(occurences) + .map(([, number]) => number) + .filter((number) => number > 0).length; + +function decrementOccurence(occurence: number) { + if (occurence === 0) { + return 0; + } + return occurence - 1; +} + +const decrementSerie = (occurences: Map<Book, number>): Map<Book, number> => + new Map(Array.from(occurences).map(([book, occurence]) => [book, decrementOccurence(occurence)])); + +const getOccurences = (books: Book[]) => + new Map( + [Book.FIRST, Book.SECOND, Book.THIRD, Book.FOURTH, Book.FIFTH].map((book) => [ + book, + books.filter((currentBook) => currentBook === book).length, + ]) + ); + +const fill = (number: number, size: number): number[] => Array(number).fill(size); + +const optimizePacks = (packs: number[]): number[] => { + const one = packs.filter((size) => size === 1).length; + const two = packs.filter((size) => size === 2).length; + const three = packs.filter((size) => size === 3).length; + const four = packs.filter((size) => size === 4).length; + const five = packs.filter((size) => size === 5).length; + + const difference = Math.min(three, five); + return [ + ...fill(one, 1), + ...fill(two, 2), + ...fill(three - difference, 3), + ...fill(four + difference * 2, 4), + ...fill(five - difference, 5), + ]; +}; + +const price = (books: Book[]): number => { + let occurences = getOccurences(books); + + const packs: number[] = []; + + while (maxSerie(occurences) > 0) { + packs.push(maxSerie(occurences)); + occurences = decrementSerie(occurences); + } + + return optimizePacks(packs) + .map((size) => size * BOOK_PRICE * discountFor(size)) + .reduce((accumulation, current) => accumulation + current, 0); +}; + +describe('H2G2', () => { + it('Price of no book should be 0', () => { + expect(price([])).toBe(0); + }); + + it('Price of one book is 8', () => { + expect(price([Book.FIRST])).toBe(8); + }); + + it('Price of same book is 8 each', () => { + expect(price([Book.SECOND, Book.SECOND, Book.SECOND, Book.SECOND])).toBe(32); + }); + + it('Price two different books should have 5% off', () => { + expect(price([Book.THIRD, Book.FOURTH])).toBe(8 * 2 * 0.95); + }); + + it('Price three different books should have 10% off', () => { + expect(price([Book.THIRD, Book.FOURTH, Book.FIFTH])).toBe(8 * 3 * 0.9); + }); + + it('Price four different books should have 20% off', () => { + expect(price([Book.FIRST, Book.THIRD, Book.FOURTH, Book.FIFTH])).toBe(8 * 4 * 0.8); + }); + + it('Price five different books should have 25% off', () => { + expect(price([Book.FIRST, Book.SECOND, Book.THIRD, Book.FOURTH, Book.FIFTH])).toBe(8 * 5 * 0.75); + }); + + it('Should discount of two different series be independent', () => { + expect(price([Book.FIRST, Book.SECOND, Book.THIRD, Book.FOURTH, Book.FIFTH, Book.FIRST, Book.SECOND])).toBe( + 8 * 5 * 0.75 + 8 * 2 * 0.95 + ); + }); + + it('Should treat price of edge case', () => { + expect(price([Book.FIRST, Book.SECOND, Book.THIRD, Book.FOURTH, Book.FIFTH, Book.FIRST, Book.SECOND, Book.FOURTH])).toBe( + 8 * 4 * 2 * 0.8 + ); + }); +}); diff --git a/h2g2/tsconfig.json b/h2g2/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..67e3cb36387cd46f5592853091cbc7dc1ff11091 --- /dev/null +++ b/h2g2/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "noImplicitAny": true, + "removeComments": true, + "preserveConstEnums": true, + "outFile": "dist/main.js", + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "**/*.spec.ts" + ] +}