I am using a CustomSort on a TStringList to order my strings from newest to oldest.
function dateSort(List: TStringList; Index1, Index2: Integer): Integer;
I noticed that that the starting value of Index1 is 0 but the starting value of Index2 is half the number of items in the TStringList rounded up (e.g 23 items in the list would mean Index2 starts at 12) - why is this?
Related
I am trying to custom sort a TStringList by a column in a .CSV file. My code below works (slowly, about 14 seconds for 200,000+ lines):
function Compare(List: TStringList; Index1, Index2: Integer): Integer;
function ColStr(const Ch: Char; const S: String; First, Last: Integer): String;
var
p1, p2: Integer;
function GetPos(const N: Integer; Start: Integer = 1): Integer;
var
I, Len, Count: Integer;
begin
Result := 0;
Len := Length(S);
if (Len = 0) or (Start > Len) or (N < 1) then Exit;
Count := 0;
for I := Start to Len do begin
if S[I] = Ch then begin
Inc(Count);
if Count = N then begin
Result := I;
Exit;
end;
end;
end;
end;
begin
p1 := GetPos(4, 1); // 4 should be a variable
p2 := GetPos(5, 1); // 5 should be a variable
if Last = 0 then Result := Copy(S, p1 + 1, length(S)) else Result := Copy(S, p1 + 1, p2 - p1 - 1);
end;
begin
Result := AnsiCompareStr(ColStr(',', List[Index1], 0, 1), ColStr(',', List[Index2], 0, 1));
end;
What I would want to do is not have this hard-coded but (where commented "should be a variable" depending on which column to sort). I know I can't use:
function Form1.Compare(List: TStringList; Index1, Index2: Integer): Integer;
for inserting variables, as I get the error:
Incompatible types: 'method pointer and regular procedure'.
I have searched through SO looking for instances of this error but cannot find one that fits my question. I would appreciate any pointers in the right direction.
This has to be done with Delphi 7 and Windows 11.
TStringList.CustomSort() does not let you pass in extra parameters, and it does not accept class methods or anonymous procedures. But, what it does do is pass the actual TStringList itself to the callback, so I would suggest deriving a new class from TStringList to add extra fields to it, and then you can access those fields inside the callback, eg:
type
TMyStringList = class(TStringList)
public
Count1: Integer;
Count2: Integer;
end;
function Compare(List: TStringList; Index1, Index2: Integer): Integer;
...
p1 := GetPos(TMyStringList(List).Count1, 1);
p2 := GetPos(TMyStringList(List).Count2, 1);
...
begin
...
end;
...
List := TMyStringList.Create;
// fill List ...
List.Count1 := ...;
List.Count2 := ...;
List.CustomSort(Compare);
So you are performing searching for k-th occurence of Ch and substring creation at every comparison.
You can optimize this process - before sorting make list/array of stringlists, created from every string, separated by needed character - use DelimitedText.
Inside compare function just work with this array and column numbers - sadly, you have to define them as global variables in current unit (for example, after Form1: TForm1)
i'm trying to sort TStringList of integers from a text file with Insertion and Selection Sort .Selection Sort works ok , but the Insertion Sort doesnt work with my code . Can someone tell me where i'm wrong ? My 'numbers.txt' has 5000 lines of numbers. Thanks in advance
UPDATE : I have edited my code a bit , it works now with Insertion-Sort but it sorts just 4 indexes of integer as on the image
var
i, Position, n: integer;
Value: string;
begin
n := Items.Count;
for i := 1 to n - 1 do
begin
Value := Items[i];
Position := i-1;
while (Position >0) and (Items[Position]>Value) do
begin
Items[Position+1]:= Items[Position] ;
Position := Position -1 ;
end;
Items[Position+1] := Value;
end;
end;
Your data in the image is sorting exactly as it should, because you're sorting on string values, and based on the comparison you're making the order is perfect. "1143" falls exactly between the string values "11413" and "11443", because the comparison is made character by character out to the length of the shortest of the values. "1141" < "1143" < "1144", based on the first four characters of each string.
If you want an actual integer sort, then you need to convert the two values to integer before comparing them. Something like this should work (note I did not test your overall sort logic - I just used values that demonstrate the concept):
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, System.Classes;
var
i, Position, n: integer;
Value: integer;
Items: TStringList;
begin
Items := TStringList.Create;
try
Items.DelimitedText := '1116,11170,11178,11206,1122,11221,11228';
n := Items.Count;
for i := 1 to n - 1 do
begin
Value := StrToInt(Items[i]);
Position := i - 1;
while (Position > 0) and (StrToInt(Items[Position]) > Value) do
begin
Items[Position + 1]:= Items[Position];
Position := Position - 1 ;
end;
Items[Position+1] := IntToStr(Value);
end;
for i := 0 to Items.Count - 1 do
WriteLn(Items[i]);
finally
Items.Free;
end;
ReadLn;
end.
The output I got from the code above in a console window:
1116
1122
11170
11178
11206
11221
11228
I have a program that stores the last three recent scores of a game. However, I would like to store only the best scores. Eg. Recent Scores: Tom - 12, Sam - 14, Sue - 16. If i played the game and got a new score of 20, i would like it to store the new score of 20 (with name) and the other two scores of Sam and Sue ...thereby losing Tom. (I'm not worried about order).
Const NoOfRecentScores = 3;
TRecentScore = Record
Name : String;
Score : Integer;
End;
TRecentScores = Array[1..NoOfRecentScores] of TRecentScore;
Var
RecentScores : TRecentScores;
When i play the game, i call a procedure called UpdateRecentScores. Here it is:
Procedure UpdateRecentScores(Var RecentScores : TRecentScores; Score : Integer);
Var
PlayerName : String;
Count,count2 : Integer;
FoundSpace : Boolean;
MinMark,position: Integer;
ScorePresent:boolean = false;
Begin
PlayerName := GetPlayerName;
FoundSpace := False;
Count :=1;
While Not FoundSpace And (Count <= NoOfRecentScores)
Do If RecentScores[Count].Name = ''
Then FoundSpace := True
Else Count := Count + 1;
Here is the part i am struggling with. If no score is previously entered, then i have to accept that the first entered score is going to be the minimum:
If ScorePresent = False then
begin
MinMark:=Score;
ScorePresent:=True;
RecentScores[Count].Name := PlayerName;
RecentScores[Count].Score := Score;
writeln('Minimum Mark is: ',MinMark);
end
...the problem with the above, however, is that if the first score is a very high score, that becomes my minimum score!
Below, i am simply saying that if the Score achieved is greater than the MinMark (i.e. the minimum score) then the score should be stored in the array.
else if Score> MinMark then
begin
For count:= 1 to NoOfRecentScores do
begin
if RecentScores[count].score<Score then
position:=count;
RecentScores[position].Name := PlayerName;
RecentScores[position].Score := Score;
end;
End;
end;
As you can see, i am trying to check what the MinMark is first of all. Then, compare the score that i've just got with the MinMarker to see if it should be stored.
To clarify therefore, I want to save the best 3 scores and not recent scores.
To store n best scores, it is convenient to order them from high to low.
Let us go with an example first. Suppose you have the following four records: Tim - 14, Susan - 7, Don - 5, and Derek - 12. Then the array will look like [('Tim', 14), ('Derek', 12), ('Susan', 7), ('Don', 5)].
Now, what happens when Bert achieves a score of 9? Turns out we just want to insert a pair in a sorted array, so that it becomes [('Tim', 14), ('Derek', 12), ('Bert', 9), ('Susan', 7), ('Don', 5)]. After that, we drop the last element.
If we have to modify the array in place, we find the position pos where to insert Bert (it is position 3), then move everything in positions pos..(n-1) into positions (pos+1)..n, and after that, write Ben and his score to position pos.
Since you're using such a small array (3 records), it's relatively simple. (If your array was considerably larger, you'd want to keep it sorted and use a faster means of locating the right position for it, but your array is extremely small.)
Let's say you now have three RecentScore records, containing Tim - 14, Susan - 7, Derek - 12.
You need to find out if there's a score lower than the one the user just attained by Gemma (9) which is in the CurrentScore record (type TRecentScore), and if so replace it with Gemma's name and score.
Here's a working console project (compiled and run in XE5):
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils;
type
TRecentScore = record
Name: string;
Score: Integer;
end;
TRecentScores = array of TRecentScore;
// Function to find lower score than the one we're trying to add.
// Returns the index of the next lower item if there is one, or
// -1 if there isn't one.
function FindLowerScore(Scores: TRecentScores; CheckScore: Integer): Integer;
var
i: Integer;
MinScore: Integer; // Lowest score found
begin
Result := -1; // No index found
MinScore := CheckScore; // Lowest score so far
for i := Low(Scores) to High(Scores) do
if Scores[i].Score < MinScore then // Lower than CheckScore?
begin
MinScore := Scores[i].Score; // Yep. Store it (new lowest)
Result := i; // and where it was found
end;
end;
// Utility procedure to display list of scores
procedure PrintScores(const Prelude: string; Scores: TRecentScores);
var
Score: TRecentScore;
begin
WriteLn(Prelude);
for Score in Scores do
Writeln(' ' + Score.Name + ' = ' + IntToStr(Score.Score));
end;
var
RecentScores: TRecentScores;
CurrentScore: TRecentScore;
i: Integer;
begin
SetLength(RecentScores, 3);
RecentScores[0].Name := 'Tim';
RecentScores[0].Score := 14;
RecentScores[1].Name := 'Susan';
RecentScores[1].Score := 7;
RecentScores[2].Name := 'Derek';
RecentScores[2].Score := 12;
// Show scores where we begin
PrintScores('Before', RecentScores);
CurrentScore.Name := 'Gemma'; CurrentScore.Score := 9;
// Check for lower score than Gemma's
i := FindLowerScore(RecentScores, CurrentScore.Score);
if i = -1 then
WriteLn('No lower score found!')
else
begin
// We have a lower score in the array. Update that one
// with our new score.
RecentScores[i].Name := CurrentScore.Name;
RecentScores[i].Score := CurrentScore.Score;
PrintScores('After', RecentScores);
end;
ReadLn;
end.
Here is what i used, following your advice...
Procedure UpdateRecentScores(Var RecentScores : TRecentScores; Score : Integer);
Var
PlayerName : String;
Count : Integer;
FoundSpace : Boolean;
Begin
PlayerName := GetPlayerName;
FoundSpace := False;
Count := 1;
While Not FoundSpace And (Count <= NoOfRecentScores)
Do If RecentScores[Count].Name = ''
Then
begin
FoundSpace := True;
RecentScores[Count].Name := PlayerName;
RecentScores[Count].Score := Score;
end
Else Count := Count + 1;
If Not FoundSpace
Then
Begin
SortRecentScores(RecentScores); // sort them into order of score
if score > RecentScores[NoOfRecentScores].Score then
begin
RecentScores[NoOfRecentScores].Name:= PlayerName;
RecentScores[NoOfRecentScores].Score:= Score;
end;
End;
End;
I've got a piece of code like this:
for I := 0 to Self.EventQueue.Count do
Dispose(Self.EventQueue[I]);
It bugs out when the Count is 0, because it tries to Dispose a nonexisting element. When I change it to
for I := 0 to Self.EventQueue.Count-1 do
Dispose(Self.EventQueue[I]);
All works fine. Is there any elegant way to get around this or is this common practice?
This is absolutely normal behavior, and is documented in the help for every list and container class in Delphi/FreePascal. The reason is pretty clear - if you have three items in the list, and the first item is at index 0, then you have items 0, 1, 2 but a Count of 3, right?
for i := 0 to StringList.Count - 1 do // TStringList
for i := 0 to List.Count - 1 do // TList
for i := 0 to StringGrid1.ColCount do // TStringGrid
The alternative isn't as clear (and to me is worse to type):
for i := 0 to Pred(StringList.Count) do
Dynamic arrays start at index 0 as well.
var
IntArray: array of Integer;
i: Integer;
begin
SetLength(IntArray, 10);
for i := Low(IntArray) to High(IntArray) do // loop is 0..9
//
end;
The only things that aren't 0 based in FPC/Delphi are string types, which start at 1, and non-dynamic arrays (ones that are declared with a fixed size in code), which can start at almost any index you want. For instance, this is perfectly legal:
var
IntArray: array[-10..10] of Integer;
i: Integer;
begin
for i := Low(IntArray) to High(IntArray) do // loop is -10..10
//
end;
Just as an aside, any time you do anything in your loop that will reduce the number of items in your list, you should iterate backwards:
for i := List.Count - 1 downto 0 do
Otherwise, you'll iterate beyond the end of the list, because the Count is only evaluated at the time the loop starts.
Omg. That's because in cycle from 0 to Self.EventQueue.Count you iterate through Self.EventQueue.Count + 1 items.
I prefere to use
for I := 1 to Self.EventQueue.Count do
Dispose(Self.EventQueue[I-1]);
That way it is clear that nothing happens if the count is zero and the correction of the index happens at the place where it matters
I need an algorithm in Delphi to generate partitions for a specified integer value.
Example: for 13 if 5 is specified as the max value for partition it will give 5,5,3; if 4 is specified as max partition value the result should be 4,4,4,1, and so on.
It's simple enough to solve the problem using div and mod. Here's an example program that I don't think needs any further explanation:
program IntegerPartitions;
{$APPTYPE CONSOLE}
function Partitions(const Total, Part: Integer): TArray<Integer>;
var
Count: Integer;
Rem: Integer;
i: Integer;
begin
Assert(Total>0);
Assert(Part>0);
Count := Total div Part;
Rem := Total mod Part;
if Rem=0 then
SetLength(Result, Count)
else
SetLength(Result, Count+1);
for i := 0 to Count-1 do
Result[i] := Part;
if Rem<>0 then
Result[Count] := Rem;
end;
var
Value: Integer;
begin
for Value in Partitions(13, 5) do
Writeln(Value);
Readln;
end.