Delphi: Store multiple Sortings of TObjectList - sorting

I have a bunch of TCoordinates which are stored in a TObjectList. To find a Coordinate faster the List must be sorted. The problem is that iam alternating searching for x and y. Is there a build in way to store the outcome of the sorting, so i dont need to sort the list again and again.
unit uStackoverflowQuestion;
interface
uses
System.Generics.Collections, System.Generics.defaults;
type
TCoordinate = class(Tobject)
public
x: Integer;
y: Integer;
end;
TMultipleSortedList = class(TObjectlist<TCoordinate>)
public
// StoredSortingByX: List;
// StoredSortingByY: List;
procedure SortAndStoreByX;
procedure SortAndStoreByY;
end;
implementation
procedure TMultipleSortedList.SortAndStoreByX;
begin
// TODO -cMM: TMultipleSortedList.SortAndStoreByX default body inserted
end;
procedure TMultipleSortedList.SortAndStoreByY;
begin
// TODO -cMM: TMultipleSortedList.SortAndStoreByY default body inserted
end;
end.

Create an index map to represent the two different orders. This is simply a dynamic array of integer.
type
TListOrder = TArray<Integer>;
When you wish to read an item using that order you do so like this:
function GetItem(Index: Integer; const Order: TListOrder): TItem;
begin
Result := List[Order[Index]];
end;
The key point here is that we don't modify the content of List ever. We regard that as unordered. Instead, we hold the order separate to the container. That allows us to have multiple such orders.
The next question is how to create an order. First of all populate the order with all the valid indices:
var
i: Integer;
Order: TListOrder;
....
SetLength(Order, List.Count);
for i := 0 to List.Count-1 do
Order[i] := i;
Now you can sort the order like so:
TArray.Sort<Integer>(Order, Comparer);
Finally, what to use as the comparer. This is where the magic happens.
var
Comparer: IComparer<Integer>;
....
Comparer :=
function(const Left, Right: Integer): Integer
var
LeftItem, RightItem: TItem;
begin
LeftItem := GetItem(Left, Order);
RightItem := GetItem(Right, Order);
Result := ...; // your compare logic goes here
end;
And that's it.

If the objects in your list do not change, you can use a TList<> to store an additional reference to the objects instead of the integer array that David Heffernan suggested. It has a small advantage in access time.

Related

Sorting TObjectList<T> swaps equal values [duplicate]

This question already has an answer here:
How Can I Replace StringList.Sort with a Stable Sort in Delphi?
(1 answer)
Closed 1 year ago.
I have the following (simplified) class definition:
TMyObject = class
private
FDoubleValue: Double;
FText: string;
protected
public
constructor Create(ADoubleValue: Double; AText: string);
property DoubleValue: Double read FDoubleValue write FDoubleValue;
property Text: string read FText write FText;
end;
The following sample code, shows how I am sorting the TObjectList<TMyObject> (FMyObjects) and displaying them in a TListBox.
constructor TfrmMain.Create(AOwner: TComponent);
begin
inherited;
FMyObjects := TObjectList<TMyObject>.Create;
FMyObjects.OwnsObjects := true; // Default but for clarity
end;
destructor TfrmMain.Destroy;
begin
FMyObjects.Free;
inherited;
end;
procedure TfrmMain.FormCreate(Sender: TObject);
var
ii: Integer;
begin
FMyObjects.Add(TMyObject.Create(100.00, 'Item 1'));
FMyObjects.Add(TMyObject.Create(200.00, 'Item 2'));
FMyObjects.Add(TMyObject.Create(300.00, 'Item 3')); // Duplicate sort value
FMyObjects.Add(TMyObject.Create(300.00, 'Item 4')); // Duplicate sort value
FMyObjects.Add(TMyObject.Create(400.00, 'Item 5'));
ObjectsToListBox;
end;
procedure TfrmMain.SortList;
var
Comparer: IComparer<TMyObject>;
begin
Comparer := TDelegatedComparer<TMyObject>.Create(
function(const MyObject1, MyObject2: TMyObject): Integer
begin
result := CompareValue(MyObject1.DoubleValue, MyObject2.DoubleValue, 0);
end);
FMyObjects.Sort(Comparer);
end;
procedure TfrmMain.ObjectsToListBox;
var
ii: Integer;
begin
ListBox1.Items.Clear;
for ii := 0 to FMyObjects.Count - 1 do
ListBox1.Items.Add(Format('%d - %.1f - %s', [ii, FMyObjects[ii].DoubleValue,
FMyObjects[ii].Text]));
end;
procedure TfrmMain.Button1Click(Sender: TObject);
begin
SortList;
ObjectsToListBox;
end;
Every time Button1 is clicked (and the list sorted), FMyObjects[2] (Item3) and FMyObjects[3] ('Item4') swap position in the list. In my "real world" (drawing) application this is undesirable.
I also experimented with different values for Epsilon in the CompareValue function call and also a different implementation of the anonymous function (comparing values and returning 1, -1 or 0), but neither seems to make a difference.
Am I missing something (e.g. a property that controls this behavior) or is this "by design" and it cannot be prevented?
This is by design. The internally used Quicksort implementation is not a stable one, so reorder of equal items is expected. To make the sort stable you need to extend your comparer to take that into account. F.i. you can compare the Text properties when the DoubleValue properties are equal.

Delphi Function that takes Integer and returns a not-so-easily decoded Integer

Can anyone share what are some common Delphi examples of a function that takes a number
and returns a number that is not so obvious?
For example :
function GetNumber(const aSeed: Integer): Integer;
begin
Result := ((aSeed+5) * aSeed) + 15;
end;
So let's say the user knows that sending aSeed = 21 gives 561
and aSeed = 2, gives 29
and so on...
is there a function that makes it hard to reverse engineer the code,
even if one can generate a large number sets of Seed/Result ?
(hard : I do not mean impossible, just need to be non-trivial)
Preferably a function that does not allow in function result exceeding the
Integer result as well.
In any case, if you are not sure whether it's hard/impossible to reverse,
do feel free to share what you have.
some other requirements:
the same input always results in the same output; cannot have Random output
the same output regardless of platform: windows/android/mac/ios
won't result in some extraordinary big number (fit in Integer)
Using a hash is a very good way to achieve what you want. Here is an example that takes an integer, converts it to a string, appends it to a salt, computes the MD5 and returns the integer corresponding to the first 4 bytes:
uses
System.Hash;
function GetHash(const s: string): TBytes;
var
MD5: THashMD5;
begin
MD5 := THashMD5.Create;
MD5.Update(TEncoding.UTF8.GetBytes(s));
Result := MD5.HashAsBytes;
end;
function GetNumber(Input: Integer): Integer;
var
Hash: TBytes;
p: ^Integer;
begin
Hash := GetHash('secret' + IntToStr(Input));
P := #Hash[0];
Result := Abs(P^);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
ShowMessage(IntToStr(GetNumber(1))); // 996659739
ShowMessage(IntToStr(GetNumber(2))); // 939216101
ShowMessage(IntToStr(GetNumber(3))); // 175456750
end;

How to forbid equal numbers

I started learning Pascal :) and I was interested on making a kind of Euromillion... However, I don't know how to forbid the same numbers or stars...
I thought this (below) would solve it... But it didn't... Help?
Program euromillion;
var num: array [1..5] of integer;
Procedure numbers;
var i, j: integer;
Begin
write ('Digite o número 1: ');
readln (num[1]);
for i:=2 to 5 do
for j:=1 to (i-1) do
Begin
repeat
write ('Digite o número ', i, ': ');
readln (num[i]);
until (num[i]>=1) and (num[i]<=50) and ((num[i]=num[j])=false);
End;
End;
Begin
numbers;
readln();
End.
Thanks guys :)
Although it is tempting to try and write a single block of code, as you have, it is better not to. Instead, a better way to write a program like this
is to think about splitting the task up into a number of procedures or functions
each of which only does a single part of the task.
One way to look at your task is to split it up into sub-tasks, as follows:
You prompt the user to enter a series of numbers
Once each number is entered, you check whether it is already in the array
If it isn't, you enter it in the array, otherwise prompt the user for another number
Once the array is filled, you output the numbers in the array
So, a key thing is that it would be helpful to have a function that checks whether
a new number is already in the array and returns True if it is and False otherwise. How to do that is the answer to your question.
You need to be careful about this because if you use the array a second time in the
program, you need to avoid comparing the new number with the array contents from
the previous time. I deliberately have not solved that problem in the example code below, to leave it as an exercise for the reader. Hint: One way would be to write a procedure which "clears" the array before each use of it, e.g. by filling it with numbers which are not valid lottery numbers, like negative numbers or zero. Another way would be to define a record which includes the NumberArray and a Count field which records how many numbers have been entered so far: this would avoid comparing the new number to all the elements in the
array and allow you to re-use the array by resetting the Count field to zero before calling ReadNumbers.
program LotteryNumbers;
uses crt;
type
TNumberArray = array[1..5] of Integer;
var
Numbers : TNumberArray;
Number : Integer;
function IsInArray(Number : Integer; Numbers : TNumberArray) : Boolean;
var
i : Integer;
begin
Result := False;
for i:= Low(Numbers) to High(Numbers) do begin
if Numbers[i] = Number then begin
Result := True;
break;
end;
end
end;
procedure ReadNumbers(var Numbers : TNumberArray);
var
i : Integer;
NewNumber : Integer;
OK : Boolean;
begin
// Note: This function needs to have a check added to it that the number
// the user enters is a valid lottery number, in other words that the
// number is between 1 and the highest ball number in the lottery
for i := Low(Numbers) to High(Numbers) do begin
repeat
OK := False;
writeln('enter a number');
ReadLn(NewNumber);
OK := not IsInArray(NewNumber, Numbers);
if not OK then
writeln('Sorry, you''ve already chosen ', NewNumber);
until OK;
Numbers[i] := NewNumber;
end;
end;
procedure ListNumbers(Numbers : TNumberArray);
var
i : Integer;
begin
for i := Low(Numbers) to High(Numbers) do
writeln(Numbers[i]);
end;
begin
ReadNumbers(Numbers);
ListNumbers(Numbers);
writeln('press any key');
readkey;
end.

Delphi - Sort TList<TObject> based on the object's properties [duplicate]

I'm kinda a Delphi-newbie and I don't get how the Sort method of a TList of Records is called in order to sort the records by ascending integer value.
I have a record like the following:
type
TMyRecord = record
str1: string;
str2: string;
intVal: integer;
end;
And a generic list of such records:
TListMyRecord = TList<TMyRecord>;
Have tried to find a code-example in the help files and found this one:
MyList.Sort(#CompareNames);
Which I can't use, since it uses classes. So I tried to write my own compare function with a little different parameters:
function CompareIntVal(i1, i2: TMyRecord): Integer;
begin
Result := i1.intVal - i2.intVal;
end;
But the compiler always throws a 'not enough parameters' - error when I call it with open.Sort(CompareIntVal);, which seems obvious; so I tried to stay closer to the help file:
function SortKB(Item1, Item2: Pointer): Integer;
begin
Result:=PMyRecord(Item1)^.intVal - PMyRecord(Item2)^.intVal;
end;
with PMyRecord as PMyRecord = ^TMyRecord;
I have tried different ways of calling a function, always getting some error...
The Sort overload you should be using is this one:
procedure Sort(const AComparer: IComparer<TMyRecord>);
Now, you can create an IComparer<TMyRecord> by calling TComparer<TMyRecord>.Construct. Like this:
var
Comparison: TComparison<TMyRecord>;
....
Comparison :=
function(const Left, Right: TMyRecord): Integer
begin
Result := Left.intVal-Right.intVal;
end;
List.Sort(TComparer<TMyRecord>.Construct(Comparison));
I've written the Comparison function as an anonymous method, but you could also use a plain old style non-OOP function, or a method of an object.
One potential problem with your comparison function is that you may suffer from integer overflow. So you could instead use the default integer comparer.
Comparison :=
function(const Left, Right: TMyRecord): Integer
begin
Result := TComparer<Integer>.Default.Compare(Left.intVal, Right.intVal);
end;
It might be expensive to call TComparer<Integer>.Default repeatedly so you could store it away in a global variable:
var
IntegerComparer: IComparer<Integer>;
....
initialization
IntegerComparer := TComparer<Integer>.Default;
Another option to consider is to pass in the comparer when you create the list. If you only ever sort the list using this ordering then that's more convenient.
List := TList<TMyRecord>.Create(TComparer<TMyRecord>.Construct(Comparison));
And then you can sort the list with
List.Sort;
The concise answer:
uses
.. System.Generics.Defaults // Contains TComparer
myList.Sort(
TComparer<TMyRecord>.Construct(
function(const Left, Right: TMyRecord): Integer
begin
Result := Left.intVal - Right.intVal;
end
)
);
I want to share my solution (based on the input I have gathered here).
It's a standard setup. A filedata class that holds data of a single file in a generic TObjectList. The list has the two private attributes fCurrentSortedColumn and fCurrentSortAscending to control the sort order. The AsString-method is the path and filename combined.
function TFileList.SortByColumn(aColumn: TSortByColums): boolean;
var
Comparison: TComparison<TFileData>;
begin
result := false;
Comparison := nil;
case aColumn of
sbcUnsorted : ;
sbcPathAndName: begin
Comparison := function(const Left, Right: TFileData): integer
begin
Result := TComparer<string>.Default.Compare(Left.AsString,Right.AsString);
end;
end;
sbcSize : begin
Comparison := function(const Left, Right: TFileData): integer
begin
Result := TComparer<int64>.Default.Compare(Left.Size,Right.Size);
if Result = 0 then
Result := TComparer<string>.Default.Compare(Left.AsString,Right.AsString);
end;
end;
sbcDate : begin
Comparison := function(const Left, Right: TFileData): integer
begin
Result := TComparer<TDateTime>.Default.Compare(Left.Date,Right.Date);
if Result = 0 then
Result := TComparer<string>.Default.Compare(Left.AsString,Right.AsString);
end;
end;
sbcState : begin
Comparison := function(const Left, Right: TFileData): integer
begin
Result := TComparer<TFileDataTestResults>.Default.Compare(Left.FileDataResult,Right.FileDataResult);
if Result = 0 then
Result := TComparer<string>.Default.Compare(Left.AsString,Right.AsString);
end;
end;
end;
if assigned(Comparison) then
begin
Sort(TComparer<TFileData>.Construct(Comparison));
// Control the sort order
if fCurrentSortedColumn = aColumn then
fCurrentSortAscending := not fCurrentSortAscending
else begin
fCurrentSortedColumn := aColumn;
fCurrentSortAscending := true;
end;
if not fCurrentSortAscending then
Reverse;
result := true;
end;
end;
I found a much simpler modified sort function to alphabetize a TList of records or nonstandard list of items.
Example
PList = ^TContact;
TContact = record //Record for database of user contact records
firstname1 : string[20];
lastname1 : string[20];
phonemobile : Integer; //Fields in the database for contact info
phonehome : Integer;
street1 : string;
street2 : string;
type
TListSortCompare = function (Item1,
Item2: TContact): Integer;
var
Form1: TForm1;
Contact : PList; //declare record database for contacts
arecord : TContact;
Contacts : TList; //List for the Array of Contacts
function CompareNames(i1, i2: TContact): Integer;
begin
Result := CompareText(i1.lastname1, i2.lastname1) ;
end;
and the function to call to sort your list
Contacts.Sort(#CompareNames);

saving a records containing a member of type string to a file (Delphi, Windows)

I have a record that looks similar to:
type
TNote = record
Title : string;
Note : string;
Index : integer;
end;
Simple. The reason I chose to set the variables as string (as opposed to an array of chars) is that I have no idea how long those strings are going to be. They can be 1 char long, 200 or 2000.
Of course when I try to save the record to a type file (file of...) the compiler complains that I have to give a size to string.
Is there a way to overcome this? or a way to save those records to an untyped file and still maintain a sort of searchable way?
Please do not point me to possible solutions, if you know the solution please post code.
Thank you
You can't do it with a typed file. Try something like this, with a TFileStream:
type
TStreamEx = class helper for TStream
public
procedure writeString(const data: string);
function readString: string;
procedure writeInt(data: integer);
function readInt: integer;
end;
function TStreamEx.readString: string;
var
len: integer;
iString: UTF8String;
begin
self.readBuffer(len, 4);
if len > 0 then
begin
setLength(iString, len);
self.ReadBuffer(iString[1], len);
result := string(iString);
end;
end;
procedure TStreamEx.writeString(const data: string);
var
len: cardinal;
oString: UTF8String;
begin
oString := UTF8String(data);
len := length(oString);
self.WriteBuffer(len, 4);
if len > 0 then
self.WriteBuffer(oString[1], len);
end;
function TStreamEx.readInt: integer;
begin
self.readBuffer(result, 4);
end;
procedure TStreamEx.writeInt(data: integer);
begin
self.WriteBuffer(data, 4);
end;
type
TNote = record
Title : string;
Note : string;
Index : integer;
procedure Save(stream: TStream);
end;
procedure TNote.Save(stream: TStream);
var
temp: TMemoryStream;
begin
temp := TMemoryStream.Create;
try
temp.writeString(Title);
temp.writeString(Note);
temp.writeInt(Index);
temp.seek(0, soFromBeginning);
stream.writeInt(temp.size);
stream.copyFrom(temp, temp.size);
finally
temp.Free;
end;
end;
I'll leave the Load procedure to you. Same basic idea, but it shouldn't need a temp stream. With the record size in front of each entry, you can read it and know how far to skip if you're looking for a certain record # instead of reading the whole thing.
EDIT: This was written specifically for versions of Delphi that use Unicode strings. On older versions, you could simplify it quite a bit.
Why not write this out as XML? See my session "Practical XML with Delphi" on how to get started with this.
Another possibility would be to make your records into classes descending form TComponent and store/retreive your data in DFM files.
This Stackoverflow entry shows you how to do that.
--jeroen
PS: Sorry my XML answer was a bit dense; I'm actually on the road for two conferences (BASTA! and DelphiLive! Germany).
Basically what you need to do is very simple: create a sample XML file, then start the Delphi XML Data Binding Wizard (available in Delphi since version 6).
This wizard will generate a unit for you that has the interfaces and classes mapping XML to Delphi objects, and a few helper functions for reading them from file, creating a new object, etc. My session (see the first link above) actually contains most of the details for this process.
The above link is a video demonstrating the usage of the Delphi XML Data Binding Wizard.
You could work with two different files, one that just stores the strings in some convenient way, the other stores the records with a reference to the strings. That way you will still have a file of records for easy access even though you don't know the size of the actual content.
(Sorry no code.)
TNote = record
Title : string;
Note : string;
Index : integer;
end;
could be translated as
TNote = record
Title : string[255];
Note : string[255];
Index : integer;
end;
and use Stream.writebuffer(ANodeVariable, sizeof(TNode), but you said that strings get go over 255 chars in this case IF a string goes over 65535 chars then change WORD to INTEGER
type
TNodeHeader=Record
TitleLen,
NoteLen: Word;
end;
(* this is for writing a TNode *)
procedure saveNodetoStream(theNode: TNode; AStream: TStream);
var
header: TNodeHeader;
pStr: PChar;
begin
...
(* writing to AStream which should be initialized before this *)
Header.TitleLen := Length(theNode.Title);
header.NodeLen := Length(theNode.Note);
AStream.WriteBuffer(Header, sizeof(TNodeHeader);
(* save strings *)
PStr := PChar(theNode.Title);
AStream.writeBuffer(PStr^, Header.TitleLen);
PStr := PChar(theNode.Note);
AStream.writebuffer(PStr^, Header.NoteLen);
(* save index *)
AStream.writebuffer(theNode.Index, sizeof(Integer));
end;
(* this is for reading a TNode *)
function readNode(AStream: TStream): TNode;
var
header: THeader
PStr: PChar;
begin
AStream.ReadBuffer(Header, sizeof(TNodeHeader);
SetLength(Result.Title, Header.TitleLen);
PStr := PChar(Result.Title);
AStream.ReadBuffer(PStr^, Header.TitleLen);
SetLength(Result.Note, Header.NoteLen);
PStr := PChar(Result.Note);
AStream.ReadBuffer(PStr^, Header.NoteLen);
AStream.ReadBuffer(REsult.Index, sizeof(Integer)(* 4 bytes *);
end;
You can use the functions available in this Open Source unit.
It allows you to serialize any record content into binary, including even dynamic arrays within:
type
TNote = record
Title : string;
Note : string;
Index : integer;
end;
var
aSave: TRawByteString;
aNote, aNew: TNote;
begin
// create some content
aNote.Title := 'Title';
aNote.Note := 'Note';
aNote.Index := 10;
// serialize the content
aSave := RecordSave(aNote,TypeInfo(TNote));
// unserialize the content
RecordLoad(aNew,pointer(aSave),TypeInfo(TNote));
// check the content
assert(aNew.Title = 'Title');
assert(aNew.Note = 'Note');
assert(aNew.Index = 10);
end;

Resources