Validation of an ID - loops

Im creating a code that will validate if the user enter all digits as the user is entering an ID. I have a code but I cant figure out how to complete it.
var name:string;
i,c:integer;
ch:char;
begin
name:Edit1.text;
n:length(name);
for i := 0 to n do
begin
ch:=name[i];
//not code//
if ch= any digits then showmessage invalid else showmessage valid;

Your code snippet does not match your question description. You ask about ID validation, but then show code for name validation instead.
In any case, try something like the following:
function IsDigit(c: Char): boolean; inline;
begin
Result := (c >= '0') and (c <= '9');
{
Or Result := CharInSet(c, ['0'..'9']);
Or Result := Character.IsDigit(c);
Or Result := c.IsDigit();
}
end;
Name check:
var
name: String;
i: Integer;
ch: Char;
begin
name := Edit1.Text;
n := Length(name);
for i := 1 to n do
begin
ch := name[i];
if IsDigit(ch) then
begin
ShowMessage('invalid');
Exit;
end;
end;
ShowMessage('valid');
end;
ID check:
var
id: String;
i: Integer;
ch: Char;
begin
id := Edit2.Text;
n := Length(id);
if n = 0 then
begin
ShowMessage('invalid');
Exit;
end;
for i := 1 to n do
begin
ch := id[i];
if not IsDigit(ch) then
begin
ShowMessage('invalid');
Exit;
end;
end;
ShowMessage('valid');
end;
Alternatively:
var
id: Integer; // or Int64
begin
if not TryStrToInt(Edit2.Text, id) then // or TryStrToInt64()
ShowMessage('invalid')
else
ShowMessage('valid');
end;

Related

Delphi 10.4 - Sort a dynamic array of record by 2 values

I am writing a program in Delphi 10.4 that is reading multiple tables from a database into a dynamic array of records. The SQL query already sorts the values by the name during the initial load of the data.
These records are then displayed on a ListView under different columns. I want to give the user the option to click on a column to sort the values according to that column. Up to this point, everything works perfectly fine. I have the current code below, and you are welcome to point out any mistakes I made.
First, I declare the record type:
type
TDDNS = record
ID : Integer; //the ID in the database
Name : String; //the client name
Alias : string; //an alias for the client
Domain : string; //the DDNS address
Login : String; //DDNS login username
Password: string; //DDNS login password
Renewed: TDate; //Date DDNS account was renewed
IsActive: Boolean; //Boolean if account is still active
end;
Secondly, I create the dynamic array:
DDNSDetails : array of TDDNS;
The data is then read into the array.
The Login and Password data is not displayed in the ListView for obvious reasons.
For the sorting, I use the following code:
procedure lvDDNSColumnClick(Sender: TObject;
Column: TListColumn);
begin
SortList(Column.Index);
ReloadLV();
end;
procedure SortList(Col : Integer);
var
i, j : Integer;
begin
if Length(DDNSDetails) > 0 then
begin
for i := 0 to Length(DDNSDetails)-1 do
begin
for j := i+1 to Length(DDNSDetails)-1 do
begin
if Col = 0 then //Name
begin
if UpperCase(DDNSDetails[i].Name) > UpperCase(DDNSDetails[j].Name) then
Resort(i, j);
end else
if Col = 1 then //Alias
begin
if UpperCase(DDNSDetails[i].Alias) > UpperCase(DDNSDetails[j].Alias) then
Resort(i, j);
end else
if Col = 2 then //Domain
begin
if UpperCase(DDNSDetails[i].Domain) > UpperCase(DDNSDetails[j].Domain) then
Resort(i, j);
end else
if (Col = 3) or (Col = 4) then //Renewal date
begin
if DDNSDetails[i].Renewed > DDNSDetails[j].Renewed then
Resort(i, j);
end;
end;
end;
lvDDNS.Columns[0].Caption := 'Client Name';
lvDDNS.Columns[1].Caption := 'Trading As';
lvDDNS.Columns[2].Caption := 'Domain Address';
lvDDNS.Columns[3].Caption := 'Renewed';
lvDDNS.Columns[4].Caption := 'Active';
lvDDNS.Columns[Col].Caption := '|| '+lvDDNS.Columns[Col].Caption+' ||';
end;
end;
procedure Resort(var i, j : Integer);
var
tempInt : Integer;
temp : string;
tempDate : TDate;
tempBool : Boolean;
begin
tempInt := DDNSDetails[i].ID;
DDNSDetails[i].ID := DDNSDetails[j].ID;
DDNSDetails[j].ID := tempInt;
temp := DDNSDetails[i].Name;
DDNSDetails[i].Name := DDNSDetails[j].Name;
DDNSDetails[j].Name := temp;
temp := DDNSDetails[i].Alias;
DDNSDetails[i].Alias := DDNSDetails[j].Alias;
DDNSDetails[j].Alias := temp;
temp := DDNSDetails[i].Domain;
DDNSDetails[i].Domain := DDNSDetails[j].Domain;
DDNSDetails[j].Domain := temp;
tempDate := DDNSDetails[i].Renewed;
DDNSDetails[i].Renewed := DDNSDetails[j].Renewed;
DDNSDetails[j].Renewed := tempDate;
tempBool := DDNSDetails[i].IsActive;
DDNSDetails[i].IsActive := DDNSDetails[j].IsActive;
DDNSDetails[j].IsActive := tempBool;
temp := DDNSDetails[i].Login;
DDNSDetails[i].Login := DDNSDetails[j].Login;
DDNSDetails[j].Login := temp;
temp := DDNSDetails[i].Password;
DDNSDetails[i].Password := DDNSDetails[j].Password;
DDNSDetails[j].Password := temp;
end;
The purpose of this program is to display DDNS records and login credentials for different DDNS accounts and some clients have more than once account.
What happens is, for example, if you sort by the DDNS renewal date, there may be 50 entries for 23/07/2022 and client "f" has 5 entries under that day, however those 5 entries are not together. In the Name column you might see
z
w
g
x
f
z
a
f
.....
The result should be
a
f
f
f
f
f
g
w
x
z
z
.....
The sorting works perfectly for each column selected. I now need to sort the name column as a secondary if the user sorts any other column.
EDIT:
As per a comment by dummzeuch, I changed procedure Resort to the following:
procedure SwapRecord(var i, j : Integer);
var
temp : TDDNS;
begin
temp := DDNSDetails[i];
DDNSDetails[i] := DDNSDetails[j];
DDNSDetails[j] := temp;
end;
If you are using Delphi 10.4 – try to use generic types. Here what I recommend:
type
//declare new type to store sort rule
TSortRule = record
ColumnID : byte; //number of column
Desc : boolean; //reverse sort direction
end;
//change array to list for storing items, it's much esier to work with it
var
xList : TList<TDDNS>;
//we need somehow passed few sort rules, i prefer TList, something like that:
var
xSortOrder : TList<TSortRule>;
Here is procedure for sorting all this staff:
procedure TForm.SortRecords(AList : TList<TDDNS>; ASortOrder : TList<TSortRule>);
begin
AList.Sort(TComparer<TDDNS>.Construct(
function(const Left, Right: TDDNS): Integer
var
LeftValue, RightValue: TDDNS;
begin
//we go for all sorting rules
for var xSortItem in ASortOrder do begin
//check if current rule is reverse
if not xSortItem.Desc then begin
LeftValue := Left;
RightValue := Right;
end else begin
//it's reverse - switch sides
LeftValue := Right;
RightValue := Left;
end{if..else};
//let's do comparation by correct property
case xSortItem.ColumnID of
0: Result := CompareStr(Left.Name, Right.Name);
1: Result := CompareStr(Left.Alias, Right.Alias);
2: Result := CompareStr(Left.Domain, Right.Domain);
3, 4: Result := TComparer<TDate>.Default.Compare(Left.Renewed, Right.Renewed);
end{case};
//if items not equval by this rule, we skip next rules
if Result <> 0 then
break;
end{for};
end
));
end;
More info about sorting of TList<> you can read in oficial doc or Here example
I took my initial code and modified that to get the results below.
Firstly, I created a new class. This is so the associated code can be used elsewhere. The record type, sort procedure and associated code are declared as follows:
type
TRec = record
dbID : Integer;
Name : String;
Alias : string;
Domain : string;
Login : String;
Password: string;
Renewed: TDate;
IsActive: Boolean;
end;
type
TData = array of TRec;
procedure SortData(Data : TData; const Field : Integer);
procedure SwapRecords(var Data : TData; const i, j : Integer);
SortData performs the comparing for the sorting and SwapRecords swaps the entries during the sorting procedure. SortData uses Goto to go to the bottom of the loop once it finds the field it needs to sort in order for it to save time and start with the next cycle.
The procedures are scripted as follows:
procedure SortData(Data : TData; const Field : Integer);
var
n, newn, i : integer;
label
bottom;
begin
n := length(Data);
repeat
newn := 0;
for i := 1 to n-1 do
begin
if Field = 1 then //Name
begin
if UpperCase(Data[i-1].Name) > UpperCase(Data[i].Name) then
begin
SwapRecords(Data, i-1, i);
newn := i;
Goto bottom;
end;
end;
if Field = 2 then //Alias
begin
if UpperCase(Data[i-1].Alias) > UpperCase(Data[i].Alias) then
begin
SwapRecords(Data, i-1, i);
newn := i;
Goto bottom;
end;
end;
if Field = 3 then //Domain
begin
if UpperCase(Data[i-1].Domain) > UpperCase(Data[i].Domain) then
begin
SwapRecords(Data, i-1, i);
newn := i;
Goto bottom;
end;
end;
if Field = 3 then //Login
begin
if UpperCase(Data[i-1].Login) > UpperCase(Data[i].Login) then
begin
SwapRecords(Data, i-1, i);
newn := i;
Goto bottom;
end;
end;
if Field = 4 then //Password
begin
if UpperCase(Data[i-1].Password) > UpperCase(Data[i].Password) then
begin
SwapRecords(Data, i-1, i);
newn := i;
Goto bottom;
end;
end;
if Field = 5 then //Renewed
begin
if Data[i-1].Renewed > Data[i].Renewed then
begin
SwapRecords(Data, i-1, i);
newn := i;
Goto bottom;
end;
end;
if Field = 6 then //IsActive
begin
if Data[i-1].IsActive > Data[i].IsActive then
begin
SwapRecords(Data, i-1, i);
newn := i;
Goto bottom;
end;
end;
bottom:
end;
n := newn;
until n < 1;
end;
procedure SwapRecords(var Data : TData; const i, j : Integer);
var
temp : TRec;
begin
temp := Data[i];
Data[i] := Data[j];
Data[j] := temp;
end;
Finally, in the main form I call this procedure after creating and filling a variable (DDNSInfo of TData).
procedure TfrmDDNS.lvDDNSColumnClick(Sender: TObject;
Column: TListColumn);
var
Field : Integer;
begin
ColActive := Column.Index;//ColActive is a global Integer in the form to keep track of which column is selected in the TListView
case ColActive of
0 : Field := 1;//Client Name
1 : Field := 2;//Trading As
2 : Field := 3;//Domain Address
3 : Field := 6;//Renewed
4 : Field := 7;//Active
else
Field := 0;
end;
//Sort array
if Field = 6 then
begin
SortData(DDNSInfo,1);//Sort according to Client Name
SortData(DDNSInfo,Field);//Sort according to Renewal Date
end else
SortData(DDNSInfo,Field);//Sort only according to selected column
//Output new array
UpdateLV(lvDDNS,DDNSInfo);//This is another procedure for updating the data displayed in the TListView
end;
I set this up to accomodate sorting according to more than one column and this can be expanded to accomodate more scenarios, such as websites, other types of passwords, etc.

Geting SQL Server instances names from registry

I have tried to get my SQLServer instances names from the registry by using this code:
type
TRegistryHelper = class helper for TRegistry
public
function ReadMultiSz(const name: string; var Strings: TStrings): boolean;
end;
function TRegistryHelper.ReadMultiSz(const name: string;
var Strings: TStrings): boolean;
var
iSizeInByte: integer;
Buffer: array of WChar;
iWCharsInBuffer: integer;
z: integer;
sString: string;
begin
iSizeInByte := GetDataSize(name);
if iSizeInByte > 0 then begin
SetLength(Buffer, Floor(iSizeInByte / sizeof(WChar)));
iWCharsInBuffer := Floor(ReadBinaryData(name, Buffer[0], iSizeInByte) / sizeof(WChar));
sString := '';
for z := 0 to iWCharsInBuffer do begin
if Buffer[z] <> #0 then begin
sString := sString + Buffer[z];
end else begin
if sString <> '' then begin
Strings.Append(sString);
sString := '';
end;
end;
end;
result := true;
end else begin
result := false;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
const
cKey = '\SOFTWARE\Microsoft\Microsoft SQL Server';
var
Registry: TRegistry;
MyList: TStrings;
begin
Registry := TRegistry.Create;
Registry.RootKey := HKEY_LOCAL_MACHINE;
if Registry.OpenKeyReadOnly(cKey) then
try
MyList := TStringList.Create();
Registry.ReadMultiSz('InstalledInstances', MyList);
ListBox1.Items.Assign(MyList);
finally
MyList.Free;
end;
Registry.Free;
end;
But I have noted the iSizeInByte = -1 every time, and I failed to get those names by this way.
Also, I have noted that when trying to get those instances from my TADOQuery connection string builder, the component also failed to get those names.
Is there any way to get them?

Is there a way to switch components mid-loop?

I am trying to animate a dropdown menu of 7 images using a for loop, using a different component to change after each iteration of the code inside the for loop. For example, the first time the loop runs: imgCourses7.Top is used, but the second time the loop runs (when I = 1) then imgCourses6.Top should be used instead.
iCoursesCount := 7;
iTotalLength := (6+41)*iCoursesCount;
imgCourses7.Top := 6;
imgCourses6.Top := 6;
imgCourses5.Top := 6;
imgCourses4.Top := 6;
imgCourses3.Top := 6;
imgCourses2.Top := 6;
imgCourses1.Top := 6;
for I := 0 to iCoursesCount -1 do
begin
while not(imgCourses7.Top = iTotalLength - 41*(I+1)) do
begin
imgCourses8.Top := imgCourses8.Top + 6;
sleep(8);
application.ProcessMessages;
if imgCourses7.Top >= iTotalLength - 41*(I+1) then
begin
imgCourses7.Top := iTotalLength - 41*(I+1);
break;
end;
end;
end;
Like #AndreasRejbrand said in a comment, you can use an array, eg:
var
Images: array[0..6] of TImage;
TargetTop: Integer;
...
...
Images[0] := imgCourses7;
Images[1] := imgCourses6;
Images[2] := imgCourses5;
Images[3] := imgCourses4;
Images[4] := imgCourses3;
Images[5] := imgCourses2;
Images[6] := imgCourses1;
iCoursesCount := Length(Images);
TargetTop := 6+(41*iCoursesCount);
for I := 0 to iCoursesCount-1 do begin
Images[I].Top := 6;
end;
for I := 0 to iCoursesCount-1 do
begin
Dec(TargetTop, 41);
while Images[I].Top <> TargetTop do
begin
Images[I].Top := Images[I].Top + 6;
Sleep(8);
Application.ProcessMessages;
if Images[I].Top >= TargetTop then
begin
Images[I].Top := TargetTop;
Break;
end;
end;
end;
That being said, you really shouldn't be using a sleeping loop that requires Application.ProcessMessages() on each iteration. You might consider using a TTimer or TThread.ForceQueue() instead. Don't block the main UI thread unnecessarily. For example:
published
procedure FormCreate(Sender: TObject);
private
Images: array[0..6] of TImage;
TargetTop: Integer;
CurrentImage: Integer;
procedure StartAnimatingMenu;
procedure StartAnimatingNextMenuItem;
procedure StepAnimateCurrentMenuItem;
...
...
procedure TMyForm.FormCreate(Sender: TObject);
begin
Images[0] := imgCourses7;
Images[1] := imgCourses6;
Images[2] := imgCourses5;
Images[3] := imgCourses4;
Images[4] := imgCourses3;
Images[5] := imgCourses2;
Images[6] := imgCourses1;
end;
procedure TMyForm.StartAnimatingMenu;
var
I: Integer;
begin
for I := Low(Images) to High(Images) do begin
Images[I].Top := 6;
end;
TargetTop := 6+(41*Length(Images));
CurrentImage := -1;
StartAnimatingNextMenuItem;
end;
procedure TMyForm.StartAnimatingNextMenuItem;
begin
Inc(CurrentImage);
if CurrentImage < Length(Images) then
begin
Dec(TargetTop, 41);
StepAnimateCurrentMenuItem;
end;
end;
procedure TMyForm.StepAnimateCurrentMenuItem;
begin
if Images[CurrentImage].Top <> TargetTop then
begin
Images[CurrentImage].Top := Images[CurrentImage].Top + 6;
TThread.ForceQueue(nil, StepAnimateCurrentMenuItem, 8);
end
else if Images[CurrentImage].Top >= TargetTop then
begin
Images[CurrentImage].Top := TargetTop;
StartAnimatingNextMenuItem;
end;
end;

How to make array distinct

I have an array of integer and string fields. To make it distinct I currently copy line by line into new array and with each record I check if the record already exists in new, if not I copy it. At the end I copy new array back to original.
It works, but is slow. Is there any faster, easier way to do this?
TArrayMixed = record
Field1: integer;
Field2: integer;
Field3: integer;
Field4: string;
Field5: string;
Field6: string;
end;
procedure TForm1.Button10Click(Sender: TObject);
var
ArrayMixed, ArrayMixed_tmp: array of TArrayMixed;
i, j, vIdx: integer;
vExists: boolean;
begin
SetLength(ArrayMixed, 100000);
for i := 0 to 99999 do
begin
ArrayMixed[i].Field1 := 1 + Random(5);
ArrayMixed[i].Field2 := 1 + Random(5);
ArrayMixed[i].Field3 := 1 + Random(5);
ArrayMixed[i].Field4 := 'String';
ArrayMixed[i].Field5 := 'Another string';
ArrayMixed[i].Field6 := 'New string';
end;
// Sort
TArray.Sort<TArrayMixed > (ArrayMixed, TComparer<TArrayMixed > .Construct(function(const Left, Right: TArrayMixed): Integer
begin
Result := MyCompareAMixed(Left, Right);
end
));
// Distinct
SetLength(ArrayMixed_tmp, Length(ArrayMixed));
vIdx := 0;
for i := Low(ArrayMixed) to High(ArrayMixed) do
begin
vExists := False;
for j := Low(ArrayMixed_tmp) to vIdx - 1 do
if (ArrayMixed_tmp[j].Field1 = ArrayMixed[i].Field1) and
(ArrayMixed_tmp[j].Field2 = ArrayMixed[i].Field2) and
(ArrayMixed_tmp[j].Field3 = ArrayMixed[i].Field3) and
(ArrayMixed_tmp[j].Field4 = ArrayMixed[i].Field4) and
(ArrayMixed_tmp[j].Field5 = ArrayMixed[i].Field5) and
(ArrayMixed_tmp[j].Field6 = ArrayMixed[i].Field6) then
begin
vExists := True;
Break;
end;
if not vExists then
begin
ArrayMixed_tmp[vIdx] := ArrayMixed[i];
Inc(vIdx);
end;
end;
SetLength(ArrayMixed_tmp, vIdx);
// now copy back to original array
SetLength(ArrayMixed, 0);
SetLength(ArrayMixed, Length(ArrayMixed_tmp));
for i := Low(ArrayMixed_tmp) to High(ArrayMixed_tmp) do
ArrayMixed[i] := ArrayMixed_tmp[i];
sleep(0);
end;
Edit:
Since in real data the strings are not all the same, the part where it makes distinct array is slower when original array is filled like this:
Edit #2: (copied wrong code in Edit #1)
for i := 0 to 999999 do
begin
ArrayMixed[i].Field1 := 1 + Random(5);
ArrayMixed[i].Field2 := 1 + Random(5);
ArrayMixed[i].Field3 := 1 + Random(5);
ArrayMixed[i].Field4 := 'String'+IntToStr(i mod 5);
ArrayMixed[i].Field5 := 'Another string'+IntToStr(i mod 5);
ArrayMixed[i].Field6 := 'New string'+IntToStr( i mod 5);
end;
Edit #3: Publishing code for sorting - only first 3 fields are sorted!
TMyArray3 = array[1..3] of Integer;
function CompareIntegerArray3(const lhs, rhs: TMyArray3): Integer;
var
i: Integer;
begin
Assert(Length(lhs) = Length(rhs));
for i := low(lhs) to high(lhs) do
if lhs[i] < rhs[i] then
exit(-1)
else if lhs[i] > rhs[i] then
exit(1);
exit(0);
end;
function GetMyArrayAMixed(const Value: TArrayMixed): TMyArray3;
begin
Result[1] := Value.Field1;
Result[2] := Value.Field2;
Result[3] := Value.Field3;
end;
function MyCompareAMixed(const lhs, rhs: TArrayMixed): Integer;
begin
Result := CompareIntegerArray3(GetMyArrayAMixed(lhs), GetMyArrayAMixed(rhs));
end;
Implement some methods (equality check, hashcode) for the record to get away with a lot of boilerplate code
type
TArrayMixed = record
Field1: integer;
Field2: integer;
Field3: integer;
Field4: string;
Field5: string;
Field6: string;
class operator Equal( const a, b: TArrayMixed ): Boolean;
class function Compare( const L, R: TArrayMixed ): integer; overload; static;
function Compare( const Other: TArrayMixed ): integer; overload;
function GetHashCode( ): integer;
end;
{ TArrayMixed }
class function TArrayMixed.Compare( const L, R: TArrayMixed ): integer;
begin
Result := L.Compare( R );
end;
function TArrayMixed.Compare( const Other: TArrayMixed ): integer;
begin
Result := Field1 - Other.Field1;
if Result = 0
then
begin
Result := Field2 - Other.Field2;
if Result = 0
then
begin
Result := Field3 - Other.Field3;
if Result = 0
then
begin
Result := CompareStr( Field4, Other.Field4 );
if Result = 0
then
begin
Result := CompareStr( Field5, Other.Field5 );
if Result = 0
then
begin
Result := CompareStr( Field6, Other.Field6 );
end;
end;
end;
end;
end;
end;
class operator TArrayMixed.Equal( const a, b: TArrayMixed ): Boolean;
begin
Result := true
{} and ( a.Field1 = b.Field1 )
{} and ( a.Field2 = b.Field2 )
{} and ( a.Field3 = b.Field3 )
{} and ( a.Field4 = b.Field4 )
{} and ( a.Field5 = b.Field5 )
{} and ( a.Field6 = b.Field6 );
end;
function TArrayMixed.GetHashCode: integer;
begin
{$IFOPT Q+}
{$Q-}
{$DEFINE SET_Q_ON}
{$ENDIF}
Result := 17;
Result := Result * 31 + Field1;
Result := Result * 31 + Field2;
Result := Result * 31 + Field3;
Result := Result * 31 + Field4.GetHashCode;
Result := Result * 31 + Field5.GetHashCode;
Result := Result * 31 + Field6.GetHashCode;
{$IFDEF SET_Q_ON}
{$Q+}
{$UNDEF SET_Q_ON}
{$ENDIF}
end;
Use a TDictionary as a HashSet to check for duplicates
procedure Test;
var
arr1, arr2: TArray<TArrayMixed>;
idx : integer;
lst : TDictionary<TArrayMixed, integer>;
begin
// fill the array
SetLength( arr1, 100000 );
for idx := low( arr1 ) to high( arr1 ) do
begin
arr1[ idx ].Field1 := 1 + Random( 5 );
arr1[ idx ].Field2 := 1 + Random( 5 );
arr1[ idx ].Field3 := 1 + Random( 5 );
arr1[ idx ].Field4 := 'String' + IntToStr( idx mod 5 );
arr1[ idx ].Field5 := 'Another string' + IntToStr( idx mod 5 );
arr1[ idx ].Field6 := 'New string' + IntToStr( idx mod 5 );
end;
// distinct
lst := TDictionary<TArrayMixed, integer>.Create( TEqualityComparer<TArrayMixed>.Construct(
function( const L, R: TArrayMixed ): Boolean
begin
Result := ( L = R );
end,
function( const i: TArrayMixed ): integer
begin
Result := i.GetHashCode( );
end ) );
try
for idx := low( arr1 ) to high( arr1 ) do
begin
lst.AddOrSetValue( arr1[ idx ], 0 );
end;
arr2 := lst.Keys.ToArray;
finally
lst.Free;
end;
end;
I would do something like this. You basically create the result on the fly and sort at the same time using a binary search to remove the duplicates.
function RemoveDuplicates(aSourceArray: TArray<TArrayMixed>): TArray<TArrayMixed>;
var
i: Integer;
index: Integer;
sortList: TList<TArrayMixed>;
begin
sortList := TList<TArrayMixed>.Create;
try
for i := Low(aSourceArray) to High(aSourceArray) do
begin
if not sortList.BinarySearch(aSourceArray[i], index,
TDelegatedComparer<TArrayMixed>.Construct(
function(const L, R: TArrayMixed): integer
begin
Result := L.Field1 - R.Field1;
if Result <> 0 then Exit;
Result := L.Field2 - R.Field2;
if Result <> 0 then Exit;
Result := L.Field3 - R.Field3;
if Result <> 0 then Exit;
Result := CompareStr(L.Field4, R.Field4);
if Result <> 0 then Exit;
Result := CompareStr(L.Field5, R.Field5);
if Result <> 0 then Exit;
Result := CompareStr(L.Field6, R.Field6);
end)) then
begin
sortList.Insert(index, aSourceArray[i]);
end;
end;
Result := sortList.ToArray;
finally
sortList.Free;
end;
end;
To use this code you could do something like this:
procedure TForm1.Button10Click(Sender: TObject);
var
ArrayMixed, ArrayMixed_tmp: TArray<TArrayMixed>;
i: Integer;
begin
SetLength(ArrayMixed, 100000);
for i := 0 to 999999 do
begin
ArrayMixed[i].Field1 := 1 + Random(5);
ArrayMixed[i].Field2 := 1 + Random(5);
ArrayMixed[i].Field3 := 1 + Random(5);
ArrayMixed[i].Field4 := 'String'+IntToStr(i mod 5);
ArrayMixed[i].Field5 := 'Another string'+IntToStr(i mod 5);
ArrayMixed[i].Field6 := 'New string'+IntToStr( i mod 5);
end;
ArrayMixed_tmp := RemoveDuplicates(ArrayMixed);
end;
Just check for duplicates next to the prior index, since the array is sorted. Here is the sorting comparer reused as well.
function RemoveDuplicates(const anArray: array of TArrayMixed): TArray<TArrayMixed>;
var
j, vIdx: integer;
begin
// Sort
TArray.Sort<TArrayMixed > (anArray, TComparer<TArrayMixed >.Construct(function(const Left, Right: TArrayMixed): Integer
begin
Result := MyCompareAMixed(Left, Right);
end
));
// Distinct
SetLength(Result, Length(anArray));
vIdx := 0;
j := 0;
while (j <= High(anArray) do
begin
Result[vIdx] := anArray[j];
Inc(j);
While (j <= High(anArray)) and (MyCompareAMixed(Result[vIdx],anArray[j]) = 0) do
Inc(j);
Inc(vIdx);
end;
SetLength(Result, vIdx);
end;
Update:
In an update to the question it is stated that the array is only partially sorted. One way to reduce the number of iterations to remove duplicates would then be to:
Find start and stop index to items that share the first sorting criteria.
Iterate among them to sort out duplicates.
Since you already sort ArrayMixed you don't need to compare each item with each other in order to find duplicates. The duplicates are already placed next to each other. So you simply need to iterate over ArrayMixed and compare the current item to the last item in ArrayMixed_tmp.
Hence, copying the distinct items can be proceed much faster and looks like that:
vIdx := 0;
for i := Low(ArrayMixed) to High(ArrayMixed) do
begin
if (vIdx = 0) or // the first item can never be a duplicate
(ArrayMixed_tmp[vIdx].Field1 <> ArrayMixed[i].Field1) or
(ArrayMixed_tmp[vIdx].Field2 <> ArrayMixed[i].Field2) or
(ArrayMixed_tmp[vIdx].Field3 <> ArrayMixed[i].Field3) or
(ArrayMixed_tmp[vIdx].Field4 <> ArrayMixed[i].Field4) or
(ArrayMixed_tmp[vIdx].Field5 <> ArrayMixed[i].Field5) or
(ArrayMixed_tmp[vIdx].Field6 <> ArrayMixed[i].Field6) then
begin
ArrayMixed_tmp[vIdx] := ArrayMixed[i];
Inc(vIdx);
end;
end;
You have not posted the code for your MyCompareAMixed() function so it is not possible to test the performance of your actual code including this undefined function, including the current sort performance.
However, since your posted duplicate detection approach is not dependent upon the array being sorted I simply removed the sort from the code.
Without any further optimisation the resulting de-duplication process completed in well under 50 msecs, which is not "slow" in my book for de-duping 100,000 complex items. i.e. not single values but items that are records of multiple values.
If the sort is necessary for other reasons then you could retain the sorting and optimise the de-dupe process based on the answers given by others, but I would first question why you think the process is slow and if sub 50 msecs really is slow, what is the target you are aiming at ?
It is possible that it is the sorting that is adding the overhead (as I say, without your compare function we cannot quantify the overhead this is adding) so if this sorting is not necessary for other reasons and if sub 50-msecs is acceptable for de-duping this array, then I would move on to other tasks.

Local string array initialization

I have sporadic problems (access violation in unit System).
Application is running 24 x 7 and it happens one - two times in a week.
I have a procedure with local string array, and I found there are cases I assign non initialised array member to string variable like in code below.
Could it be a reason for access violation?
procedure tform1.DoSomething;
var
sarr: array [1..4] of String;
s: String;
begin
sarr[1] := 'aaaa';
sarr[2] := 'bbbb';
s := sarr[3]; // Can I get here an access violation???
end;
Real function code below
exception happens when obj.opcode = cmdp_done
function is called from thread message queue. obj is created in another thread, and
sent in PostThreadMessage as msg.lparam
procedure ORTR_ProcessFiscalResponse(obj: TDataForOrpak);
const
maxdim = 4;
var
s: array [1..maxdim] of String;
i, n, fiscalNr, statusToSend: Integer;
sql, pdf, ortrName, docType, rut: String;
ortr: TCustomizedOrtr;
oFiscal: TFiscalDevice;
fpds: TFPDeviceState;
begin
try
case obj.opcode of
cmdp_status: N := 3;
cmdp_done: N := 2;
plcl_docissued: N := 4;
plcl_docfailed: N := 1;
else
Exit;
end;
for i:=1 to n do
s[i] := GetTerm(obj.ident, i, ';');
if s[1] = '' then
Exit;
statusToSend := 0;
ortrName := GetTerm(s[1], 1, '+');
fiscalNr := StrToIntDef(GetTerm(s[1]+'+', 2, '+'), 999999);
docType := s[3];
rut := s[4];
ortr := TCustomizedOrtr.GetTerminalByName(ortrName) as TCustomizedOrtr;
if ortr = nil then
Exit;
if (ortr.FPState = fps_idle) or (ortr.fiscalNr <> fiscalNr) then begin
if (StrToIntDef(s[2], 0) <> 0) and (obj.opcode = cmdp_done) then
fiscal_Confirm(obj.otherdevname, obj.ident);
if obj.opcode = plcl_docissued then begin
try
PLCL_SetDocState(s[1], rut, false, StrToInt(s[2]), StrToInt(docType));
except
AddToLogFile('*** PLCL_SetDocState', log_exceptions);
end;
end;
Exit;
end;
oFiscal := fiscal_Assigned(ortr.ctlPump.PumpID) as TFiscalDevice;
case obj.opcode of
plcl_docissued:
begin
ortr.authData.ECRReceiptNr := s[2];
pdf := StringFromHexPresentation(obj.rawdata);
sql := format(sql_PaperlessAdd, [
ToLocalSQL_DateTime(ortr.ctlPump.FinalTime),
ToLocalSQL_Integer(ortr.ctlPump.PumpID),
ToLocalSQL_String(pdf)]);
try
UpdateLocalDB(sql);
except
AddToLogFile('*** PL save pdf', log_exceptions);
end;
try
PLCL_SetDocState(s[1], rut, true, StrToInt(s[2]), StrToInt(docType));
except
AddToLogFile('*** PLCL_SetDocState', log_exceptions);
end;
ortr.FPState := fps_idle;
ortr.currStage := TTextIndexType(0);
ortr.currStage := tivirt_towelcome; // VADIM
ExternalProcessPumpState(ortr.authData.gsPump.PumpID);
end;
plcl_docfailed:
ortr.FPState := fps_plerror;
cmdp_status:
begin
ortr.FPError := StrToIntDef(s[3], 0);
fpds := TFPDeviceState(StrToIntDef(s[2], 0));
ortr.FPState := fpds;
if (fpds in [fps_nocomm, fps_error]) and (ortr.fiscalMode = 1) and
(ortr.authData = nil) and (ortr.fiscalNr < 0) then
SpecialInterface.SendFiscalNrToPromax(-ortr.fiscalNr, '0');
case fpds of
fps_nopaper: statusToSend := wph_prnpaperout;
fps_nocomm: statusToSend := wph_prncommfailure;
fps_error: statusToSend := wph_prngenericfailure;
end;
end;
cmdp_done:
begin
if ortr.fiscalMode = 1 then begin
if ortr.authData = nil then begin // DRY GOOD
SpecialInterface.SendFiscalNrToPromax(-fiscalNr, s[2]);
end
else begin
ortr.authData.ECRReceiptNr := s[2];
ExternalProcessPumpState(ortr.authData.gsPump.PumpID);
end
end;
if StrToIntDef(s[2], 0) <> 0 then
fiscal_Confirm(obj.otherdevname, obj.ident);
statusToSend := wph_prnidle;
ortr.FPState := fps_idle;
ortr.currStage := ti_takereceipt;
end;
end;
if (statusToSend <> 0) and (oFiscal <> nil) then
PostNonVisualCommand(nv_devicestate, statusToSend, Integer(oFiscal));
finally
obj.free;
end;
end;
Your initial piece of code, the tform1.DoSomething routine, is unable of producing an access violation:
For the static array variable sarr, memory is allocated for all its elements.
The string variable s, as well as the elements in sarr, are automatically initialized to empty. 1
Thus you are simply assigning an empty string, and s remains empty.
Concerning your actual code, assuming that it does produce the access violation, my guess would be that:
the obj parameter still refers to an already destroyed object,
that obj.opcode reads an invalid piece of memory, but since it is compared to an numerical value, will pass,
that Exit is called in de case else clause, and
that obj.Free fails in the finally clause.
1 All string variables are initilized to empty, except string function results:
If the function exits without assigning a value to Result or the function name, then the function's return value is undefined.
The missing compiler warning is still a bug.

Resources