Is there a function in the Delphi standard library to search string arrays for a particular value?
e.g.
someArray:=TArray<string>.Create('One','Two','Three');
if ArrayContains(someArray, 'Two') then
ShowMessage('It contains Two');
There is absolutely no need to reinvent the wheel. StrUtils.MatchStr does the job.
procedure TForm1.FormCreate(Sender: TObject);
var
someArray: TArray<string>;
begin
someArray:=TArray<string>.Create('One','Two','Three');
if MatchStr('Two', someArray) then
ShowMessage('It contains Two');
end;
Note the parameter order convention.
Another note: MatchStr is a canonicalized name assigned to this function somewhen in between Delphi 7 and Delphi 2007. Historical name is AnsiMatchStr (convention is the same as in the rest of RTL: Str/Text suffix for case-sensitivity, Ansi prefix for MBCS/Locale)
I wrote one I modeled after the old Clipper AScan function (tested in XE). #RRUZ's answer is more correct (there is one existing), but mine doesn't require the array to be sorted first and is fast enough on small arrays. (It also works in pre-generics versions of Delphi.) I also overload it for various types of array - here are the implementations for string and integer:
// Returns the 0-based index of Value if it's found in the array,
// -1 if not. (Similar to TStrings.IndexOf)
function AScan(const Ar: array of string; const Value: string): Integer; overload;
var
i: Integer;
begin
Result := -1;
for i := Low(Ar) to High(Ar) do
if SameText(Ar[i], Value) then
begin
Result := i;
Break
end;
end;
function AScan(const Ar: array of Integer; const Value: Integer): Integer; overload;
var
i: Integer;
begin
Result := -1;
for i := Low(Ar) to High(Ar) do
if (Ar[i] = Value) then
begin
Result := i;
Break
end;
end;
procedure TForm2.FormShow(Sender: TObject);
var
someStrArray: TArray<string>;
someIntArray: TArray<Integer>;
Idx: Integer;
begin
someStrArray := TArray<string>.Create('One', 'Two', 'Three');
Idx := AScan(someStrArray, 'Two');
if Idx > -1 then
ShowMessage(Format('It contains Two at index %d', [Idx]))
else
ShowMessage('Not found');
someIntArray := TArray<Integer>.Create(8, 16, 32);
Idx := AScan(someIntArray, 32);
if Idx > -1 then
ShowMessage(Format('It contains 32 at %d', [Idx]))
else
ShowMessage('16 not found');
end;
For versions of Delphi that support generics, here's a version that doesn't require the array to be sorted, and that also allows you to provide the comparison function if needed:
Interface:
type
TGenericsUtils = class
public
class function AScan<T>(const Arr: array of T; const Value: T; const Comparer: IEqualityComparer<T>): Integer; overload;
class function AScan<T>(const Arr: array of T; const Value: T): Integer; overload;
end;
Implementation
class function TGenericsUtils.AScan<T>(const Arr: array of T; const Value: T): Integer;
begin
Result := AScan<T>(Arr, Value, TEqualityComparer<T>.Default);
end;
class function TGenericsUtils.AScan<T>(const Arr: array of T; const Value: T;
const Comparer: IEqualityComparer<T>): Integer;
var
i: Integer;
begin
for i := Low(Arr) to High(Arr) do
if Comparer.Equals(Arr[i], Value) then
Exit(i);
Exit(-1);
end;
Test code:
var
AIntTest: TIntegerDynArray;
AStrTest: TStringDynArray;
begin
AIntTest := TIntegerDynArray.Create(12, 15, 6, 1, 4, 9, 5);
AStrTest := TStringDynArray.Create('One', 'Six', 'Three', 'Four', 'Twelve');
WriteLn('AIntTest contains 9 at index ', TGenericsUtils.AScan<Integer>(AIntTest, 9));
WriteLn('AStrTest contains ''Four'' at index ', TGenericsUtils.AScan<String>(AStrTest, 'Four'));
ReadLn;
end.
you can use the TArray.BinarySearch function, which is part of the Generics.Collections unit.
check this sample
{$APPTYPE CONSOLE}
{$R *.res}
uses
Generics.Defaults,
Generics.Collections,
System.SysUtils;
Var
someArray: TArray<string>;
FoundIndex : Integer;
begin
try
someArray:=TArray<string>.Create('a','b','c');
if TArray.BinarySearch<String>(someArray, 'b', FoundIndex, TStringComparer.Ordinal) then
Writeln(Format('Found in index %d',[FoundIndex]))
else
Writeln('Not Found');
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
Note: BinarySearch requires that the array be sorted.
Related
I'm trying to iterate over a Dynamic array passed into a generic function
I'm using TValue to achive this, but i can't get the length of the array and there for I can not get the elements.
I've written a small demo project to illustrate my problem:
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, System.TypInfo, System.Rtti;
type
TMyEnum = (me_One, me_Two, me_Three);
Writer = class
public
class procedure Write<T>(Value: T);
end;
{ Writer }
class procedure Writer.Write<T>(Value: T);
var
ArrayValue: TValue;
TypeInfo: pTypeInfo;
begin
TypeInfo := System.TypeInfo(T);
if TypeInfo.Kind <> tkDynArray then
exit;
TValue.Make(nil, TypeInfo, ArrayValue);
Writeln(ArrayValue.GetArrayLength);
//Here I have my problem ArrayValue.GetArrayLength returns 0 and not 2
end;
var
Enums: array of TMyEnum;
Dummy : String;
begin
try
SetLength(Enums, 2);
Enums[0] := me_One;
Enums[1] := me_Two;
Writer.Write(Enums);
Readln(Dummy);
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
I found the answer my self.
The problem where I was using TValue.Make and not TValue.From
This will do. Way more simple compared to where I started.
class function Writer.WriteLine<T>(AValue: T): string;
var
ElementValue, Value: TValue;
i: Integer;
begin
Value := TValue.From(AValue);
try
if not Value.IsArray then
Exit('');
if Value.GetArrayLength = 0 then
Exit('[]');
Result := '[';
for i := 0 to Value.GetArrayLength - 1 do
begin
ElementValue := Value.GetArrayElement(i);
Result := Result + ElementValue.ToString + ',';
end;
Result[Length(Result)] := ']';
finally
Writeln(Result);
end;
end;
Let suppose I have dynamic array
type TCharArr = Array of byte;
type PcharArr = ^TCharArr;
var charArr: PcharArr;
Which I want to allocate memory in Heap in the way of New(charArr);
However, how can I specify size and indexes? Is it possible dynamic array to have indexes eg. from 512.. to 1024?
Assuming a more recent Delphi version, you can mimic that with a generic record:
type
TDynArray<T> = record
private
FData: TArray<T>;
FOffset: Integer;
function GetData(Index: Integer): T;
function GetHigh: Integer;
function GetLength: Integer;
function GetLow: Integer;
procedure SetData(Index: Integer; const Value: T);
public
constructor Create(ALow, AHigh: Integer);
property Data[Index: Integer]: T read GetData write SetData; default;
property High: Integer read GetHigh;
property Length: Integer read GetLength;
property Low: Integer read GetLow;
end;
constructor TDynArray<T>.Create(ALow, AHigh: Integer);
begin
FOffset := ALow;
SetLength(FData, AHigh - ALow + 1);
end;
function TDynArray<T>.GetData(Index: Integer): T;
begin
Result := FData[Index - FOffset];
end;
function TDynArray<T>.GetHigh: Integer;
begin
Result := FOffset + System.High(FData);
end;
function TDynArray<T>.GetLength: Integer;
begin
Result := System.Length(FData);
end;
function TDynArray<T>.GetLow: Integer;
begin
Result := FOffset;
end;
procedure TDynArray<T>.SetData(Index: Integer; const Value: T);
begin
FData[Index - FOffset] := Value;
end;
The usage could look then like this:
var
arr: TDynArray<Integer>;
I: Integer;
begin
arr := TDynArray<Integer>.Create(512, 1024);
for I := arr.Low to arr.High do
arr[I] := I;
for I := arr.Low to arr.High do
Writeln(I, '=', arr[I]);
Readln;
end;
Dynamic arrays are always zero based. If you want to use array indices with a different base, then you would need to encapsulate the array access accounting for the offset to the indices. Something like this:
const
Offset = 512;
function GetValue(Index: Integer): Byte;
begin
Result := Arr[Index - Offset];
end;
procedure SetValue(Index: Integer; Value: Byte);
begin
Arr[Index - Offset] := Value;
end;
In addition there is the concept of a sparse array (sparse matrix). Delphi does not support it out of the box, but there were implementations in TurboPower SysTools, if I remember correctly.
The source was put on SourceForge, when the company closed about 15 years ago:
https://sourceforge.net/projects/tpsystools/
But these have not been updated for a looooong time.
This also seems to be the same library, maybe a bit more up to date:
https://github.com/TurboPack/SysTools
My code is:
procedure TfrmSettings.btnFillDictClick(Sender: TObject);
var
Dict: TDictionary<string, string>;
Item: TPair<string, string>;
begin
Dict := TDictionary<string, string>.Create();
Dict.Add('Key1', 'Text1');
Dict.Add('Key2', 'Text2');
Dict.Add('Key3', 'Text3');
Dict.Add('Key4', 'Text4');
for Item in Dict do
begin
ShowMessage(Item.Key + ' ' + Item.Value);
end;
end;
Why almost every time I'm getting a different value in Showmessage?
Why values are not stored in the order in which they were added?
I'm a noob in Delphi and do not know how Dictionary is working. And I didn't find any information about this in Google.
Could you please explain me why it is so?
Is there any way to use Dictionary without using TList<> for sort data?
Thanks
Dictionary does not maintain order of elements because the way it is internally organized as look up table and it is ordered by the hash of the key. They are optimized for speed and not to preserve ordering.
If you need to maintain order of elements you need pair list instead of dictionary. Delphi does not provide that out of the box. You can use following code to implement simple pair list and customize it for your needs.
type
TPairs<TKey, TValue> = class(TList < TPair < TKey, TValue >> )
protected
fKeyComparer: IComparer<TKey>;
fValueComparer: IComparer<TValue>;
function GetValue(Key: TKey): TValue;
procedure SetValue(Key: TKey; const Value: TValue);
function ComparePair(const Left, Right: TPair<TKey, TValue>): Integer;
public
constructor Create; overload;
procedure Add(const aKey: TKey; const aValue: TValue); overload;
function IndexOfKey(const aKey: TKey): Integer;
function ContainsKey(const aKey: TKey): Boolean; inline;
property Values[Key: TKey]: TValue read GetValue write SetValue;
end;
constructor TPairs<TKey, TValue>.Create;
begin
if fKeyComparer = nil then fKeyComparer := TComparer<TKey>.Default;
if fValueComparer = nil then fValueComparer := TComparer<TValue>.Default;
inherited Create(TDelegatedComparer <TPair<TKey, TValue>>.Create(ComparePair));
end;
function TPairs<TKey, TValue>.ComparePair(const Left, Right: TPair<TKey, TValue>): Integer;
begin
Result := fKeyComparer.Compare(Left.Key, Right.Key);
if Result = 0 then Result := fValueComparer.Compare(Left.Value, Right.Value);
end;
function TPairs<TKey, TValue>.IndexOfKey(const aKey: TKey): Integer;
var
i: Integer;
begin
Result := -1;
for i := 0 to Count - 1 do
if fKeyComparer.Compare(Items[i].Key, aKey) = 0 then
begin
Result := i;
break;
end;
end;
function TPairs<TKey, TValue>.ContainsKey(const aKey: TKey): Boolean;
begin
Result := IndexOfKey(aKey) >= 0;
end;
function TPairs<TKey, TValue>.GetValue(Key: TKey): TValue;
var
i: Integer;
begin
i := IndexOfKey(Key);
if i >= 0 then Result := Items[i].Value
else Result := default (TValue);
end;
procedure TPairs<TKey, TValue>.SetValue(Key: TKey; const Value: TValue);
var
i: Integer;
Pair: TPair<TKey, TValue>;
begin
i := IndexOfKey(Key);
if i >= 0 then FItems[i].Value := Value
else
begin
Pair.Create(Key, Value);
inherited Add(Pair);
end;
end;
procedure TPairs<TKey, TValue>.Add(const aKey: TKey; const aValue: TValue);
begin
SetValue(aKey, aValue);
end;
And then you can use it the same way you would use dictionary, but order of elements will be maintained.
var
Pairs: TPairs<string, string>;
Item: TPair<string, string>;
begin
Pairs := TPairs<string, string>.Create();
Pairs.Add('Key1', 'Text1');
Pairs.Add('Key2', 'Text2');
Pairs.Add('Key3', 'Text3');
Pairs.Add('Key4', 'Text4');
Pairs.Add('Key5', 'Text5');
for Item in Pairs do
begin
Memo1.Lines.Add(Item.Key + ' ' + Item.Value);
end;
end;
SetValue update for newer Delphi versions where FItems is not available in TList<T> descendant classes.
procedure TPairs<TKey, TValue>.SetValue(Key: TKey; const Value: TValue);
var
i: Integer;
Pair: TPair<TKey, TValue>;
begin
i := IndexOfKey(Key);
if i >= 0 then
begin
Pair := Items[i];
Pair.Value := Value;
Items[i] := Pair;
end
else
begin
Pair.Create(Key, Value);
inherited Add(Pair);
end;
end;
It is possible to create a function which accepts variable number of arguments:
function f(const x: array of const): String;
and use it this way:
f([1,3,4, "hello"]);
It is also possible to define an argument as "changeable":
function g(var x: Byte): String;
var B: Byte;
g(B);
But is it possible to define a function which can take any number of arguments of whatver type and change all of their values?
I know I can do this using pointers but then I don't know the type of the parameter passed so it is quite unsafe to mess with them.
I just want to create a function which can return variable number of variables of many different types, not just of 1 type or just 1 variable. And I don't want to write zillions of lines to use the function- it should just be the function itself, no SetLength() before the function call or anything. So here is the best thing I made so far:
type TVarArr = Array of Variant;
PVarArr = ^TVarArr;
Procedure f(a: PVarArr);
var
i:Integer;
begin
SetLength(A^, 4);
a^[0] := 46;
end;
ar: TVarArr;
begin
f(#ar);
caption := IntToStr(ar[0]);
Tom will not be able to use this answer as his Delphi version isn't high enough, but anybody on D2010 or higher will be able to put the extended rtti's TValue to good use on this type of challenge.
Following is a small console app showing how to:
create a dynamic array of TValue's from a open array parameter;
copy a dynamic array of TValue's to a new dynamic array modifying individual values on the way;
modify the items in a dynamic array of TValues "in place".
Enjoy.
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.Rtti,
System.SysUtils,
System.TypInfo;
const
StringKinds: set of TTypeKind = [tkChar, tkString, tkWChar, tkLString, tkWString, tkUString];
type
TValueArray = array of TValue;
function ValueArrayFromConstArray(const aSource: array of TValue): TValueArray;
var
idx: Integer;
begin
SetLength(Result, Length(aSource));
for idx := Low(aSource) to High(aSource) do
Result[idx] := aSource[idx];
end;
function ReturnNewArray(const aSource: TValueArray): TValueArray;
var
idx: Integer;
begin
SetLength(Result, Length(aSource));
for idx := Low(aSource) to High(aSource) do
if aSource[idx].Kind in StringKinds then
Result[idx] := 'Dest' + aSource[idx].ToString
else
if aSource[idx].Kind in [tkInteger] then
Result[idx] := 10 + aSource[idx].AsInteger
else
Result[idx] := aSource[idx];
end;
procedure ModifyArrayValues(var aArray: TValueArray);
var
idx: Integer;
begin
for idx := Low(aArray) to High(aArray) do
if aArray[idx].Kind in StringKinds then
aArray[idx] := 'Dest' + aArray[idx].ToString
else
if aArray[idx].Kind in [tkInteger] then
aArray[idx] := 10 + aArray[idx].AsInteger
else
;//aArray[idx] := aArray[idx];
end;
var
Source: TValueArray;
Destination: TValueArray;
Item: TValue;
idx: Integer;
begin
Source := ValueArrayFromConstArray(['Some', 42, TObject]);
Destination := ReturnNewArray(Source);
idx := 0;
WriteLn('', #9, 'Old', #9, 'New');
WriteLn('-', #9, '----', #9, '----');
for Item in Source do
begin
WriteLn(idx, #9, Item.ToString, #9, Destination[idx].ToString);
Inc(idx);
end;
WriteLn;
WriteLn;
WriteLn('', #9, 'Modified');
WriteLn('-', #9, '----');
Source := ValueArrayFromConstArray(['first', 50, TValue.From<TFloatValue>(fvCurrency)]);
ModifyArrayValues(Source);
for Item in Source do
begin
WriteLn(idx, #9, Item.ToString);
end;
ReadLn;
end.
Procedure All(var a:Array of Variant);
var
i:Integer;
begin
for I := Low(a) to High(a) do
begin
if VarType(a[i])=258 then
a[i] := a[i] + ' modified';
end;
end;
Procedure AllConst( a:Array of Variant);
var
i:Integer;
begin
for I := Low(a) to High(a) do
begin
Showmessage(a[i]);
end;
end;
procedure TForm3.Button1Click(Sender: TObject);
var
a:Array of Variant;
begin
AllConst([1,2,'Test']);
SetLength(a,3);
a[0] := 3.141;
a[1] := 'Test';
a[2] := 27;
all(a);
Showmessage(a[1]);
end;
To read a index file in a specific format, I cooked the following piece of code without considering byte ordering:
unit uCBI;
interface
uses
SysUtils,
Classes,
Generics.Collections;
type
TIndexList = class
private
FIndexList:TList<Cardinal>;
FOwnedStream:Boolean;
FMemoryStream: TMemoryStream;
function GetCount: Integer;
protected
public
constructor Create(AStream:TMemoryStream; OwnedStream:Boolean=True);
destructor Destroy; override;
function Add(const Value: Cardinal): Integer;
procedure Clear;
procedure SaveToFile(AFileName:TFileName);
procedure LoadFromFile(AFileName:TFileName);
property Count: Integer read GetCount;
end;
implementation
{ TIndexList }
function TIndexList.Add(const Value: Cardinal): Integer;
begin
Result := FIndexList.Add(Value)
end;
procedure TIndexList.Clear;
begin
FIndexList.Clear;
end;
constructor TIndexList.Create(AStream: TMemoryStream; OwnedStream: Boolean);
begin
FMemoryStream := AStream;
FOwnedStream := OwnedStream;
FIndexList := TList<Cardinal>.Create;
end;
destructor TIndexList.Destroy;
begin
if (FOwnedStream and Assigned(FMemoryStream)) then
FMemoryStream.Free;
FIndexList.Free;
//
inherited;
end;
function TIndexList.GetCount: Integer;
begin
Result := FIndexList.Count;
end;
procedure TIndexList.LoadFromFile(AFileName: TFileName);
var
lMemoryStream:TMemoryStream;
lCount:Cardinal;
begin
lMemoryStream := TMemoryStream.Create;
try
lMemoryStream.LoadFromFile(AFileName);
lMemoryStream.ReadBuffer(lCount,SizeOf(Cardinal));
if (lCount = Cardinal((lMemoryStream.Size-1) div SizeOf(Cardinal))) then
begin
FMemoryStream.Clear;
lMemoryStream.Position :=0;
FMemoryStream.CopyFrom(lMemoryStream,lMemoryStream.Size)
end else
raise Exception.CreateFmt('Corrupted CBI file: %s',[ExtractFileName(AFileName)]);
finally
lMemoryStream.Free;
end;
end;
procedure TIndexList.SaveToFile(AFileName: TFileName);
var
lCount:Cardinal;
lItem:Cardinal;
begin
FMemoryStream.Clear;
lCount := FIndexList.Count;
FMemoryStream.WriteBuffer(lCount,SizeOf(Cardinal));
for lItem in FIndexList do
begin
FMemoryStream.WriteBuffer(lItem,SizeOf(Cardinal));
end;
//
FMemoryStream.SaveToFile(AFileName);
end;
end.
It tested it and seems to work well as needed. Great was my suprise when I pursue extensive tests with real sample file. In fact the legacy format was devised with Amiga computer with a different byte ordering.
My Question:
How can I fix it ?
I want to keep the code unchanged and wonder wether a decorated TMemorySream will do so that I can transparently switch between big endian and little endian.
To change 'endianness' of Cardinals you can use the following:
function EndianChange(Value: Cardinal): Cardinal;
var
A1: array [0..3] of Byte absolute Value;
A2: array [0..3] of Byte absolute Result;
I: Integer;
begin
for I:= 0 to 3 do begin
A2[I]:= A1[3 - I];
end;
end;
If you want to keep your code unchanged, you can write your own TMemoryStream descendant and override its Read and Write methods using the above function, like that:
function TMyMemoryStream.Read(var Buffer; Count: Integer): Longint;
var
P: PCardinal;
I, N: Integer;
begin
inherited;
P:= #Buffer;
Assert(Count and 3 = 0);
N:= Count shr 2;
while N > 0 do begin
P^:= EndianChange(P^);
Inc(P);
Dec(N);
end;
end;