0%

[TDD Kata] Potter

repo: https://github.com/kenchen879/kata-potter

需求設計

曾幾何時,有一個 5 本書系列,講述了一位名叫哈利的非常英國的英雄。 (至少在這個 Kata 被發明的時候,只有 5 個。從那以後他們成倍增加)全世界的孩子都認為他很棒,當然,出版商也一樣。因此,為了對人類慷慨解囊,(並為了增加銷售額),他們建立了以下定價模型,以利用哈利吸引人的優點。

這五本書中的任何一本都需要 8 歐元。但是,如果您從該系列中購買了兩本不同的書,則可以在這兩本書上獲得 5% 的折扣。
如果您購買 3 種不同的書籍,您將獲得 10% 的折扣。購買 4 種不同的書籍,您將獲得 20% 的折扣。如果您全部購買 5 個,您將獲得 25% 的巨大折扣。

請注意,例如,如果您購買了四本書,其中 3 本書是不同的書名,那麼您可以在其中的 3 本書上獲得 10% 的折扣,但第四本書的價格仍然是 8 歐元。

波特狂熱正在席捲全國,各地青少年的父母都在排隊,購物籃裡裝滿了波特的書籍。你的任務是編寫一段代碼來計算任何可以想像到的購物籃的價格,並提供盡可能大的折扣。

舉例來說,這個購物籃會花費多少錢?

  • 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
1
2
3
4
(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

開始 Red–Green–Refactor cycle

  1. 第一個紅燈 (Red)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // potter.spec.ts
    test('testBasics: buy one book', () => {
    const potter = new Potter();
    potter.addToBasket([]);
    expect(potter.price).toBe(0);
    potter.addToBasket([1]);
    expect(potter.price).toBe(8);
    potter.addToBasket([2]);
    expect(potter.price).toBe(8);
    potter.addToBasket([3]);
    expect(potter.price).toBe(8);
    potter.addToBasket([4]);
    expect(potter.price).toBe(8);
    potter.addToBasket([1, 1, 1]);
    expect(potter.price).toBe(8 * 3);
    });
    1
    2
    3
    4
    5
    6
    7
    // potter.ts
    export class Potter {
    addToBasket(pins: number) {}
    get price() {
    return -1;
    }
    }
  2. 第一個綠燈 (Green)

    將 score method 回傳值改為 0,形成第一個綠燈。

    1
    2
    3
    4
    5
    6
    7
    // potter.ts
    export class Potter {
    addToBasket(pins: number) {}
    get price() {
    return 0;
    }
    }
  3. 第一次重構

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // potter.spec.ts
    import { Potter } from './potter';

    describe('Kata - Harry Potter Book', () => {
    let potter: Potter;

    beforeEach(() => {
    potter = new Potter();
    });
    it('should create an instance', () => {
    expect(potter).toBeTruthy();
    });

    test('testBasics: buy one book', () => {
    potter.addToBasket([]);
    expect(potter.price).toBe(0);
    potter.addToBasket([1]);
    expect(potter.price).toBe(8);
    potter.addToBasket([2]);
    expect(potter.price).toBe(8);
    potter.addToBasket([3]);
    expect(potter.price).toBe(8);
    potter.addToBasket([4]);
    expect(potter.price).toBe(8);
    potter.addToBasket([1, 1, 1]);
    expect(potter.price).toBe(8 * 3);
    });
    });
  4. 第二個紅燈

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // potter.spec.ts
    test('testBasics: buy one book', () => {
    potter.addToBasket([0, 1]);
    expect(potter.price).toBe(8 * 2 * 0.95);
    potter.addToBasket([0, 2, 4]);
    expect(potter.price).toBe(8 * 3 * 0.9);
    potter.addToBasket([0, 1, 2, 4]);
    expect(potter.price).toBe(8 * 4 * 0.8);
    potter.addToBasket([0, 1, 2, 3, 4]);
    expect(potter.price).toBe(8 * 5 * 0.75);
    });
  5. 第二個綠燈

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    export class Potter {
    private _basket: number[] = [];
    addToBasket(book: number[]) {
    this._basket = [];
    for (let i = 0; i < book.length; i++)
    this._basket.push(book[i]);
    }
    get price() {
    let price = 0;
    let totalBookNumber = 0;
    let distunctBookNumber = 0;

    totalBookNumber = this._basket.length;
    const distinctBasket = this._basket.filter((ele , pos) => {
    return this._basket.indexOf(ele) == pos;
    });
    distunctBookNumber = distinctBasket.length;

    if (distunctBookNumber == 1) {
    price = 8 * totalBookNumber;
    } else {
    switch (totalBookNumber) {
    case 2:
    price = 8 * 2 * 0.95;
    break;
    case 3:
    price = 8 * 3 * 0.9;
    break;
    case 4:
    price = 8 * 4 * 0.8;
    break;
    case 5:
    price = 8 * 5 * 0.75;
    break;
    default:
    break;
    }
    }
    return price;
    }
    }
  6. 第二次重構

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    // potter.ts
    export class Potter {
    private _basket: number[] = [];
    addToBasket(book: number[]) {
    this._basket = [];
    for (let i = 0; i < book.length; i++)
    this._basket.push(book[i]);
    }
    get price() {
    let price = 8;
    let discount = [1, 1, 0.95, 0.9, 0.8, 0.75];
    let totalBookNumber = 0;
    let distinctBookNumber = 0;

    totalBookNumber = this._basket.length;

    // 取得不重複的購物籃
    const distinctBasket = this.distinctBasket;
    // 取得不重複購物籃的數量
    distinctBookNumber = distinctBasket.length;

    if (distinctBookNumber == 1) {
    price *= totalBookNumber;
    } else {
    switch (totalBookNumber) {
    case 2:
    price *= 2 * discount[2];
    break;
    case 3:
    price *= 3 * discount[3];
    break;
    case 4:
    price *= 4 * discount[4];
    break;
    case 5:
    price *= 5 * discount[5];
    break;
    default:
    price = 0;
    }
    }
    return price;
    }

    get distinctBasket () {
    const distinctBasket = [...(new Set(this._basket))];
    return distinctBasket;
    }
    }
  7. 第三個紅燈

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // potter.spec.ts
    test('testSeveralDiscounts', () => {
    potter.addToBasket([0, 0, 1]);
    expect(potter.price).toBe(8 + (8 * 2 * 0.95));
    potter.addToBasket([0, 0, 1, 1]);
    expect(potter.price).toBe(2 * (8 * 2 * 0.95));
    potter.addToBasket([0, 0, 1, 2, 2, 3]);
    expect(potter.price).toBe((8 * 4 * 0.8) + (8 * 2 * 0.95));
    potter.addToBasket([0, 1, 1, 2, 3, 4]);
    expect(potter.price).toBe(8 + (8 * 5 * 0.75));
    });
  8. 第三個綠燈

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    // potter.ts
    export class Potter {
    private _basket: number[] = [];
    addToBasket(book: number[]) {
    this._basket = [];
    for (let i = 0; i < book.length; i++)
    this._basket.push(book[i]);
    }
    get price() {
    let price = 0;
    const discount = [1, 1, 0.95, 0.9, 0.8, 0.75];
    let totalBookNumber = 0;
    // 取得不重複的購物籃
    let distinctBasket = [];
    let distinctBookNumber = 0;
    let index = [];

    totalBookNumber = this._basket.length;

    while (totalBookNumber > 0) {
    // 取得不重複的購物籃
    distinctBasket = this.distinctBasket;
    // 取得不重複購物籃的數量
    distinctBookNumber = distinctBasket.length;
    switch (distinctBookNumber) {
    case 1:
    price += 8 * distinctBookNumber;
    break;
    case 2:
    price += 8 * 2 * discount[2];
    break;
    case 3:
    price += 8 * 3 * discount[3];
    break;
    case 4:
    price += 8 * 4 * discount[4];
    break;
    case 5:
    price += 8 * 5 * discount[5];
    break;
    default:
    price = 0;
    }
    index = distinctBasket.filter(e => this._basket.indexOf(e));
    index.forEach(e => this._basket.splice(e, distinctBookNumber));
    totalBookNumber -= distinctBookNumber;
    }
    return price;
    }

    get distinctBasket () {
    const distinctBasket = [...(new Set(this._basket))];
    return distinctBasket;
    }
    }
  9. 第三次重構

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    // potter.spec.ts
    export class Potter {
    private _basket: number[] = [];
    addToBasket(book: number[]) {
    this._basket = [];
    book.forEach(e => this._basket.push(e));
    }
    get price() {
    let price = 0;
    const discount = [1, 1, 0.95, 0.9, 0.8, 0.75];
    let totalBookNumber = 0;
    // 取得不重複的購物籃
    let distinctBasket = [];
    let distinctBookNumber = 0;
    let index = [];

    totalBookNumber = this._basket.length;

    while (totalBookNumber > 0) {
    // 取得不重複的購物籃
    distinctBasket = this.distinctBasket;
    // 取得不重複購物籃的數量
    distinctBookNumber = distinctBasket.length;
    switch (distinctBookNumber) {
    case 1:
    price += 8 * distinctBookNumber;
    break;
    case 2:
    price += 8 * 2 * discount[2];
    break;
    case 3:
    price += 8 * 3 * discount[3];
    break;
    case 4:
    price += 8 * 4 * discount[4];
    break;
    case 5:
    price += 8 * 5 * discount[5];
    break;
    default:
    price = 0;
    }
    index = distinctBasket.filter(e => this._basket.indexOf(e));
    index.forEach(e => this._basket.splice(e, distinctBookNumber));
    totalBookNumber -= distinctBookNumber;
    }
    return price;
    }

    get distinctBasket () {
    const distinctBasket = [...(new Set(this._basket))];
    return distinctBasket;
    }
    }
  10. 第四個紅燈

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // potter.spec.ts
    test('testEdgeCases', () => {
    potter.addToBasket([0, 0, 1, 1, 2, 2, 3, 4]);
    expect(potter.price).toBe(2 * (8 * 4 * 0.8));
    potter.addToBasket([0, 0, 0, 0, 0,
    1, 1, 1, 1, 1,
    2, 2, 2, 2,
    3, 3, 3, 3, 3,
    4, 4, 4, 4]);
    expect(potter.price).toBe(3 * (8 * 5 * 0.75) + 2 * (8 * 4 * 0.8));
    });
  11. 第四個綠燈以及第四次重構

    要取得價格最佳解。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    // potter.ts
    export class Potter {
    private _basket: number[] = [];
    private totalBookNumber = 0;

    constructor () {
    this._basket = [];
    this.totalBookNumber = 0;
    }

    addToBasket(book: number[]) {
    book.forEach(e => this._basket.push(e));
    this.totalBookNumber = book.length;
    }

    get price() {
    let price = 0;
    const discount = [1, 1, 0.95, 0.9, 0.8, 0.75];
    // 取得不重複的購物籃
    let distinctBasket:number[] = [];
    // 取得不重複購物籃的數量
    let distinctBookNumber = 0;

    while (this.totalBookNumber > 0) {
    if (this.checkOptimalPrice()) {
    price += 2 * 8 * 4 * discount[4];
    } else {
    distinctBasket = this.createDistinctBasket();
    distinctBookNumber = distinctBasket.length;
    price += 8 * distinctBookNumber * discount[distinctBookNumber];
    this.removeBook(distinctBasket, distinctBookNumber);
    }
    }
    return price;
    }

    checkOptimalPrice () {
    if (this.totalBookNumber < 8) return false;
    let distinctBasket1: number[] = [];
    let distinctBasket2: number[] = [];

    distinctBasket1 = this.createDistinctBasket();
    this.removeBook(distinctBasket1, distinctBasket1.length);
    distinctBasket2 = this.createDistinctBasket();
    this.removeBook(distinctBasket2, distinctBasket2.length);

    if (distinctBasket1.length == 5 && distinctBasket2.length == 3) {
    return true;
    } else {
    distinctBasket1.forEach(e => this._basket.push(e));
    this.totalBookNumber += distinctBasket1.length;
    distinctBasket2.forEach(e => this._basket.push(e));
    this.totalBookNumber += distinctBasket2.length;
    return false;
    }
    }

    removeBook (distinct: number[], num: number) {
    for (let i = 0; i < num; i++) {
    this._basket.splice(this._basket.indexOf(distinct[i]), 1);
    }
    this.totalBookNumber -= num;
    }

    createDistinctBasket () {
    // 取得不重複的購物籃
    const distinctBook = [...(new Set(this._basket))];
    // 取得不重複購物籃的數量
    return distinctBook;
    }
    }