I have a class which stores generic items in an array. These items are accessed by a default array property:
TMyList<TData> = class
private
items: array of TItem<TData>;
public
function get(position: integer): TData;
procedure edit(position: integer; data: TData);
property Values[position: integer]: TData read get write edit; default;
end;
implementation
function TMyList<TData>.get(position: integer): TData;
begin
result:= items[position];
end;
procedure TMyList<TData>.edit(position: integer; data: TData);
var
item: TItem<TData>;
begin
items[position]:= item;
end;
end;
In this case the items I am storing are all of the type TTest which also has its own properties:
TTest = record
private
FTest: string;
procedure setFTest(const Value: string);
public
property Test: string read FTest write setFTest;
end;
implementation
procedure TColouredPoint.setFTest(const Value: String);
begin
FTest:= Value;
end;
end;
I want to be able to change the value of FTest for an instance of TTest like this:
var
points: TMyList<TTest>;
...
points[index].Test:= 'test';
but this doesn't do anything. There is no error message but the value of points[index].Test doesn't change.
Instead I have to do this:
var
points: TMyList<TTest>;
temp: TTest;
...
temp:= points[index];
temp.Test:= 'test';
points[index]:= temp;
Why does the first version not work?
Why does the first version not work?
Consider this code
points[index].Test := 'test';
The indexed property is converted by the compiler into the a function call and so the compiler effectively compiles this:
points.get(index).Text := 'test';
Now, points.get(index) returns a copy of the TTest value. Since you don't assign that to anything, the compiler introduces a local variable to hold the return value.
So your code becomes, in effect:
var
tmp: TTest;
...
tmp := points.get(index);
tmp.Text := 'test';
That's the last thing that is ever done with tmp, and so the modifications that you make to tmp.Text are simply discarded leaving the underlying object untouched.
This issue is pretty hard to get around when working with value types.
The generic Delphi TList<T> collection allows you direct access to the underlying array, which allows you to operate on the stored values directly.
Another approach is to use a reference rather than a value. One simple way to achieve that is to use a T that is a class rather than a record, i.e. a reference type rather than a value type.
It is because TTest is a record. The getter of the list returns a copy of the actual record and that is what you are changing. It should work when you declare TTest as a class, but then you have to take care of creating and destroying it.
Can you use a record as a Key value in TDictionary? I want to find objects based on combination of string, integer and integer.
TUserParKey=record
App:string;
ID:integer;
Nr:integer;
end;
...
var
tmpKey:TUserParKey;
tmpObject:TObject;
begin
tmpObject:= TTObject.Create(1);
tmpKey.App:='1';
tmpKey.ID :=1;
tmpKey.Nr :=1;
DTUserPars.Add(tmpKey,tmpObject)
...
var
tmpKey:TUserParKey;
begin
tmpKey.App:='1';
tmpKey.ID :=1;
tmpKey.Nr :=1;
if not DTUserPars.TryGetValue(tmpKey,Result) then begin
result := TTObject.Create(2);
end;
This returns object 2.
Yes, you can use records as keys in a TDictionary but you should provide your own IEqualityComparer when creating the dictionary because the default one for records just does a dumb binary compare of the record.
This fails for a record containing a string because it just compares the pointer of that string which may be different even if the string contains the same value.
Such a comparer would look like this:
type
TUserParKeyComparer = class(TEqualityComparer<TUserParKey>)
function Equals(const Left, Right: TUserParKey): Boolean; override;
function GetHashCode(const Value: TUserParKey): Integer; override;
end;
function TUserParKeyComparer.Equals(const Left, Right: TUserParKey): Boolean;
begin
Result := (Left.App = Right.App) and (Left.ID = Right.ID) and (Left.Nr = Right.Nr);
end;
function TUserParKeyComparer.GetHashCode(const Value: TUserParKey): Integer;
begin
Result := BobJenkinsHash(PChar(Value.App)^, Length(Value.App) * SizeOf(Char), 0);
Result := BobJenkinsHash(Value.ID, SizeOf(Integer), Result);
Result := BobJenkinsHash(Value.Nr, SizeOf(Integer), Result);
end;
Instead of using the record as a key, you could use a string consisting of the serialized record. You could use something like https://github.com/hgourvest/superobject to do the serialization.
Since strings have built-in comparison semantics and hashcodes, you don't need to write comparison and hashcode functions.
My best approach should be to joint the default hash code of the base types.
For instance:
Value.App.GetHashCode + Value.ID.GetHashCode + Value.Nr.GetHashCode;
Is there a standard way to convert between TVarRec and Variant values?
I want to parse an 'array of const' and use the values to populate parameters in a TMSQuery. To do this I'm using a list of column names (generated from TMSQuery.KeyFields), and matching the values in the array with the column names in KeyFields (by position), then using the column name to set the corresponding parameter using ParamByName.
The code below is what I've come up with, but VarRecToVariant doesn't seem very elegant. Is there a better solution?
keyFields: TStringList;
// List of table column names (keyFields.DelimitedText := query.KeyFields;)
// e.g. Name, Age
query: TMSQuery;
// Parametrized query with a parameter for each field in keyFields
// SELECT * FROM People WHERE Age=:Age AND Name=:Name
// If keyValues is ['Bob', 42] the resulting query should be
// SELECT * FROM People WHERE Age=42 AND Name='Bob'
procedure Read(keyValues: array of const);
var
i: Integer;
name: string;
value: Variant;
begin
...
for i := 0 to keyFields.Count - 1 do
begin
name := keyFields[i];
value := VarRecToVariant(keyValues[i]);
query.ParamByName(name).Value := value;
end;
query.Open
...
end;
function VarRecToVariant(varRec: TVarRec): Variant;
begin
case varRec.VType of
vtInteger: result := varRec.VInteger;
vtBoolean: result := varRec.VBoolean;
vtChar: result := varRec.VChar;
vtExtended: result := varRec.VExtended^;
vtString: result := varRec.VString^;
...
end;
end;
Notes:
The values in the array of const depend on the parameters in the query. The caller knows what these are, but the method that uses the array doesn't know how many or what type to expect. I.e. I can't change the method to Read(name: string; age: integer).
The parameters are not necessarily used in the same order that the values are specified in the array of const. In the example, keyFields are specified as "Name,Age" but the query uses Age before Name. This means Params[i].Value := keyValues[i] won't work. I think VarRecToVariant would still be needed anyway, which I'm trying to avoid).
Replace
procedure Read(keyValues: array of const);
with
procedure Read(keyValues: array of Variant);
Then you will not need to convert TVarRec to Variant.
const
states : array [0..49,0..1] of string =
(
('Alabama','AL'),
('Montana','MT'),
('Alaska','AK'),
('Nebraska','NE'),
('Arizona','AZ'),
('Nevada','NV'),
('Arkansas','AR'),
('New Hampshire','NH'),
('California','CA'),
('New Jersey','NJ'),
('Colorado','CO'),
('New Mexico','NM'),
('Connecticut','CT'),
('New York','NY'),
('Delaware','DE'),
('North Carolina','NC'),
('Florida','FL'),
('North Dakota','ND'),
('Georgia','GA'),
('Ohio','OH'),
('Hawaii','HI'),
('Oklahoma','OK'),
('Idaho','ID'),
('Oregon','OR'),
('Illinois','IL'),
('Pennsylvania','PA'),
('Indiana','IN'),
('Rhode Island','RI'),
('Iowa','IA'),
('South Carolin','SC'),
('Kansas','KS'),
('South Dakota','SD'),
('Kentucky','KY'),
('Tennessee','TN'),
('Louisiana','LA'),
('Texas','TX'),
('Maine','ME'),
('Utah','UT'),
('Maryland','MD'),
('Vermont','VT'),
('Massachusetts','MA'),
('Virginia','VA'),
('Michigan','MI'),
('Washington','WA'),
('Minnesota','MN'),
('West Virginia','WV'),
('Mississippi','MS'),
('Wisconsin','WI'),
('Missouri','MO'),
('Wyoming','WY')
);
function getabb(state:string):string;
var
I:integer;
begin
for I := 0 to length(states) -1 do
if lowercase(state) = lowercase(states[I,0]) then
begin
result:= states[I,1];
end;
end;
function getstate(state:string):string;
var
I:integer;
begin
for I := 0 to length(states) -1 do
if lowercase(state) = lowercase(states[I,1]) then
begin
result:= states[I,0];
end;
end;
procedure TForm2.Button1Click(Sender: TObject);
begin
edit1.Text:=getabb(edit1.Text);
end;
procedure TForm2.Button2Click(Sender: TObject);
begin
edit1.Text:=getstate(edit1.Text);
end;
end.
Is there a bette way to do this?
Should this kind of data be hard coded?
Wouldn't it be better to use something like a XML file or even just a CSV.
Or Name Value Pairs, i.e. IA=Iowa
then loaded into a TStringList to get
States.Values['IA'] = 'Iowa';
Then you just need to write something to search the Values to work backwards like
//***Untested***
//Use: NameOfValue(States, 'Iowa') = 'IA'
function NameOfValue(const strings: TStrings; const Value: string): string;
var
i : integer;
P: Integer;
S: string;
begin
for i := 0 to strings.count - 1 do
begin
S := strings.ValueFromIndex[i];
P := AnsiPos(strings.NameValueSeparator, S);
if (P <> 0) and (AnsiCompareText(Copy(S, 1, P - 1), Value) = 0) then
begin
Result := strings.Names[i];
Exit;
end;
end;
Result := '';
end;
I'm fairly sure its case insensitive too
If you're on D2009 or D2010, use a TDictionary<string, string> from Generics.Collections. Declare the array of constants like you have it, then set up your dictionary by putting each pair in to the dictionary. Then just use the dictionary's default property to do your lookups.
Notice that lowercase(a) = lowercase(b) is slower than sameText(a, b).
In addition, you can speed up the procedure further by storing the strings in the array as lower-case only, and then in the look-up routine start with converting the input to lower-case as well. Then you can use the even faster function sameStr(a, b). But of course, when a match is found, you then need to format it by capitalizing the initial letters. This speed-up approach is probably not very important for such a small list of strings. After all, there are not too many states in the US.
Also, you should declare the functions using const arguments, i.e. write
function getabb(const state:string):string;
instead of
function getabb(state:string):string;
(unless you want to change state in the routine).
Finally, you could make the code more compact and readable by omitting the begin and end of the for loops.
I would have your lists sorted. That way you can use a binary search to cut the lookup times down. It all depends on the number of iterations you will be exercising. Around 50 items doesn't seem like much, until your iterating over the list a few thousand times looking for the last item in the list.
Also you should ALWAYS bail from your loops as soon as you get get a match if you know the rest of the list will not match.
Arrays are fine, and depending on how your using the data, you might need to add some of the "territories" that also have abbreviations (PR = PUERTO RICO, GU = GUAM, etc.).
I'm trying to store a set inside the object property (and read it) of a TStringList (I will also use it to store text associated to the set) but I get a invalid typecast for the set.
What's the best way to store a set inside a StringList object? Also, will this object need to be freed when destroying the StringList?
Here's some example code:
type
TDummy = (dOne, dTwo, dThree);
TDummySet = set of TDummy;
var
DummySet: TDummySet;
SL: TStringList;
begin
SL := TStringList.Create;
Try
DummySet := [dOne, dThree];
SL.AddObject('some string', TObject(DummySet)); // Doesn't work. Invalid typecast
Finally
SL.Free;
End;
end;
First read the other answers - probably you'll find a less hacky solution.
But FTR: You can write
SL.AddObject('some string', TObject(Byte(DummySet)));
and
DummySet := TDummySet(Byte(SL.Objects[0]));
if you really want.
Note: You'll have to change the keyword Byte if you add enough elements to the TDummySet type. For example, if you add six more elements (so that there is a total of nine) you need to cast to Word.
I can't add non objects on that case.
What you can do, is create an object that have TDummySet as Field.
Something like
TExemple = class
DummySet = TDummySet;
end;
Or you can use a different approach:
Declarations:
TDummy = (dOne, dTwo, dThree);
TDummySet = set of TDummy;
PDummySet = ^TDummySet;
How to use:
var
DummySet: PDummySet;
begin
New(DummySet);
DummySet^ := [dOne, dThree];
You should not store a set via TStringList.Objects because what Objects use (TObject) is a 32 bit value type and sets can be represented up to 256 bits depending on the size of the set. That's probably why the compiler doesn't even allow casting.
A better way to serialize sets is using RTTI. I am not sure where VCL exposes its builtin set serialization mechanism but JCL has a JclRTTI unit with JclSetToStr and JclStrToSet functions.
var
fs: TFontStyles;
begin
JclStrToSet(TypeInfo(TFontStyles), fs, 'fsBold, fsItalic'); // from string
Showessage(JclSetToStr(TypeInfo(TFontStyles), fs)); // to string
end;
I don't think a stringlist is the way to go. Why not an array of TDummySet? And no, there is no need to free it because the set is not an object.
var
Test: Array of TDummySet;
SetLength(Test, 2);
Test[0] := [dOne, dThree];
Test[1] := [dTwo];
When you're done:
SetLength(Test, 0);
You cannot make a typecast from your set to a TObject, because your variable is not a pointer.
You have to store a pointer to your variable in the TStringList. In that case, you'll have to allocate and deallocate it manually too.
Try something like this:
type
TEnum = (one, two, three);
TSet = set of TEnum;
PSet = ^TSet;
var s: TStringList;
p: PSet;
begin
s := TStringList.Create;
p := AllocMem(SizeOf(TSet));
p^ := [two, three];
S.AddObject('a', TObject(p));
// bla bla bla
// Here you read the set in the string list
if (two in PSet(S.Objects[0])^)) then begin
// your checks here
end
...