2

I have been working with TStack to try and implement a simple Undo/Redo feature in my program. The thought behind this is that as an action is performed the current state of the program is saved - i.e. pushed to the stack. When the user clicks undo the last state of the program is reloaded - i.e. popped from the stack.

The flaw in this idea is that the stack cannot go on growing forever meaning that after a capacity value is reached, the oldest items (those at the bottom of the stack) should be removed as new items are pushed on top.

The TStack object in Delphi contains a Capacity property which I assumed would automatically perform this 'clean-up' but when I overload the stack (e.g. push 11 items to one with capcity 10) the capacity updates to accomodate for more items.

Can anyone offer me any advice on how to use TStack more effectively in this instance? I understand that an alternative would be to use an array structure but I like the prospective ease of using stacks.

Regards

Toby Dobbs
  • 31
  • 3
  • Perhaps easier to use a queue and an indicator where the last added item in the queue is. That way you'll have redo option to. – Lieven Keersmaekers Sep 07 '16 at 10:30
  • Stack is not going to work for you. Give it up. It's the wrong data structure. You are looking for a deque perhaps. Or just create your own using a fixed length array. Use circular indexing when the array gets full. – David Heffernan Sep 07 '16 at 10:31
  • Sorry to say, but the `Capacity` property is, as you found out, automatically increased. his is the same with all `TOrderedList` derived collection objects. There is also no setting to force it not to grow. You may be able to use the `OnNotify` event to check if your own limit is reached, and if so change the content as needed. – Tom Brunberg Sep 07 '16 at 10:33
  • @TomBrunberg A stack doesn't lend itself to removing from the bottom, so how would you go about enforcing a limit? – David Heffernan Sep 07 '16 at 10:39
  • @DavidHeffernan You are right. Better build it from scratch on basis of an array as you suggested. – Tom Brunberg Sep 07 '16 at 12:09
  • Consider using `TList` instead of `TStack`. `TList` will let you add and remove items from anywhere in the list. – Remy Lebeau Sep 07 '16 at 16:07
  • @Remy going to be inefficient though to be constantly moving the entries array. Circular buffer better. – David Heffernan Sep 07 '16 at 19:31
  • Thanks for the suggestions. – Toby Dobbs Sep 08 '16 at 12:32

1 Answers1

3

In reality the TStack is powered by a dynamic array.
If you really wanted to you could abuse this fact to have the stack remove items from the bottom, but now you'd be fighting the stack.

I think it's a better idea just to use a circular list.

A simple design might work like this.

type
  TCircularList<T> = class(TObject);
  private
    FStorage: TList<T>;
    FCapacity: cardinal;  //duplication for performance reasons
    FCurrentIndex: cardinal;
  protected
    function GetItem(index: cardinal): T;
    procedure SetItem(index: cardinal; const Value: T);
  public
    constructor Create(Size: cardinal = 10);
    destructor Destroy; override;
    procedure Add(const Item: T);
    property Items[index: cardinal]: T read GetItem write SetItem; default;
  end;

constructor TCircularList<T>.Create(Size: cardinal = 10);
begin
  inherited Create;
  Assert(Size >= 2);
  FStorage:= TList<T>.Create;
  FCapacity:= Size;
  FStorage.Capacity:= Size;
end;

destructor TCircularList<T>.Destroy;
begin
  FStorage.Free;
  inherited;
end;

procedure TCircularList<T>.Add(const Item: T);
begin
  FCurrentIndex:= (FCurrentIndex + 1) mod FCapacity;
  FStorage[FCurrentIndex]:= Item;
end;

function TCircularList<T>.GetItem(index: cardinal): T;
var
  cIndex: cardinal;
begin
  cIndex:= Index mod FCapacity;
  Result:= FStorage[index]; 
end;

procedure TCircularList<T>.SetItem(index: cardinal; const Value: T);
var
  cIndex: cardinal;
begin
  cIndex:= index mod FCapacity;
  FStorage[index]:= Value;
end;

Obviously you'd need a few more methods like a last and delete method, but I leave that up to you, you should be able to extrapolate from here.

Usability remarks
I have to say that from a UX perspective I think the idea of an undo/redo function sucks.
Why not have a list of snapshots, you that you can go backwards and forwards in time, much like the situation would be if you saved a number of backup files on disk.
The undo function requires you to memorize exactly what you've done the last x steps which does not scale very well.
I also don't understand why there must be a limit, why not allow as much undo/snapshots as memory/disk space permits?

Johan
  • 74,508
  • 24
  • 191
  • 319