Everything about generic TList
. I have this structure:
Type
TExtract = record
Wheel: string;
Extract: array [1..5] of Byte;
end;
Here we go again.
You should use the built-in TList<T>.BinarySearch()
function, even if it rightfully asks for a TEstr
record as a parameter. You'll first need to use TList<T>.Sort()
to sort the list using the same criteria as for the search, then call BinarySearch()
to find your record.
Here's a function that does both (sort and search):
uses Generics.Defaults; // this provides TDelegatedComparer
uses Math; // this provides Sign()
function SearchList(Date:TDate; Sort:Boolean; List:TList<TEstr>): Integer;
var Comparer: IComparer<TEstr>;
Dummy: TEstr;
begin
// Prepare a custom comparer that'll be used to sort the list
// based on Date alone, and later to BinarySearch the list using
// date alone.
Comparer := TDelegatedComparer<TEstr>.Construct(
function (const L, R: TEstr): Integer
begin
Result := Sign(L.Date - R.Date);
end
);
// If the list is not sorted, sort it. We don't know if it's sorted or not,
// so we rely on the "Sort" parameter
if Sort then List.Sort(Comparer);
// Prepare a Dummy TEstr record we'll use for searching
Dummy.Date := Date;
// Call BinarySearch() to look up the record based on Date alone
if not List.BinarySearch(Dummy, Result, Comparer) then
Result := -1;
end;
BinarySearch
assumes the list is sorted (that's the essence of binary searching!). On your first call you need to set Sort=True
so the list is properly sorted. On subsequent calls Sort
should be False
. Of course, in actual use you'd probably have separate routines for searching and sorting, and you'd probably have them as methods of a class descending from TList<TEstr>
(to make things easyer). I places both in the same routine for dempnstration purposes.
You could also declare a helper class like this, to avoid the requirement of IComparer
that both left and right side of the comparison must be of the specialized type:
type
TLeftComparison<T> = reference to function(const Left: T; var Value): Integer;
TListHelper<T> = class
public
class function BinarySearch(Instance: TList<T>; var Value; out FoundIndex: Integer;
Comparison: TLeftComparison<T>; Index, Count: Integer): Boolean; overload;
class function BinarySearch(Instance: TList<T>; var Value; out FoundIndex: Integer;
Comparison: TLeftComparison<T>): Boolean; overload;
class function Contains(Instance: TList<T>; var Value; Comparison: TLeftComparison<T>): Boolean;
class function IndexOf(Instance: TList<T>; var Value; Comparison: TLeftComparison<T>): Integer;
class function LastIndexOf(Instance: TList<T>; var Value; Comparison: TLeftComparison<T>): Integer;
end;
class function TListHelper<T>.BinarySearch(Instance: TList<T>; var Value; out FoundIndex: Integer;
Comparison: TLeftComparison<T>; Index, Count: Integer): Boolean;
var
L, H: Integer;
mid, cmp: Integer;
begin
Result := False;
L := Index;
H := Index + Count - 1;
while L <= H do
begin
mid := L + (H - L) shr 1;
cmp := Comparison(Instance[mid], Value);
if cmp < 0 then
L := mid + 1
else
begin
H := mid - 1;
if cmp = 0 then
Result := True;
end;
end;
FoundIndex := L;
end;
class function TListHelper<T>.BinarySearch(Instance: TList<T>; var Value; out FoundIndex: Integer;
Comparison: TLeftComparison<T>): Boolean;
begin
Result := BinarySearch(Instance, Value, FoundIndex, Comparison, 0, Instance.Count);
end;
class function TListHelper<T>.Contains(Instance: TList<T>; var Value; Comparison: TLeftComparison<T>): Boolean;
begin
Result := IndexOf(Instance, Value, Comparison) >= 0;
end;
class function TListHelper<T>.IndexOf(Instance: TList<T>; var Value; Comparison: TLeftComparison<T>): Integer;
var
I: Integer;
begin
for I := 0 to Instance.Count - 1 do
if Comparison(Instance[I], Value) = 0 then
Exit(I);
Result := -1;
end;
class function TListHelper<T>.LastIndexOf(Instance: TList<T>; var Value; Comparison: TLeftComparison<T>): Integer;
var
I: Integer;
begin
for I := Instance.Count - 1 downto 0 do
if Comparison(Instance[I], Value) = 0 then
Exit(I);
Result := -1;
end;
Then you could use it like this:
// TComparison (requires instances on both sides)
function CompareEstr(const Left, Right: TEstr): Integer;
begin
if Left.Date < Right.Date then
Exit(-1);
if Left.Date > Right.Date then
Exit(1);
Result := 0;
end;
// TLeftComparison: requires instance only on the left side
function CompareEstr2(const Left: TEstr; var Value): Integer;
begin
if Left.Date < TDateTime(Value) then
Exit(-1);
if Left.Date > TDateTime(Value) then
Exit(1);
Result := 0;
end;
procedure Main;
var
Date: TDate;
Comparer: IComparer<TEstr>;
List: TEstrList;
Item: TEstr;
Index: Integer;
I: Integer;
begin
Comparer := nil;
List := nil;
try
// create a list with a comparer
Comparer := TComparer<TEstr>.Construct(CompareEstr);
List := TEstrList.Create(Comparer);
// fill with some data
Date := EncodeDate(2011, 1, 1);
for I := 0 to 35 do
begin
Item.Date := IncMonth(Date, I);
List.Add(Item);
end;
// sort (using our comparer)
List.Sort;
Date := EncodeDate(2011, 11, 1);
Item.Date := Date;
// classic approach, needs Item on both sides
Index := List.IndexOf(Item);
Writeln(Format('TList.IndexOf(%s): %d', [DateToStr(Date), Index]));
List.BinarySearch(Item, Index);
Writeln(Format('TList.BinarySearch(%s): %d', [DateToStr(Date), Index]));
Writeln;
// here we can pass Date directly
Index := TListHelper<TEstr>.IndexOf(List, Date, CompareEstr2);
Writeln(Format('TListHelper.IndexOf(%s): %d', [DateToStr(Date), Index]));
TListHelper<TEstr>.BinarySearch(List, Date, Index, CompareEstr2);
Writeln(Format('TListHelper.BinarySearch(%s): %d', [DateToStr(Date), Index]));
Readln;
finally
List.Free;
end;
end;
This is of course less type-safe (due to the untyped right-side comparison parameter) but needed to allow to generically compare values of different types. With a bit of care this should not be a problem. Otherwise you could also write overloaded versions for most used types you need to compare.
Does it really needs to be a TList? Imo, binary searches are too complicated for this. Maybe you could simply use a TDictionary
:
type
TEstrCollection = TDictionary<TDate, TEstr>;
var
EstrCollection: TEstrCollection;
begin
EstrCollection := TEstrCollection.Create;
// Add an item
EstrCollection.Add(Date, TExtractList.Create)
// Search
ExtractList := EstrCollection[Date];
end;
Now, this requires the date field to be unique, as it's the key of the dictionary. Also, the items have no specific order.
If order is important, I would combine data structures. For example, you can have a TList just to hold the items in order plus a TDictionary just to perform the search, things like that.
The only thing is that records
are not pointers. To add the same record
in two distinct data structures you need to create pointers for them
PEstr = ^TEstr
Or just use objects, as they are pointers already. You could use TObjectList
and TObjectDictionary
to have life time of the items automatically managed by the collection (just remember to have only one of the collections managing the lifetime of the object if it is in more than one collection).
I think you have this function in TDynArrayHashed , an example in this Post
There is only a single way I've found to search through a list with a specific value.
I'll reuse Cosmin Prund example :
uses Generics.Defaults; // this provides TDelegatedComparer
uses Math; // this provides Sign()
function SearchList(Date:TDate; Sort:Boolean; List:TList<TEstr>): Integer;
var Dummy : TEstr;
begin
// If the list is not sorted, sort it. We don't know if it's sorted or not,
// so we rely on the "Sort" parameter
if Sort then List.Sort(TDelegatedComparer<TEstr>.Construct(
function (const L, R: TEstr): Integer
begin
Result := Sign(L.Date - R.Date);
end
);
// Call BinarySearch() to look up the record based on Date alone
if not List.BinarySearch(Dummy, Result, TDelegatedComparer<TEstr>.Construct(
function (const L, R: TEstr): Integer
begin
//By implementation, the binarySearch Dummy parameter is passed in the "R" parameter of the Comparer function. (In delphi 2010 at least)
Result := Sign(L.Date - Date); //Use the Date parameter instead of R.Date
end) then
Result := -1;
end;
This approach, however, is only valid "by implementation" and not "by design" (as far as I know). In other word, it is prone to break between versions of Delphi. So this is only advisable to use this approach for items that can be "performance expensive" to create. If you do so, I strongly advise to add something like this in your code.
{$IF RTLVersion > *YourCurrentVersion*}
{$MESSAGE WARNING 'Verify if BinarySearch implementation changed'}
{$IFEND}
p That way, next time you build this code in a newer version of Delphi, you will automatically get a warning telling you to make sure your code will still work as expected. But this could still causes problems if your code needs to support more than 1 version of Delphi at the same time.