├── .gitignore ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src └── kata │ └── golf │ ├── hole1 │ ├── incalculable.ts │ ├── takehomecalculator.spec.ts │ └── takehomecalculator.ts │ ├── hole2 │ ├── incalculable.ts │ ├── takehomecalculator.spec.ts │ └── takehomecalculator.ts │ ├── hole3 │ ├── incalculable.ts │ ├── takehomecalculator.spec.ts │ └── takehomecalculator.ts │ ├── hole4 │ ├── incalculable.ts │ ├── takehomecalculator.spec.ts │ └── takehomecalculator.ts │ ├── hole5 │ ├── incalculable.ts │ ├── money.ts │ ├── takehomecalculator.spec.ts │ └── takehomecalculator.ts │ ├── hole6 │ ├── incalculable.ts │ ├── money.ts │ ├── takehomecalculator.spec.ts │ ├── takehomecalculator.ts │ └── taxrate.ts │ └── hole7 │ ├── incalculable.ts │ ├── money.ts │ ├── takehomecalculator.spec.ts │ ├── takehomecalculator.ts │ └── taxrate.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | coverage -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Refactoring Golf 2 | 3 | Refactoring Golf is a game designed to stretch your refactoring muscles and get you to explore your IDE to see what's really possible using shortcuts and automation. 4 | 5 | This repo contains several source trees, or numbered "Holes" based on a single exercise. Each hole carries on from the last for a single exercise - which is the application of a tax amount to a set of monetary amount. 6 | 7 | Your goal is to safely and efficiently as possible refactor the Hole-X code to look like the Hole X+1 code. You must aim to do it in as few "strokes" as possible. 8 | 9 | A "stroke" is essentially a change made to the code, and every stroke costs you points. 10 | 11 | Your pairing partner should carefully score you as follows: 12 | 13 | - 1 point for every change made to the code using a shortcut or automated IDE feature (e.g., an automated refactoring, code template, or Find/Replace) 14 | - 2 points for every manual edit. Note that a single "edit" could cover multiple lines of code. 15 | - Double points for every change made while the code cannot pass the tests after the previous change. 16 | - Zero points for code formatting (e.g., deleting whitespace or optimizing imports). 17 | 18 | Allow yourselves a maximum of 2 attempts at each round to determine your best score. 19 | 20 | ## Hints: 21 | 22 | 1. You may find that customising your IDE is useful (e.g. custom code templates, or even custom refactorings.) 23 | 24 | 2. If possible, it would be a good idea to have the two versions (Hole X and Hole X+1) of each set of source files open on different machines, as you could easily tie yourself in knots editing the wrong files! 25 | 26 | 3. Keep that second machine available as a tooling environment. Writing custom tools (scripts, templates etc) costs you zero points in refactoring golf. 27 | 28 | 29 | ## Acknowledgements: 30 | These instructions were mostly stolen from @daviddenton's Java version 31 | 32 | 33 | 34 | # Typescript + Jest 35 | 36 | ## Install & Run 37 | `npm i` 38 | 39 | `npm start` 40 | 41 | ## Only run specific Kata unit test 42 | `npm start ` (per describe block) 43 | 44 | 45 | ie: `npm start subtract` 46 | 47 | ## Watch a specific Kata unit test 48 | 49 | `npm run watch ` 50 | 51 | 52 | ie: `npm run watch subtract` 53 | 54 | 55 | ## Watch All tests 56 | 57 | `npm run watch-all` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src'], 3 | preset: 'ts-jest', 4 | // collectCoverage: true, 5 | // collectCoverageFrom: ['./src/**/*.{js,jsx,ts}', '!**/node_modules/**', '!**/vendor/**'], 6 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-kata", 3 | "version": "1.0.0", 4 | "description": "Typescript Kata Seed based on Justin Conklin version", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "jest --", 8 | "watch": "jest --watch --", 9 | "watch-all": "jest --watchAll" 10 | }, 11 | "author": "javier.chacana@codurance.com", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@types/node": "^14.14.31", 15 | "deep-equal": "^2.0.5", 16 | "typescript": "^4.2.2", 17 | "ts-node": "^9.1.1", 18 | "tslib": "^2.1.0" 19 | }, 20 | "devDependencies": { 21 | "@types/deep-equal": "^1.0.1", 22 | "@types/jest": "^26.0.20", 23 | "jest": "^26.6.3", 24 | "jest-mock-extended": "^1.0.13", 25 | "ts-jest": "^26.5.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/kata/golf/hole1/incalculable.ts: -------------------------------------------------------------------------------- 1 | export class Incalculable extends Error{ 2 | 3 | } -------------------------------------------------------------------------------- /src/kata/golf/hole1/takehomecalculator.spec.ts: -------------------------------------------------------------------------------- 1 | import {Pair, Takehomecalculator} from "./takehomecalculator"; 2 | 3 | describe('TakeHomeCalculator', () => { 4 | it("can calculate tax", () => { 5 | const first: number = new Takehomecalculator(10).netAmount(new Pair(40, "GBP"), new Pair(50, "GBP"), new Pair(60, "GBP")).first; 6 | expect(Math.trunc(first)).toBe(135) 7 | }) 8 | 9 | it("cannot sum different currencies", () => { 10 | expect(() => new Takehomecalculator(10).netAmount(new Pair(4, "GBP"), new Pair(5, "USD"))).toThrow() 11 | }) 12 | }) -------------------------------------------------------------------------------- /src/kata/golf/hole1/takehomecalculator.ts: -------------------------------------------------------------------------------- 1 | import {Incalculable} from "./incalculable"; 2 | 3 | export class Pair { 4 | public first: A; 5 | public second: B; 6 | 7 | constructor(first: A, second: B) { 8 | this.first = first; 9 | this.second = second; 10 | } 11 | } 12 | 13 | export class Takehomecalculator { 14 | private percent: number 15 | 16 | constructor(percent: number) { 17 | this.percent = percent; 18 | } 19 | 20 | netAmount(first: Pair, ...rest : Pair[] ): Pair { 21 | 22 | const pairs: Array> = Array.from(rest); 23 | let total: Pair = first 24 | 25 | for (let next of pairs) { 26 | if (next.second != total.second) { 27 | throw new Incalculable() 28 | } 29 | } 30 | 31 | for (const next of pairs) { 32 | total = new Pair(total.first + next.first, next.second) 33 | } 34 | 35 | const amount:number = total.first * (this.percent / 100.0 ); 36 | const tax: Pair = new Pair(Math.trunc(amount), first.second); 37 | 38 | if (total.second == tax.second) { 39 | return new Pair(total.first - tax.first, first.second) 40 | } else { 41 | throw new Incalculable() 42 | } 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/kata/golf/hole2/incalculable.ts: -------------------------------------------------------------------------------- 1 | export class Incalculable extends Error{ 2 | 3 | } -------------------------------------------------------------------------------- /src/kata/golf/hole2/takehomecalculator.spec.ts: -------------------------------------------------------------------------------- 1 | import {Pair, Takehomecalculator} from "./takehomecalculator"; 2 | 3 | 4 | describe('TakeHomeCalculator', () => { 5 | it("can calculate tax", () => { 6 | const first: number = new Takehomecalculator(10).netAmount(new Pair(40, "GBP"), new Pair(50, "GBP"), new Pair(60, "GBP")).first; 7 | expect(Math.trunc(first)).toBe(135) 8 | }) 9 | 10 | it("cannot sum different currencies", () => { 11 | expect(() => new Takehomecalculator(10).netAmount(new Pair(4, "GBP"), new Pair(5, "USD"))).toThrow() 12 | }) 13 | }) -------------------------------------------------------------------------------- /src/kata/golf/hole2/takehomecalculator.ts: -------------------------------------------------------------------------------- 1 | import {Incalculable} from "./incalculable"; 2 | 3 | export class Pair { 4 | public first: A; 5 | public second: B; 6 | 7 | constructor(first: A, second: B) { 8 | this.first = first; 9 | this.second = second; 10 | } 11 | } 12 | 13 | export class Takehomecalculator { 14 | private percent: number 15 | 16 | constructor(percent: number) { 17 | this.percent = percent; 18 | } 19 | 20 | netAmount(first: Pair, ...rest : Pair[] ): Pair { 21 | 22 | const pairs: Array> = Array.from(rest); 23 | let total: Pair = first 24 | 25 | for (let next of pairs) { 26 | if (next.second !== total.second) { 27 | throw new Incalculable() 28 | } 29 | } 30 | 31 | for (const next of pairs) { 32 | total = new Pair(total.first + next.first, next.second) 33 | } 34 | 35 | const amount:number = total.first * (this.percent / 100.0 ); 36 | const tax: Pair = new Pair(Math.trunc(amount), first.second); 37 | 38 | if (total.second !== tax.second) { 39 | throw new Incalculable() 40 | } 41 | return new Pair(total.first - tax.first, first.second) 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /src/kata/golf/hole3/incalculable.ts: -------------------------------------------------------------------------------- 1 | export class Incalculable extends Error{ 2 | 3 | } -------------------------------------------------------------------------------- /src/kata/golf/hole3/takehomecalculator.spec.ts: -------------------------------------------------------------------------------- 1 | import {Money, Takehomecalculator} from "./takehomecalculator"; 2 | 3 | 4 | describe('TakeHomeCalculator', () => { 5 | it("can calculate tax", () => { 6 | const first: number = new Takehomecalculator(10).netAmount(new Money(40, "GBP"), new Money(50, "GBP"), new Money(60, "GBP")).value; 7 | expect(Math.trunc(first)).toBe(135) 8 | }) 9 | 10 | it("cannot sum different currencies", () => { 11 | expect(() => new Takehomecalculator(10).netAmount(new Money(4, "GBP"), new Money(5, "USD"))).toThrow() 12 | }) 13 | }) -------------------------------------------------------------------------------- /src/kata/golf/hole3/takehomecalculator.ts: -------------------------------------------------------------------------------- 1 | import {Incalculable} from "./incalculable"; 2 | 3 | export class Money { 4 | public value: number; 5 | public currency: string; 6 | 7 | 8 | constructor(value: number, currency: string) { 9 | this.value = value; 10 | this.currency = currency; 11 | } 12 | } 13 | 14 | export class Takehomecalculator { 15 | private percent: number 16 | 17 | constructor(percent: number) { 18 | this.percent = percent; 19 | } 20 | 21 | netAmount(first: Money, ...rest : Money[] ): Money { 22 | 23 | const monies: Array = Array.from(rest); 24 | let total: Money = first 25 | 26 | for (let next of monies) { 27 | if (next.currency !== total.currency) { 28 | throw new Incalculable() 29 | } 30 | } 31 | 32 | for (const next of monies) { 33 | total = new Money(total.value + next.value, next.currency) 34 | } 35 | 36 | const amount:number = total.value * (this.percent / 100.0 ); 37 | const tax: Money = new Money(Math.trunc(amount), first.currency); 38 | 39 | if (total.currency !== tax.currency) { 40 | throw new Incalculable() 41 | } 42 | return new Money(total.value - tax.value, first.currency) 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/kata/golf/hole4/incalculable.ts: -------------------------------------------------------------------------------- 1 | export class Incalculable extends Error{ 2 | 3 | } -------------------------------------------------------------------------------- /src/kata/golf/hole4/takehomecalculator.spec.ts: -------------------------------------------------------------------------------- 1 | import {Money, Takehomecalculator} from "./takehomecalculator"; 2 | 3 | 4 | describe('TakeHomeCalculator', () => { 5 | it("can calculate tax", () => { 6 | const first: number = new Takehomecalculator(10).netAmount(new Money(40, "GBP"), new Money(50, "GBP"), new Money(60, "GBP")).value; 7 | expect(Math.trunc(first)).toBe(135) 8 | }) 9 | 10 | it("cannot sum different currencies", () => { 11 | expect(() => new Takehomecalculator(10).netAmount(new Money(4, "GBP"), new Money(5, "USD"))).toThrow() 12 | }) 13 | }) -------------------------------------------------------------------------------- /src/kata/golf/hole4/takehomecalculator.ts: -------------------------------------------------------------------------------- 1 | import {Incalculable} from "./incalculable"; 2 | 3 | export class Money { 4 | public value: number; 5 | public currency: string; 6 | 7 | 8 | constructor(value: number, currency: string) { 9 | this.value = value; 10 | this.currency = currency; 11 | } 12 | 13 | plus(other: Money): Money { 14 | if(other.currency !== this.currency){ 15 | throw new Incalculable(); 16 | } 17 | return new Money(this.value + other.value, other.currency); 18 | } 19 | } 20 | 21 | export class Takehomecalculator { 22 | private percent: number; 23 | 24 | constructor(percent: number) { 25 | this.percent = percent; 26 | } 27 | 28 | netAmount(first: Money, ...rest : Money[] ): Money { 29 | 30 | const monies: Array = Array.from(rest); 31 | let total: Money = first; 32 | 33 | for (const next of monies) { 34 | total = total.plus(next); 35 | } 36 | 37 | const amount:number = total.value * (this.percent / 100.0 ); 38 | const tax: Money = new Money(Math.trunc(amount), first.currency); 39 | 40 | if (total.currency !== tax.currency) { 41 | throw new Incalculable() 42 | } 43 | return new Money(total.value - tax.value, first.currency) 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/kata/golf/hole5/incalculable.ts: -------------------------------------------------------------------------------- 1 | export class Incalculable extends Error{ 2 | 3 | } -------------------------------------------------------------------------------- /src/kata/golf/hole5/money.ts: -------------------------------------------------------------------------------- 1 | import {Incalculable} from "./incalculable"; 2 | 3 | export class Money { 4 | public value: number; 5 | public currency: string; 6 | 7 | 8 | private constructor(value: number, currency: string) { 9 | this.value = value; 10 | this.currency = currency; 11 | } 12 | 13 | plus(other: Money): Money { 14 | if (other.currency !== this.currency) { 15 | throw new Incalculable(); 16 | } 17 | return money(this.value + other.value, other.currency); 18 | } 19 | 20 | minus(other: Money): Money { 21 | if(this.currency !== other.currency) { 22 | throw new Incalculable(); 23 | } 24 | return money(this.value - other.value, this.currency); 25 | } 26 | 27 | static money(value: number, currency: string): Money { 28 | return new Money(value, currency); 29 | } 30 | } 31 | 32 | export function money(value: number, currency: string): Money { 33 | return Money.money(value, currency); 34 | } -------------------------------------------------------------------------------- /src/kata/golf/hole5/takehomecalculator.spec.ts: -------------------------------------------------------------------------------- 1 | import {Takehomecalculator} from "./takehomecalculator"; 2 | import {money} from "./money"; 3 | 4 | describe('TakeHomeCalculator', () => { 5 | it("can calculate tax", () => { 6 | const first: number = new Takehomecalculator(10).netAmount(money(40, "GBP"), money(50, "GBP"), money(60, "GBP")).value; 7 | expect(Math.trunc(first)).toBe(135) 8 | }) 9 | 10 | it("cannot sum different currencies", () => { 11 | expect(() => new Takehomecalculator(10).netAmount(money(4, "GBP"), money(5, "USD"))).toThrow() 12 | }) 13 | }) -------------------------------------------------------------------------------- /src/kata/golf/hole5/takehomecalculator.ts: -------------------------------------------------------------------------------- 1 | import {Incalculable} from "./incalculable"; 2 | import {money, Money} from "./money"; 3 | 4 | export class Takehomecalculator { 5 | private readonly percent: number; 6 | 7 | constructor(percent: number) { 8 | this.percent = percent; 9 | } 10 | 11 | netAmount(first: Money, ...rest : Money[] ): Money { 12 | 13 | const monies: Array = Array.from(rest); 14 | let total: Money = first; 15 | 16 | for (const next of monies) { 17 | total = total.plus(next); 18 | } 19 | 20 | const amount:number = total.value * (this.percent / 100.0 ); 21 | const tax: Money = money(Math.trunc(amount), first.currency); 22 | 23 | if (total.currency !== tax.currency) { 24 | throw new Incalculable(); 25 | } 26 | return total.minus(tax); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/kata/golf/hole6/incalculable.ts: -------------------------------------------------------------------------------- 1 | export class Incalculable extends Error{ 2 | 3 | } -------------------------------------------------------------------------------- /src/kata/golf/hole6/money.ts: -------------------------------------------------------------------------------- 1 | import {Incalculable} from "./incalculable"; 2 | 3 | export class Money { 4 | public value: number; 5 | public currency: string; 6 | 7 | 8 | private constructor(value: number, currency: string) { 9 | this.value = value; 10 | this.currency = currency; 11 | } 12 | 13 | plus(other: Money): Money { 14 | if (other.currency !== this.currency) { 15 | throw new Incalculable(); 16 | } 17 | return money(this.value + other.value, other.currency); 18 | } 19 | 20 | minus(other: Money): Money { 21 | if(this.currency !== other.currency) { 22 | throw new Incalculable(); 23 | } 24 | return money(this.value - other.value, this.currency); 25 | } 26 | 27 | static money(value: number, currency: string): Money { 28 | return new Money(value, currency); 29 | } 30 | } 31 | 32 | export function money(value: number, currency: string): Money { 33 | return Money.money(value, currency); 34 | } -------------------------------------------------------------------------------- /src/kata/golf/hole6/takehomecalculator.spec.ts: -------------------------------------------------------------------------------- 1 | import {Takehomecalculator} from "./takehomecalculator"; 2 | import {money} from "./money"; 3 | import {taxRate} from "./taxrate"; 4 | 5 | describe('TakeHomeCalculator', () => { 6 | it("can calculate tax", () => { 7 | const first: number = new Takehomecalculator(taxRate(10)).netAmount(money(40, "GBP"), money(50, "GBP"), money(60, "GBP")).value; 8 | expect(Math.trunc(first)).toBe(135) 9 | }) 10 | 11 | it("cannot sum different currencies", () => { 12 | expect(() => new Takehomecalculator(taxRate(10)).netAmount(money(4, "GBP"), money(5, "USD"))).toThrow() 13 | }) 14 | }) -------------------------------------------------------------------------------- /src/kata/golf/hole6/takehomecalculator.ts: -------------------------------------------------------------------------------- 1 | import {Money} from "./money"; 2 | import {TaxRate} from "./taxrate"; 3 | 4 | export class Takehomecalculator { 5 | private readonly taxRate: TaxRate; 6 | 7 | constructor(taxRate: TaxRate) { 8 | this.taxRate = taxRate; 9 | } 10 | 11 | netAmount(first: Money, ...rest : Money[] ): Money { 12 | 13 | const monies: Array = Array.from(rest); 14 | let total: Money = first; 15 | 16 | for (const next of monies) { 17 | total = total.plus(next); 18 | } 19 | 20 | const tax: Money = this.taxRate.apply(total); 21 | return total.minus(tax); 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/kata/golf/hole6/taxrate.ts: -------------------------------------------------------------------------------- 1 | import {money, Money} from "./money"; 2 | 3 | export function taxRate(percent: number): TaxRate { 4 | return TaxRate.taxRate(percent); 5 | } 6 | 7 | export class TaxRate { 8 | 9 | public percent: number; 10 | 11 | private constructor(percent: number) { 12 | this.percent = percent; 13 | } 14 | 15 | static taxRate(percent: number): TaxRate { 16 | return new TaxRate(percent); 17 | } 18 | 19 | apply(total: Money): Money { 20 | const amount: number = total.value * (this.percent / 100.0); 21 | return money(Math.trunc(amount), total.currency); 22 | } 23 | } -------------------------------------------------------------------------------- /src/kata/golf/hole7/incalculable.ts: -------------------------------------------------------------------------------- 1 | export class Incalculable extends Error{ 2 | 3 | } -------------------------------------------------------------------------------- /src/kata/golf/hole7/money.ts: -------------------------------------------------------------------------------- 1 | import {Incalculable} from "./incalculable"; 2 | 3 | export class Money { 4 | public value: number; 5 | public currency: string; 6 | 7 | 8 | private constructor(value: number, currency: string) { 9 | this.value = value; 10 | this.currency = currency; 11 | } 12 | 13 | plus(other: Money): Money { 14 | if (other.currency !== this.currency) { 15 | throw new Incalculable(); 16 | } 17 | return money(this.value + other.value, other.currency); 18 | } 19 | 20 | minus(other: Money): Money { 21 | if(this.currency !== other.currency) { 22 | throw new Incalculable(); 23 | } 24 | return money(this.value - other.value, this.currency); 25 | } 26 | 27 | static money(value: number, currency: string): Money { 28 | return new Money(value, currency); 29 | } 30 | } 31 | 32 | export function money(value: number, currency: string): Money { 33 | return Money.money(value, currency); 34 | } -------------------------------------------------------------------------------- /src/kata/golf/hole7/takehomecalculator.spec.ts: -------------------------------------------------------------------------------- 1 | import {Takehomecalculator} from "./takehomecalculator"; 2 | import {money} from "./money"; 3 | import {taxRate} from "./taxrate"; 4 | 5 | describe('TakeHomeCalculator', () => { 6 | it("can calculate tax", () => { 7 | const first: number = new Takehomecalculator(taxRate(10)).netAmount(money(40, "GBP"), money(50, "GBP"), money(60, "GBP")).value; 8 | expect(Math.trunc(first)).toBe(135) 9 | }) 10 | 11 | it("cannot sum different currencies", () => { 12 | expect(() => new Takehomecalculator(taxRate(10)).netAmount(money(4, "GBP"), money(5, "USD"))).toThrow() 13 | }) 14 | }) -------------------------------------------------------------------------------- /src/kata/golf/hole7/takehomecalculator.ts: -------------------------------------------------------------------------------- 1 | import {Money} from "./money"; 2 | import {TaxRate} from "./taxrate"; 3 | 4 | export class Takehomecalculator { 5 | private readonly taxRate: TaxRate; 6 | 7 | constructor(taxRate: TaxRate) { 8 | this.taxRate = taxRate; 9 | } 10 | 11 | netAmount(first: Money, ...rest : Money[] ): Money { 12 | let total: Money = rest.reduce((previousValue, currentValue) => previousValue.plus(currentValue), first) 13 | const tax: Money = this.taxRate.apply(total); 14 | return total.minus(tax); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /src/kata/golf/hole7/taxrate.ts: -------------------------------------------------------------------------------- 1 | import {money, Money} from "./money"; 2 | 3 | export function taxRate(percent: number): TaxRate { 4 | return TaxRate.taxRate(percent); 5 | } 6 | 7 | export class TaxRate { 8 | 9 | public percent: number; 10 | 11 | private constructor(percent: number) { 12 | this.percent = percent; 13 | } 14 | 15 | static taxRate(percent: number): TaxRate { 16 | return new TaxRate(percent); 17 | } 18 | 19 | apply(total: Money): Money { 20 | const amount: number = total.value * (this.percent / 100.0); 21 | return money(Math.trunc(amount), total.currency); 22 | } 23 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "strict": true, 6 | "declaration":true, 7 | "noImplicitAny": true, 8 | "sourceMap": true, 9 | "outDir": "./dist", 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules"] 13 | } --------------------------------------------------------------------------------