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

eferim's solution

to Book Store in the Delphi Pascal Track

Published at Sep 12 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 may receive assistance from a mentor.

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
    function Total : integer;
  end;

  function NewBasket(Basket: TArray<integer>) : IBasket;

implementation

uses System.Generics.Collections;

type
  TBasket = class(TInterfacedObject, IBasket)
  private
    FColRangs : TList<integer>;
    FDiscounts : TDictionary<integer, single>;
    function CombValue(ACom : TList<integer>) : integer;
  public
    constructor Create(ABasket: TArray<integer>);
    destructor Destroy; override;
    function Total : integer;
  end;

function NewBasket(Basket: TArray<integer>) : IBasket;
begin
  Result := TBasket.Create(Basket);
end;

{ TBasket }

constructor TBasket.Create(ABasket: TArray<integer>);
var
  FQtys : TDictionary<integer, integer>;
  BSets : TList<integer>;

  function FindQty(ABasket: TArray<integer>) : TDictionary<integer, integer>;
  var B : integer;
  begin
    Result :=  TDictionary<integer, integer>.Create;
    for B in ABasket do
    if not Result.ContainsKey(B) then
      Result.Add(B, 1)
    else
      Result.Items[B] := Result.Items[B] + 1;
  end;

  function QtyToSets(AQtys : TDictionary<integer, integer>) : TList<integer>;
  var BSet: TPair<integer, integer>;
  begin
    Result := TList<integer>.Create;
    for BSet in AQtys do
      Result.Add(BSet.Value);
    Result.Sort;
  end;

  function RangOfSets(ASets : TList<integer>) : TList<integer>;
  var i, R, Tmp: Integer;
  begin
    Result := TList<integer>.Create;
    if ASets.Count > 0 then
      for i := 0 to ASets.Last - 1 do
      begin
        Tmp := 0;
        for R in ASets do
          if R - i > 0 then
            inc(Tmp);
        Result.Add(Tmp);
      end;
  end;

begin
  // Discounts
  FDiscounts := TDictionary<integer, single>.Create;
  FDiscounts.Add(1, 1);
  FDiscounts.Add(2, 0.95);
  FDiscounts.Add(3, 0.9);
  FDiscounts.Add(4, 0.8);
  FDiscounts.Add(5, 0.75);

  // Find qty of each book
  FQtys :=  FindQty(ABasket);

  // Group to sets
  BSets := QtyToSets(FQtys);
  FQtys.DisposeOf;

  // Make rang of sets
  FColRangs := RangOfSets(BSets);
  BSets.DisposeOf;
end;

destructor TBasket.Destroy;
begin
  FDiscounts.DisposeOf;
  FColRangs.DisposeOf;
  inherited;
end;

function TBasket.Total : integer;
var LVal : integer;

  procedure Recombine;
  begin
    if FColRangs.First - FColRangs.Last > 1 then
    begin
      FColRangs[0] := FColRangs.First - 1;
      FColRangs[FColRangs.Count - 1] := FColRangs.Last + 1;
      FColRangs.Sort;
      FColRangs.Reverse;
    end;
  end;

begin
  if FColRangs.Count = 0 then
    exit(0);
  Result := CombValue(FColRangs);

  while FColRangs.First - FColRangs.Last > 1 do
  begin
    Recombine;
    LVal := CombValue(FColRangs);
    if LVal < Result then
      Result := LVal;
  end;

end;

function TBasket.CombValue(ACom : TList<integer>) : integer;
var
  S: integer;
begin
  Result := 0;
  for S in Acom do
    Result := Result + round(S * 800 * FDiscounts[S]);
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?