[Back to OOP SWAG index]  [Back to Main SWAG index]  [Original]

unit TSortLst;

(*
    Version 1.0 5/12/95 - Mike Stortz

      Borland Pascal 7.0 has a very useful object called a TCollection that
    allows you to sort items by any key you wish and that disposes
    of any objects it owns when the collection itself is freed.
    Inexplicably, these very useful behaviors were omitted from Delphi's
    TStringList.  But now... <trumpet fanfare>

      This unit contains a class called "TSortableList" derived from
    TStringList that supports the ability for you to define descendent classes
    that sort the list any way you wish and has the option to "own" the objects
    that are added to it, so that they are disposed with the list.

    How to use a TSortableList that owns its objects:

    1. Create a new TSortableList and tell it that it should own any objects
       in it.

      var
        my_list : TSortableList;
      begin
      my_list := TSortableList.Create(True);     { "True" = Owns objects }
      my_list.Add('Aaa', TObject.Create);        { In a TStringList, this... }
      my_list.Add('Bbb', TObject.Create);        { ...would be... }
      my_list.Add('Ccc', TObject.Create);        { ...very bad! }
      my_list.Free;                              { All objects freed }
      end;

    How to sort on an arbitrary key:

    Suppose you wanted a list of strings sorted by all but the first character
    (i.e. this order -> "ZAble", "YBaker", "XCharlie").

    1. Declare a descendent of TSortableList and override the Compare method.
       The Compare method should return an integer such that the result is:
         -1 if the item at index i1 is "less" than the item at i2
          0 if the item at index i1 is "equal" than the item at i2
          1 if the item at index i1 is "more" than the item at i2

      TExList = class(TSortableList)
        function Compare(i1, i2 : Integer) : Integer; override;
        end;

    2. Define the new compare method

      function Compare(i1, i2 : Integer) : Integer;
        begin
        case Key of
          1 :
            Result := AnsiCompareText(Copy(Strings[i1], 2, 254),
                                      Copy(Strings[i2], 2, 254));
          else
            Result := inherited Compare(i1, i2);
          end;
        end;

    3. Specify the key you just defined.

      var
        my_list : TExList;
      begin
      my_list := TExList.Create;
      my_list.Key := 1;               { <<<<< New key is made active }
      DoSomeStuff;
      my_list.Free;
      end;

      There you go!  I =strongly= suggest that any Key that you define Compare
    methods for have a value of at least 1 and that all unhandled Key
    values be passed to the inherited method, as above.  A Key of 0 is
    defined to be the default alphabetical sort.

      Note that you can define a Key based on the objects in a list, like so:

      function Compare(i1, i2 : Integer) : Integer;
        begin
        case Key of
          1 :
            Result := AnsiCompareText(TSomeObject(Objects[i1]).Text,
                                      TSomeObject(Objects[i2].Text);
          else
            Result := inherited Compare(i1, i2);
          end;
        end;

      Of course, it is your responsibility to be sure that the objects in
    the list are the type that your Compare method assumes them to be.

                  === Important ===

   If you do have a list that is
     a) sorted, and
     b) determines its order via values derived from the objects that are
        stored in the list,
   watch out for changing objects in such a way as to change their sort order;
   the TSortableList will not know that the list is now out of order and
   calls to routines that depend on knowing this (such as Add) may fail to
   work.  Your best bet in this case is to set Sorted to False, make whatever
   changes to the objects you wish, and then set Sorted back to True.  This
   will resort the list.

     I also took the liberty of "protecting" the Find method against the
   possibility that someone would call it when the list was not sorted --
   in that case, it now calls IndexOf (which, somewhat recursively, would
   call Find if the list =was= sorted).  This is exactly what happened in
   the example code for Find!  If you look at the method list for
   TStringList, you won't find Find -- but you can do a topic search for
   it.  The example code for Find works, but only by chance -- add another
   string to either end of the list, and "Flowers" won't be found.  What's
   wrong with the example code is that the list's Sorted property is not set
   to True; the reason it (accidently) works is because the item that
   was being found ("Flowers") happened to be the middle item in the list,
   which is where the search algorithm looks first.

     If you have any comments, suggestions or even criticisms <g> for
   TSortableList, hey, tough!  No, really, send me some mail at 71744,422.
   I am particularly interested in bug reports.

*)

interface

Uses
  Classes;

type
  TSortableList = class(TStringList)
    private
    FOwnsObjects : Boolean;
    FKey : Integer;
    FAscending : Boolean;
    procedure QuickSort(left, right: Integer);
    function CallCompare(i1, i2 : Integer) : Integer;
    procedure SetAscending(value : Boolean);
    procedure SetKey(value : Integer);
    protected
    procedure PutObject(index : Integer; AObject : TObject); override;
    function Compare(i1, i2 : Integer) : Integer; virtual;
    public
    constructor Create(owns_objects : Boolean);
    procedure Clear; override;
    procedure Delete(index : Integer); override;
    function Find(const s : string; var index : Integer): Boolean; override;
    procedure Sort; override;
    property Ascending : Boolean read FAscending write SetAscending;
    property Key : Integer read FKey write SetKey;
    property OwnsObjects : Boolean read FOwnsObjects;
    end;

implementation

Uses
  SysUtils;

{ Private Methods }

procedure TSortableList.QuickSort(left, right: Integer);
  var
    i, j, pivot : Integer;
    s : String;
  begin
  i := left;
  j := right;

  { Rather than store the pivot value (which was assumed to be a string),
    store the pivot index }
  pivot := (left + right) shr 1;

  repeat
    while CallCompare(i, pivot) < 0 do
      Inc(i);
    while CallCompare(j, pivot) > 0 do
      Dec(j);
    if i <= j then
      begin
      Exchange(i, j);

      { If we just moved the pivot item, reset the pivot index }
      if pivot = i then
        pivot := j
      else if pivot = j then
        pivot := i;

      Inc(i);
      Dec(j);
      end;
    until i > j;
  if left < j then
    QuickSort(left, j);
  if i < right then
    QuickSort(i, right);
  end;

function TSortableList.CallCompare(i1, i2 : Integer) : Integer;
  begin
  Result := Compare(i1, i2);
  if not FAscending then
    Result := -Result;
  end;

procedure TSortableList.SetAscending(value : Boolean);
  begin
  if value <> FAscending then
    begin
    FAscending := value;
    if Sorted then
      begin
      Sorted := False;
      Sorted := True;
      end
    end;
  end;

procedure TSortableList.SetKey(value : Integer);
  begin
  if value <> FKey then
    begin
    FKey := value;
    if Sorted then
      begin
      Sorted := False;
      Sorted := True;
      end
    end;
  end;

{ Protected Methods }

function TSortableList.Compare(i1, i2 : Integer) : Integer;
  begin
  Result := AnsiCompareText(Strings[i1], Strings[i2]);
  end;

{ Public Methods }

constructor TSortableList.Create(owns_objects : Boolean);
  begin
  inherited Create;
  FOwnsObjects := owns_objects;
  FKey := 0;
  FAscending := True;
  end;

procedure TSortableList.Clear;
  var
    index : Integer;
  begin
  Changing;
  if FOwnsObjects then
    for index := 0 to Count - 1 do
      GetObject(index).Free;
  inherited Clear;
  Changed;
  end;

procedure TSortableList.Delete(index: Integer);
  begin
  Changing;
  if FOwnsObjects then
    GetObject(index).Free;
  inherited Delete(index);
  Changed;
  end;

function TSortableList.Find(const s : string; var index : Integer): Boolean;
  begin
  if not Sorted then
    begin
    index := IndexOf(s);
    Result := (index <> -1);
    end
  else
    Result := inherited Find(s, index);
  end;

procedure TSortableList.PutObject(index: Integer; AObject: TObject);
  begin
  Changing;
  if FOwnsObjects then
    GetObject(index).Free;
  inherited PutObject(index, AObject);
  Changed;
  end;

procedure TSortableList.Sort;
  begin
  if not Sorted and (Count > 1) then
    begin
    Changing;
    QuickSort(0, Count - 1);
    Changed;
    end;
  end;

end.

[Back to OOP SWAG index]  [Back to Main SWAG index]  [Original]