ðŸŽ‰ Exercism Research is now launched. Help Exercism, help science and have some fun at research.exercism.io ðŸŽ‰

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.

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

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.

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

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`

.

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

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

```
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.
```

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?

Level up your programming skills with 3,450 exercises across 52 languages, and insightful discussion with our volunteer team of welcoming mentors.
Exercism is
**100% free forever**.

## Community comments