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

rpottsoh's solution

to Book Store in the Delphi Pascal Track

Published at Sep 17 2018 · 0 comments
Instructions
Test suite
Solution

Note:

This exercise has changed since this solution was written.

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 can see how others have completed the exercise.

uBookStoreTests.pas

unit uBookStoreTests;

interface
uses
  DUnitX.TestFramework;

const
   CanonicalVersion = '1.3.0';

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

    [Test]
    [Ignore]
    procedure A_basket_containing_only_two_of_the_same_book;

    [Test]
    [Ignore]
    procedure No_charge_to_carry_around_an_empty_basket;

    [Test]
    [Ignore]
    procedure A_basket_containing_only_two_different_books;

    [Test]
    [Ignore]
    procedure A_basket_with_three_different_books;

    [Test]
    [Ignore]
    procedure A_basket_with_four_different_books;

    [Test]
    [Ignore]
    procedure A_basket_with_five_different_books;

    [Test]
    [Ignore]
    procedure A_basket_containing_eight_books_consisting_of_a_pair_each_of_the_first_three_books_plus_one_copy_each_of_the_last_two_books;

    [Test]
    [Ignore]
    procedure A_basket_containing_nine_books_consisting_of_a_pair_each_of_the_first_four_books_plus_one_of_the_last_book;

    [Test]
    [Ignore]
    procedure A_basket_containing_ten_books_consisting_of_two_copies_of_each_book_in_the_series;

    [Test]
    [Ignore]
    procedure A_basket_containing_eleven_books_consisting_of_three_copies_of_the_first_book_plus_two_each_of_the_remaing_four_bookes_in_the_series;

    [Test]
    [Ignore]
    procedure A_basket_containing_twelve_books_consisting_of_three_copies_of_the_first_two_books_plus_two_each_of_the_remaining_three_books_in_the_series;

    [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.A_basket_containing_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.A_basket_containing_only_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.No_charge_to_carry_around_an_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.A_basket_containing_only_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.A_basket_with_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.A_basket_with_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.A_basket_with_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.A_basket_containing_eight_books_consisting_of_a_pair_each_of_the_first_three_books_plus_one_copy_each_of_the_last_two_books;
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.A_basket_containing_nine_books_consisting_of_a_pair_each_of_the_first_four_books_plus_one_of_the_last_book;
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.A_basket_containing_ten_books_consisting_of_two_copies_of_each_book_in_the_series;
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.A_basket_containing_eleven_books_consisting_of_three_copies_of_the_first_book_plus_two_each_of_the_remaing_four_bookes_in_the_series;
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.A_basket_containing_twelve_books_consisting_of_three_copies_of_the_first_two_books_plus_two_each_of_the_remaining_three_books_in_the_series;
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(IInvokable)
  ['{22B4BAF3-88E6-456D-9DE5-F6BAC743A655}']
    function Total:extended;
  end;

function NewBasket(aBasket: TArray<Integer>): IBasket;

implementation
uses System.SysUtils, System.Generics.collections, System.Math;

const
  seriesBooks = '12345';
  cNumberOfBooks = 5;
  cDiscounts: array[1..cNumberOfBooks] of extended = (1.00,
                                                      0.95,
                                                      0.90,
                                                      0.80,
                                                      0.75);

type
  TBasket = class(TInterfacedObject, IBasket)
  private
    fSingleBookPrice: extended;
    fBasket: string;
    fIntList: TList<integer>;
    class function Head(inStr: string): string; static;
    class function Tail(inStr: string): string; static;
    class function ConvertIntArrayToString(const aIntArray: TArray<Integer>): string; static;
    function DiscountPercentage(inStr : string): extended;
    function GroupBasket:TArray<String>;
    function NumberOfDifferentBooks(inStr : string):integer;
  public
    function Total:extended;
    constructor Create(aBasket: TArray<Integer>);
  end;

function NewBasket(aBasket: TArray<Integer>): IBasket;
begin
  result := TBasket.Create(aBasket);
end;

class function TBasket.Head(inStr : string):string;
begin
  result := inStr.Remove(1);
end;

class function TBasket.Tail(inStr : string):string;
begin
  result := inStr.Remove(0,1);
end;

class function TBasket.ConvertIntArrayToString(const aIntArray: TArray<Integer>): string;
var arrayItem: integer;
begin
  result := '';
  if length(aIntArray) > 0 then
    for arrayItem in aIntArray do
      result := result + arrayItem.ToString;
end;

constructor TBasket.Create(aBasket: TArray<Integer>);
begin
  fSingleBookPrice := 8;
  fIntList := TList<integer>.Create;
  fIntList.AddRange(aBasket);
  fIntList.Sort;
  fBasket := ConvertIntArrayToString(aBasket);
end;

function TBasket.GroupBasket:TArray<String>;
var lStrArray: TArray<String>;
    wrkBasket: string;
    tmpStr   : string;
    thisBook : string;
    Index    : integer;
    StrCount : integer;
begin
  wrkBasket := fBasket;
  StrCount := 1;
  SetLength(lStrArray,StrCount);
  thisBook := Head(wrkBasket);
  while wrkBasket.Length > 0 do
  begin
    Index := 0;
    repeat
      tmpStr := lStrArray[Index];
      if thisBook.Length > 0 then
      begin
        if not tmpStr.Contains(thisBook) then
        begin
          tmpStr := tmpStr + thisBook;
          lStrArray[Index] := tmpStr;
          wrkBasket := Tail(wrkBasket);
          thisBook := Head(wrkBasket);
        end
        else
        if (Index = StrCount - 1) then
        begin
          inc(StrCount);
          SetLength(lStrArray,StrCount);
        end;
        inc(Index);
      end;
    until (Index = StrCount) or wrkBasket.IsEmpty;
  end;
  result := lStrArray;
end;

function TBasket.Total:extended;
var
    subBaskets    : TArray<String>;
    subResult     : array[0..1] of extended;
    lSortedBasket : TArray<integer>;

    function computeTotal: extended;
    var wrkSubBasket: string;
        totalBooks  : integer;
        subTotal    : extended;
    begin
      result := 0;
      for wrkSubBasket in subBaskets do
      begin
        totalBooks := wrkSubBasket.Length;
        subTotal := totalBooks * (fSingleBookPrice * DiscountPercentage(wrkSubBasket));
        Result := Result + subTotal;
      end;
    end;

begin
  fillchar(subResult, sizeof(extended), #0);

  subBaskets := GroupBasket;
  subResult[0] := computeTotal;

  lSortedBasket := fIntList.ToArray;
  fBasket := ConvertIntArrayToString(lSortedBasket);
  subBaskets := GroupBasket;
  subResult[1] := computeTotal;

  result := min(subResult[0], subResult[1]);
end;

function TBasket.DiscountPercentage(inStr : string):extended;
var numDiffBooks: integer;
begin
  numDiffBooks := NumberOfDifferentBooks(inStr);
  result := CDiscounts[numDiffBooks];
end;

function TBasket.NumberOfDifferentBooks(inStr : string):integer;
var Book: char;
begin
  result := 0;
  for Book in seriesBooks do
    if inStr.Contains(Book) then
      inc(result);
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?