🎉 Exercism Research is now launched. Help Exercism, help science and have some fun at research.exercism.io 🎉
Avatar of Greycoat21

Greycoat21's solution

to Book Store in the Delphi Pascal Track

Published at Apr 17 2020 · 0 comments
Instructions
Test suite
Solution

To try and encourage more sales of different books from a popular 5 book series, a bookshop has decided to offer discounts on multiple book purchases.

One copy of any of the five books costs $8.

If, however, you buy two different books, you get a 5% discount on those two books.

If you buy 3 different books, you get a 10% discount.

If you buy 4 different books, you get a 20% discount.

If you buy all 5, you get a 25% discount.

Note: that if you buy 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.

Your mission is to write a piece of code to calculate the price of any conceivable shopping basket (containing only books of the same series), 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

One way of grouping these 8 books is:

  • 1 group of 5 --> 25% discount (1st,2nd,3rd,4th,5th)
  • +1 group of 3 --> 10% discount (1st,2nd,3rd)

This would give a total of:

  • 5 books at a 25% discount
  • +3 books at a 10% discount

Resulting in:

  • 5 x (8 - 2.00) == 5 x 6.00 == $30.00
  • +3 x (8 - 0.80) == 3 x 7.20 == $21.60

For a total of $51.60

However, a different way to group these 8 books is:

  • 1 group of 4 books --> 20% discount (1st,2nd,3rd,4th)
  • +1 group of 4 books --> 20% discount (1st,2nd,3rd,5th)

This would give a total of:

  • 4 books at a 20% discount
  • +4 books at a 20% discount

Resulting in:

  • 4 x (8 - 1.60) == 4 x 6.40 == $25.60
  • +4 x (8 - 1.60) == 4 x 6.40 == $25.60

For a total of $51.20

And $51.20 is the price with the biggest discount.

Testing

In order to run the tests for this track, you will need to install DUnitX. Please see the installation instructions for more information.

Loading Exercises into Delphi

If Delphi is properly installed, and *.dpr file types have been associated with Delphi, then double clicking the supplied *.dpr file will start Delphi and load the exercise/project. control + F9 is the keyboard shortcut to compile the project or pressing F9 will compile and run the project.

Alternatively you may opt to start Delphi and load your project via. the File drop down menu.

When Questions Come Up

We monitor the Pascal-Delphi support room on gitter.im to help you with any questions that might arise.

Submitting Exercises

Note that, when trying to submit an exercise, make sure the exercise file you're submitting is in the exercism/delphi/<exerciseName> directory.

For example, if you're submitting ubob.pas for the Bob exercise, the submit command would be something like exercism submit <path_to_exercism_dir>/delphi/bob/ubob.pas.

Source

Inspired by the harry potter kata from Cyber-Dojo. http://cyber-dojo.org

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you may request help from a mentor.

uBookStoreTests.pas

unit uBookStoreTests;

interface
uses
  DUnitX.TestFramework;

const
   CanonicalVersion = '1.4.0';

type
  [TestFixture]
  hpTests = class(TObject)
  public
    [Test]
//    [Ignore('Comment the "[Ignore]" statement to run the test')]
    procedure only_a_single_book;

    [Test]
    [Ignore]
    procedure two_of_the_same_book;

    [Test]
    [Ignore]
    procedure empty_basket;

    [Test]
    [Ignore]
    procedure two_different_books;

    [Test]
    [Ignore]
    procedure three_different_books;

    [Test]
    [Ignore]
    procedure four_different_books;

    [Test]
    [Ignore]
    procedure five_different_books;

    [Test]
    [Ignore]
    procedure two_groups_of_four_is_cheaper_than_group_of_five_plus_group_of_three;

    [Test]
    [Ignore]
    procedure two_groups_of_four_is_cheaper_than_groups_of_five_and_three;

    [Test]
    [Ignore]
    procedure group_of_four_plus_group_of_two_is_cheaper_than_two_groups_of_three;

    [Test]
    [Ignore]
    procedure two_each_of_first_4_books_and_1_copy_each_of_rest;

    [Test]
    [Ignore]
    procedure two_copies_of_each_book;

    [Test]
    [Ignore]
    procedure three_copies_of_first_book_and_2_each_of_remaining;

    [Test]
    [Ignore]
    procedure three_each_of_first_2_books_and_2_each_of_remaining_books;

    [Test]
    [Ignore]
    procedure four_groups_of_four_are_cheaper_than_two_groups_each_of_five_and_three;
  end;

implementation
uses System.SysUtils, uBookStore;

procedure hpTests.only_a_single_book;
var Basket: TArray<integer>;
    fBasket: IBasket;
    Expected: integer;
begin
  SetLength(Basket, 1);
  Basket[0] := 1;
  Expected := 800;

  fBasket := NewBasket(Basket);
  assert.AreEqual(Expected, fBasket.Total, format('Total should be %d cents',[Expected]));
end;

procedure hpTests.two_of_the_same_book;
var Basket: TArray<integer>;
    fBasket: IBasket;
    Expected: integer;
begin
  SetLength(Basket, 2);
  Basket[0] := 2;
  Basket[1] := 2;
  Expected := 1600;

  fBasket := NewBasket(Basket);
  assert.AreEqual(Expected, fBasket.Total, format('Total should be %d cents',[Expected]));
end;

procedure hpTests.empty_basket;
var Basket: TArray<integer>;
    fBasket: IBasket;
    Expected: integer;
begin
  SetLength(Basket, 0);
  Expected := 0;

  fBasket := NewBasket(Basket);
  assert.AreEqual(Expected, fBasket.Total, format('Total should be %d cents',[Expected]));
end;

procedure hpTests.two_different_books;
var Basket: TArray<integer>;
    fBasket: IBasket;
    Expected: integer;
begin
  SetLength(Basket, 2);
  Basket[0] := 1;
  Basket[1] := 2;
  Expected := 1520;

  fBasket := NewBasket(Basket);
  assert.AreEqual(Expected, fBasket.Total, format('Total should be %d cents',[Expected]));
end;

procedure hpTests.three_different_books;
var Basket: TArray<integer>;
    fBasket: IBasket;
    Expected: integer;
begin
  SetLength(Basket, 3);
  Basket[0] := 1;
  Basket[1] := 2;
  Basket[2] := 3;
  Expected := 2160;

  fBasket := NewBasket(Basket);
  assert.AreEqual(Expected, fBasket.Total, format('Total should be %d cents',[Expected]));
end;

procedure hpTests.four_groups_of_four_are_cheaper_than_two_groups_each_of_five_and_three;
var Basket: TArray<integer>;
    fBasket: IBasket;
    Expected: integer;
begin
  SetLength(Basket, 16);
  Basket[0] := 1;
  Basket[1] := 1;
  Basket[2] := 2;
  Basket[3] := 2;
  Basket[4] := 3;
  Basket[5] := 3;
  Basket[6] := 4;
  Basket[7] := 5;
  Basket[8] := 1;
  Basket[9] := 1;
  Basket[10] := 2;
  Basket[11] := 2;
  Basket[12] := 3;
  Basket[13] := 3;
  Basket[14] := 4;
  Basket[15] := 5;
  Expected := 10240;

  fBasket := NewBasket(Basket);
  assert.AreEqual(Expected, fBasket.Total, format('Total should be %d cents',[Expected]));
end;

procedure hpTests.group_of_four_plus_group_of_two_is_cheaper_than_two_groups_of_three;
var Basket: TArray<integer>;
    fBasket: IBasket;
    Expected: integer;
begin
  SetLength(Basket, 6);
  Basket[0] := 1;
  Basket[1] := 1;
  Basket[2] := 2;
  Basket[3] := 2;
  Basket[4] := 3;
  Basket[5] := 4;
  Expected := 4080;

  fBasket := NewBasket(Basket);
  assert.AreEqual(Expected, fBasket.Total, format('Total should be %d cents',[Expected]));
end;

procedure hpTests.four_different_books;
var Basket: TArray<integer>;
    fBasket: IBasket;
    Expected: integer;
begin
  SetLength(Basket, 4);
  Basket[0] := 1;
  Basket[1] := 2;
  Basket[2] := 3;
  Basket[3] := 4;
  Expected := 2560;

  fBasket := NewBasket(Basket);
  assert.AreEqual(Expected, fBasket.Total, format('Total should be %d cents',[Expected]));
end;

procedure hpTests.five_different_books;
var Basket: TArray<integer>;
    fBasket: IBasket;
    Expected: integer;
begin
  SetLength(Basket, 5);
  Basket[0] := 1;
  Basket[1] := 2;
  Basket[2] := 3;
  Basket[3] := 4;
  Basket[4] := 5;
  Expected := 3000;

  fBasket := NewBasket(Basket);
  assert.AreEqual(Expected, fBasket.Total, format('Total should be %d cents',[Expected]));
end;

procedure hpTests.two_groups_of_four_is_cheaper_than_group_of_five_plus_group_of_three;
var Basket: TArray<integer>;
    fBasket: IBasket;
    Expected: integer;
begin
  SetLength(Basket, 8);
  Basket[0] := 1;
  Basket[1] := 1;
  Basket[2] := 2;
  Basket[3] := 2;
  Basket[4] := 3;
  Basket[5] := 3;
  Basket[6] := 4;
  Basket[7] := 5;
  Expected := 5120;

  fBasket := NewBasket(Basket);
  assert.AreEqual(Expected, fBasket.Total, format('Total should be %d cents',[Expected]));
end;

procedure hpTests.two_groups_of_four_is_cheaper_than_groups_of_five_and_three;
var Basket: TArray<integer>;
    fBasket: IBasket;
    Expected: integer;
begin
  SetLength(Basket, 8);
  Basket[0] := 1;
  Basket[1] := 1;
  Basket[2] := 2;
  Basket[3] := 3;
  Basket[4] := 4;
  Basket[5] := 4;
  Basket[6] := 5;
  Basket[7] := 5;
  Expected := 5120;

  fBasket := NewBasket(Basket);
  assert.AreEqual(Expected, fBasket.Total, format('Total should be %d cents',[Expected]));
end;

procedure hpTests.two_each_of_first_4_books_and_1_copy_each_of_rest;
var Basket: TArray<integer>;
    fBasket: IBasket;
    Expected: integer;
begin
  SetLength(Basket, 9);
  Basket[0] := 1;
  Basket[1] := 1;
  Basket[2] := 2;
  Basket[3] := 2;
  Basket[4] := 3;
  Basket[5] := 3;
  Basket[6] := 4;
  Basket[7] := 4;
  Basket[8] := 5;
  Expected := 5560;

  fBasket := NewBasket(Basket);
  assert.AreEqual(Expected, fBasket.Total, format('Total should be %d cents',[Expected]));
end;

procedure hpTests.two_copies_of_each_book;
var Basket: TArray<integer>;
    fBasket: IBasket;
    Expected: integer;
begin
  SetLength(Basket, 10);
  Basket[0] := 1;
  Basket[1] := 1;
  Basket[2] := 2;
  Basket[3] := 2;
  Basket[4] := 3;
  Basket[5] := 3;
  Basket[6] := 4;
  Basket[7] := 4;
  Basket[8] := 5;
  Basket[9] := 5;
  Expected := 6000;

  fBasket := NewBasket(Basket);
  assert.AreEqual(Expected, fBasket.Total, format('Total should be %d cents',[Expected]));
end;

procedure hpTests.three_copies_of_first_book_and_2_each_of_remaining;
var Basket: TArray<integer>;
    fBasket: IBasket;
    Expected: integer;
begin
  SetLength(Basket, 11);
  Basket[0] := 1;
  Basket[1] := 1;
  Basket[2] := 2;
  Basket[3] := 2;
  Basket[4] := 3;
  Basket[5] := 3;
  Basket[6] := 4;
  Basket[7] := 4;
  Basket[8] := 5;
  Basket[9] := 5;
  Basket[10] := 1;
  Expected := 6800;

  fBasket := NewBasket(Basket);
  assert.AreEqual(Expected, fBasket.Total, format('Total should be %d cents',[Expected]));
end;

procedure hpTests.three_each_of_first_2_books_and_2_each_of_remaining_books;
var Basket: TArray<integer>;
    fBasket: IBasket;
    Expected: integer;
begin
  SetLength(Basket, 12);
  Basket[0] := 1;
  Basket[1] := 1;
  Basket[2] := 2;
  Basket[3] := 2;
  Basket[4] := 3;
  Basket[5] := 3;
  Basket[6] := 4;
  Basket[7] := 4;
  Basket[8] := 5;
  Basket[9] := 5;
  Basket[10] := 1;
  Basket[11] := 2;
  Expected := 7520;

  fBasket := NewBasket(Basket);
  assert.AreEqual(Expected, fBasket.Total, format('Total should be %d cents',[Expected]));
end;

initialization
  TDUnitX.RegisterTestFixture(hpTests);
end.
unit uBookStore;

interface

type
  IBasket = interface
    function Total: Integer;
  end;

  TIntArray = TArray<Integer>;

function NewBasket(Basket: TIntArray): IBasket;

implementation

type
  TBook = (firstTitle=1, secondTitle=2, thirdTitle=3, fourthTitle=4, fifthTitle=5);
  // Indicates whether a given book is included in a grouping.
  TBookGroup = array [TBook] of Boolean;
  TBookGroups = TArray<TBookGroup>;
  TBasket = class(TInterfacedObject, IBasket)
  private const
    DiscountedPrice: array [1..5] of Integer = (
      800, // Regular Price
      760, // 5% off
      720, // 10% off
      640, // 20% off
      600);// 25% off
  private var
    BookGroups: TBookGroups;
    procedure ApplyKnownCheaperBasketTransforms;
  public
    function Total: Integer;
    constructor Create(Basket: TIntArray);
  end;

function NewBasket(Basket: TIntArray): IBasket;
begin
  Result := TBasket.Create(Basket);
end;

function EmptyBookGroup: TBookGroup;
begin
  for var Book := Low(TBook) to High(TBook) do
    Result[Book] := False;
end;

{ TBasket }

function Count(BookGroup: TBookGroup): Integer;
begin
  Result := 0;
  for var Book := Low(TBook) to High(TBook) do
    if BookGroup[Book] then
      Inc(Result);
end;

procedure TBasket.ApplyKnownCheaperBasketTransforms;
begin
  // Case1: 3/5 -> 4/4
  // Case2: 3/3 -> 4/2; This case is covered by the implementation that
    // populates BookGroups. It is impossible to have two groups of N books that
    // don't have the maximum possible number of books in common. Therefore, it
    // is impossible to move a book from one group of N books to another.

  { If there is a group of five, and a group of three,
    and one of the items from the group of five can go in the group of three,
    then the net cost will decrease. }

  for var i := Low(BookGroups) to High(BookGroups) do begin
    if Count(BookGroups[i]) = 5 then begin
      for var j := Low(BookGroups) to High(BookGroups) do begin
        if Count(BookGroups[j]) = 3 then begin
          for var Book := Low(TBook) to High(TBook) do begin
            if BookGroups[i][Book] and not BookGroups[j][Book] then begin
              BookGroups[i][Book] := False;
              BookGroups[j][Book] := True;
              // Stop looping after one book is moved to avoid moving two books
              // from the 5 group to the 3 group which would leave the groups
              // in basically the same condition as before.
              Break;
            end;
          end;
        end;
      end;
    end;
  end;
end;

constructor TBasket.Create(Basket: TIntArray);
var
  BasketItem, BookNumber: Integer;
begin
  { Note that this implementation uses a 'waterfall': for each book in the
    basket, it tries to add the book to the first book group, if it is already
    there, then it moves on to the next book group (creating new groups as
    necessary) and tries to add the book there, etc.

    This implementation ensures that if there are multiple groups of N books,
    that each group contains all of the same books. This is important for
    determining the minimum basket cost.
    (see notes in ApplyKnownCheaperBasketTransforms routine) }

  BookGroups := BookGroups + [EmptyBookGroup];
  for BasketItem := Low(Basket) to High(Basket) do begin
    BookNumber := Basket[BasketItem];
    var Book := TBook(BookNumber);
    var BookAdded := False;
    for var i := Low(BookGroups) to High(BookGroups) do begin
      if not BookGroups[i][Book] then begin
        BookGroups[i][Book] := True;
        BookAdded := True;
        // Stop looping to avoid adding the same basket item to multiple groups.
        Break;
      end;
    end;

    if not BookAdded then begin
      var NewBookGroup := EmptyBookGroup;
      NewBookGroup[Book] := True;
      BookGroups := BookGroups + [NewBookGroup];
    end;
  end;

  ApplyKnownCheaperBasketTransforms;
end;

function TBasket.Total: Integer;
var
  Book: TBook;
begin
  Result := 0;
  for var BookGroup in BookGroups do begin
    var BookCount := Count(BookGroup);
    Result := Result + BookCount * DiscountedPrice[BookCount];
  end;
end;

end.

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?