TStringList.CustomSort: Compare() with variables - algorithm

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)

Related

Delphi - descending sort of string grid rows, sorting by 1 column

I've run into a bit of a wall with my sorting, I managed to sort the rows of my string grid from smallest to largest but now I'm not sure how to sort it in descending order. I've tried using the code I used from the other sort and I only changed the 2nd last loop in the code to see if I can read from the bottom of the TStringList, but it hasn't worked and only takes one row from the list and duplicates it into the rest of the rows. Is there perhaps a way to reverse read a TStringList after sorting?
Code I used for the other sort I have and tried to implement for this sort (only changed the 2nd last loop):
procedure TfrmPuntehou.SortLTSGrid(var grid: TStringGrid; columntotal: Integer);
const
separator = ',';
var
iCount,i,j,k,iPos:integer;
TheList:TStringList;
sString,sTempString:string;
m: Integer;
o: Integer;
begin
//procedure to sort from large to small values
//get row amount
iCount:=grid.RowCount-1;
//create list
TheList:=TStringList.Create;
TheList.Sorted:=False;
//start of try..finally block
try
begin
//fill the list
for i := 1 to (iCount - 1) do
begin
TheList.Add(grid.Rows[i].Strings[columntotal]+separator+grid.Rows[i].Text);
end;
//sort the list
TheList.Sort;
for k := 1 to TheList.Count do
begin
//take the line of the list and put it in a string var
sString:= TheList.Strings[(k-1)];
//get separator pos in that string
iPos:=AnsiPos(separator,sString);
sTempString:='';
//remove separator and the column text at the front of the string
sTempString:=Copy(sString,(iPos+1),Length(sString));
TheList.Strings[(k-1)]:= '';
TheList.Strings[(k-1)]:= sTempString;
end;
//fill the grid
for j:= (iCount - 1) downto 1 do
begin
for o := 1 to (iCount - 1) do
begin
grid.Rows[j].Text := TheList.Strings[(o-1)] ;
end;
end;
//fill the row numbers
for m := 1 to iCount do
begin
grid.Cells[0,m]:= IntToStr(m);
end;
end;
finally
TheList.Free;
end;
//end of try..finally block
end;
Thanks in advance for the help!Kind RegardsPrimeBeat
Use TStringList.CustomSort to sort the list using a specific method for comparison.
The specification for the comparer is given here.
Example:
function Compare1( // Normal alphanum sort
List : TStringList;
Index1 : Integer;
Index2 : Integer) : Integer;
begin
if List[Index1] = List[Index2] then
Result := 0
else if List[Index1] < List[Index2] then
Result := -1
else
Result := 1;
end;
function Compare2( // Reverse alphanum sort
List : TStringList;
Index1 : Integer;
Index2 : Integer) : Integer;
begin
if List[Index1] = List[Index2] then
Result := 0
else if List[Index1] < List[Index2] then
Result := 1
else
Result := -1;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
SList : TStringList;
S : String;
begin
SList := TStringList.Create;
try
SList.Add('Pierre');
SList.Add('Albert');
SList.Add('Paul');
SList.Add('Jean');
SList.Add('Simon');
Memo1.Lines.Add('=== Compare1 ===');
SList.CustomSort(Compare1);
for S in SList do
Memo1.Lines.Add(S);
Memo1.Lines.Add('=== Compare2 ===');
SList.CustomSort(Compare2);
for S in SList do
Memo1.Lines.Add(S);
finally
SList.Free;
end;
end;

Read integers from a string

I'm learning algorithms and I'm trying to make an algorithm that extracts numbers lets say n in [1..100] from a string. Hopefully I get an easier algorithm.
I tried the following :
procedure ReadQuery(var t : tab); // t is an array of Integer.
var
x,v,e : Integer;
inputs : String;
begin
//readln(inputs);
inputs:='1 2 3';
j:= 1;
// make sure that there is one space between two integers
repeat
x:= pos(' ', inputs); // position of the space
delete(inputs, x, 1)
until (x = 0);
x:= pos(' ', inputs); // position of the space
while x <> 0 do
begin
x:= pos(' ', inputs); //(1) '1_2_3' (2) '2_3'
val(copy(inputs, 1, x-1), v, e); // v = value | e = error pos
t[j]:=v;
delete(inputs, 1, x); //(1) '2_3' (2) '3'
j:=j+1; //(1) j = 2 (2) j = 3
//writeln(v);
end;
//j:=j+1; // <--- The mistake were simply here.
val(inputs, v, e);
t[j]:=v;
//writeln(v);
end;
I get this result ( resolved ) :
1
2
0
3
expected :
1
2
3
PS : I'm not very advanced, so excuse me for reducing you to basics.
Thanks for everyone who is trying to share knowledge.
Your code is rather inefficient and it also doesn't work for strings containing numbers in general.
A standard and performant approach would be like this:
type
TIntArr = array of Integer;
function GetNumbers(const S: string): TIntArr;
const
AllocStep = 1024;
Digits = ['0'..'9'];
var
i: Integer;
InNumber: Boolean;
NumStartPos: Integer;
NumCount: Integer;
procedure Add(Value: Integer);
begin
if NumCount = Length(Result) then
SetLength(Result, Length(Result) + AllocStep);
Result[NumCount] := Value;
Inc(NumCount);
end;
begin
InNumber := False;
NumCount := 0;
for i := 1 to S.Length do
if not InNumber then
begin
if S[i] in Digits then
begin
NumStartPos := i;
InNumber := True;
end;
end
else
begin
if not (S[i] in Digits) then
begin
Add(StrToInt(Copy(S, NumStartPos, i - NumStartPos)));
InNumber := False;
end;
end;
if InNumber then
Add(StrToInt(Copy(S, NumStartPos)));
SetLength(Result, NumCount);
end;
This code is intentionally written in a somewhat old-fashioned Pascal way. If you are using a modern version of Delphi, you wouldn't write it like this. (Instead, you'd use a TList<Integer> and make a few other adjustments.)
Try with the following inputs:
521 cats, 432 dogs, and 1487 rabbits
1 2 3 4 5000 star 6000
alpha1beta2gamma3delta
a1024b2048cdef32
a1b2c3
32h50s
5020
012 123!
horses
(empty string)
Make sure you fully understand the algorithm! Run it on paper a few times, line by line.

Swap two numbers in pascal

I'm trying to swap two values but I'm getting a Warning: Local variable "temp" does not seem to be initialized. I want to do it similar as how I've done it. I'm compiling it from the command line with fpc Main.pas. I've tried initializing the temp variable to 0, but it still says Fatal: there were 3 errors compiling module, stopping.
'Main.pas'
Program Main;
procedure Main();
var
n1, n2: Integer;
begin
n1 := 5;
n2 := 10;
Swap(#n1, #n2);
writeln('n1 = ', n1);
writeln('n2 = ', n2);
end;
BEGIN
Main();
END.
'Number.pas'
unit Number;
interface
type
IntPtr = ^Integer;
procedure Swap(n1, n2: IntPtr);
implementation
procedure Swap(n1, n2: IntPtr);
var
temp: Integer;
begin
temp = n1^;
n1^ = n2^;
n2^ = temp;
end;
end.
As you have already discovered, you mixed up the assignment (:=) and equality (=) operators. Thus,
procedure Swap(A, B: PInteger);
var
Temp: Integer;
begin
Temp := A^;
A^ := B^;
B^ := Temp;
end;
where PInteger is defined as ^Integer, does the job:
Swap(#Val1, #Val2); // swaps integers Val1 and Val2
However, I suggest you do this slightly differently:
procedure Swap(var A, B: Integer);
var
Temp: Integer;
begin
Temp := A;
A := B;
B := Temp;
end;
Using a var parameter is more idiomatic and it allows you to write simply
Swap(Val1, Val2); // swaps integers Val1 and Val2
and it also gives you a bit more type safety.

Pascal, reading unknown number of integers

My question is how can I read some number of integers that user enters on standard input, and place them in array.However I don't how many numbers user will enter and i can't ask him that? User enters numbers in one line.
Okay i have just one more answer i would like to add.Thanks all for your help this is code written based on suggestions.I added a line for writting array backwards just for you can see that it has readed well.
program backo;
var niz:array [1..100] of integer;
n, i:integer;
begin
i:=1;
writeln('enter elements of array');
read(niz[i]);
while not eoln do
begin
i:=i+1;
read(niz[i]);
end;
for n:=i downto 1 do
writeln(niz[n]);
end.
Ok, based on comments there is three ways demonstrated:
program readmultiint;
{$mode objfpc}{$H+}
uses
StrUtils;
const
CMaxValues = 3;
var
s: string;
darr: array of Integer;
sarr: array [0..CMaxValues-1] of Integer;
i, cnt: Integer;
begin
// Dynamic array using WordCount
Writeln('Enter values:');
Readln(s);
cnt := WordCount(s, StdWordDelims);
SetLength(darr, cnt); // Allocate room for values
for i := 0 to cnt - 1 do
Val(ExtractWord(i + 1, s, StdWordDelims), darr[i]);
for i in darr do
Writeln(i);
// Dynamic array usin EOLN
SetLength(darr, 0);
Writeln('Enter values:');
while not eoln do
begin
SetLength(darr, Length(darr) + 1); // Expand array for next value
Read(darr[High(darr)]);
end;
Readln; // Read <Enter> itself
for i in darr do
Writeln(i);
// Static array
cnt := 0;
Writeln('Enter values:');
while (not eoln) and (cnt < CMaxValues) do // Reads not more then CMaxValues values
begin
Read(sarr[cnt]);
Inc(cnt);
end;
Readln; // Read <Enter> itself
for i := 0 to cnt-1 do
Writeln(sarr[i]);
end.
Feel free to use one of them or provide your own :)
PS: Some readings:
Dynamic arrays
Val procedure
for-in loop

How many times does one number divide into another, and how much is left over?

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.

Resources