How to save/load Set of Types?
Asked Answered
D

10

20

I have this code

type
  TXSample = (xsType1, xsType2, xsType3, xsType4, xsType5, xsType6, xsType6, xsTyp7, xsType8); // up to FXSample30;  
..

private
  FXSample = Set of TXSample;  
..

published
  property Sample: TXSample read FXSample  write FXSample; 
..

  //if Sample has a value of
  Sample := [xsType2, xsType4, xsType5, xsType6, xsTyp7];

how can i save/load the property of Sample?
i would like to save it in the database.
is it possible?

Deary answered 4/3, 2012 at 8:40 Comment(6)
You could have a bunch of boolean fields. Or you could compress into an integer or a number of integers using one bit per flag. I would use the former.Waddle
Have a look at TReader.ReadSet and WriteSet in TWriter.WriteProperty in Classes.pas. That's how the VCL streams set properties to and from dfm files. If you use those, you would need a string field in your database to store the written out set. Please note that going this way will make reading / writing easier than storing the values in separate boolean fields, but may make filtering your dataset using SQL a lot harder.Loos
Not that it's right or wrong, but why aren't you making a type called TXSamples = set of TXSample and then the field FXSample: TXSamples... I'm confused as to why you're declaring it this way in the private field. You're much better off making another type TXSamples and then using that type for FXSample... At least that's how I've always seen it done and have done myself.Lympho
@Jerry Dodge, sorry if it's not so clear. I edited the question for details.Deary
@Deary Not that there's anything unclear, just I'd highly advise to define a type for this set rather than cast it every time you need to use it.Lympho
Try thisPressmark
H
29

Provided your set will never exceed 32 possibilities (Ord(High(TXSample)) <= 31), then it is perfectly fine to typecast the set into an Integer and back:

type
  TXSamples = set of TXSample;
var 
  XSamples: TXSamples;
begin
  ValueToStoreInDB := Integer(XSamples);
  Integer(XSamples) := ValueReadFromDB;
end;

To be more specific: SizeOf(TXSamples) has to be precisely equal to SizeOf(StorageTypeForDB). Thus the following ranges apply for Ord(High(TXSample)) when typecasting TXSamples to:

  • Byte: Ord(High(TXSample)) < 8
  • Word: 8 <= Ord(High(TXSample)) < 16
  • Longword: 16 <= Ord(High(TXSample)) < 32
  • UInt64: 32 <= Ord(High(TXSample)) < 64
Hiroshima answered 4/3, 2012 at 10:41 Comment(5)
Does this actually work? I get an E2089 Invalid typecast error when casting the set to a integer.Courtier
@Dangph Yes. Did you check the maximum set size/enumeration boundary? (E.g. when your set is made up of an enumeration with 16 possibilities, you must cast the set to a Word. When the maximum set size is 17, you must cast to an Integer.)Hiroshima
I see, thanks. I needed to cast to a Byte, not to an Integer. I was assuming that compiler would just automatically widen the value to an Integer.Courtier
Thanks a lot, this is really cool, I used the following code to condense a 8 liner into one:CustomFont.Style := TFontStyles(Byte( (ord(fsBold) and (Ord(isBold) shl Byte(ord(fsBold)))) or (ord(fsItalic) and (Ord(isItalic) shl Byte(ord(fsItalic)))) or (ord(fsStrikeOut) and (Ord(isStrikeout) shl Byte(ord(fsStrikeOut)))) or (ord(fsUnderline) and (Ord(isUnderline) shl Byte(ord(fsUnderline))))) ); where isBold, etc.. are booleansRetentivity
You can't cast to UInt64 for sizes between 32 and 64 in 32 bit Delphi. See my answer here for the differences between 32 bit and 64 bit. #30337120Haygood
H
7

Directly typecasting a set variable is not possible in Delphi, but internally Delphi stores the set as a byte-value. By using an untyped move, it is easy copied into an integer. Note that these functions only go up to a size of 32 (bounds of an integer). To increase the bounds, use Int64 instead.

function SetToInt(const aSet;const Size:integer):integer;
begin
  Result := 0;
  Move(aSet, Result, Size);
end;

procedure IntToSet(const Value:integer;var aSet;const Size:integer);
begin
  Move(Value, aSet, Size);
end;

Demo

type
  TMySet = set of (mssOne, mssTwo, mssThree, mssTwelve=12);
var
  mSet: TMySet;
  aValue:integer;
begin
  IntToSet(7,mSet,SizeOf(mSet));
  Include(mSet,mssTwelve);
  aValue := SetToInt(mSet, SizeOf(mSet));
end;
Horney answered 16/1, 2013 at 14:46 Comment(1)
It would be a good idea to test somewhere that sizeof(TMySet) = sizeof(integer) just in case your set was too small or too large.Courtier
M
3

Personally, I would convert the set to an integer and store it in the database as an INT field, like others suggested. @teran suggested using the TIntegerSet type, and here is my approach working on native integers using bit operations.

Note that you can use SampleInInteger() to determine whether a certain element from the enumeration is present in the integer mask generated by SampleSetToInteger().

Here's the code:

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type
  { .: TXSample :. }
  TXSample = (xsType1 = 0, xsType2, xsType3, xsType4, xsType5,
    xsType6, xsType7, xsType8); // up to FXSample30;
  TXSampleSet = set of TXSample;

// Converts a TXSampleSet to an integer.
function SampleSetToInteger(const S: TXSampleSet): Integer;
var
  Sample: TXSample;
begin
  Result := 0;

  for Sample := Low(TXSample) to High(TXSample) do
    if (Sample in S) then
      Result := Result or (1 shl Ord(Sample));
end;

// Converts an integer to TXSampleSet.
function IntegerToSampleSet(const Int: Integer): TXSampleSet;
var
  I: Integer;
begin
  Result := [];

  for I := 0 to Ord(High(TXSample)) do
    if Int and (1 shl I) <> 0 then
      Result := Result + [TXSample(I)];
end;

// Checks if a TXSample is present in the integer.
function SampleInInteger(const S: TXSample; const Int: Integer): Boolean;
begin
  Result := Int and (1 shl Ord(S)) <> 0;
end;

var
  XSample, XSample1: TXSampleSet;
  Tmp: Integer;
begin
  XSample := [xsType2, xsType4, xsType5, xsType6, xsType7];
  XSample1 := [xsType1];
  Tmp := SampleSetToInteger(XSample);

  Writeln(Tmp);
  XSample1 := IntegerToSampleSet(Tmp);
  if (xsType5 in XSample1) then
    Writeln('Exists');
  if (SampleInInteger(xsType1, Tmp)) then
    Writeln('Exists in int');


  Readln;
end.
Merl answered 4/3, 2012 at 10:12 Comment(0)
W
3

A Delphi set is simply a collection of (possibly) related boolean flags. Each boolean flag corresponds to whether or not the matching ordinal value is in the set.

You could certainly pack a set into an integer value by representing the set as a bitset. Or you could create a textual representation of the set.

However, both of these options leave you with no tractable ability to query the database at the SQL level. For this reason I would advise you to represent each value in the set, i.e. each boolean flag, as a separate field (i.e. column) of the database table. This gives you the most powerful representation of the data.

Waddle answered 4/3, 2012 at 10:41 Comment(1)
It depends. If you'd have, let's say, more than 20 elements in the set's enumeration, making column for each element could be just wasting of resources. I would personally make columns for at most 5 elements. For more of them I would use bitwise operators in SQL queries (not much comfortable though), or for those who knows SQL but cannot use bitwise operators I would make table views.Kooky
A
2

the easiest way to store set in database (as @DavidHeffernan mentioned in comment) is to convert your set to bit-mask. in int32 (integer) value you have 32 bits and can save set up to 32 fields; Delphi has TIntegerSet (see http://docwiki.embarcadero.com/Libraries/en/System.SysUtils.TIntegerSet) type defined in SysUtils. it is declared as:

TIntegerSet = set of 0..SizeOf(Integer) * 8 - 1;

so using it, it is simple to convert set to integer and back (just casting TIngeterSet to integer or vice versa);

bit-mask is also good option because it is only one INT field in your database table.

also you can create separate table in your DB to store set content (main table (id, ...), and setValuesTable (main_id, setElementValue)) (this option is good for using in db queries)

here is an example of using TIntegerSet:

program Project1;
{$APPTYPE CONSOLE}
uses System.SysUtils;

type
    TXSample = (xsType1, xsType2, xsType3, xsType4, xsType5, xsType6,  xsType7, xsType8);
    TSampleSet = set of TXSample;



    function SampleSetToInteger(ss : TSampleSet) : integer;
    var intset : TIntegerSet;
        s : TXSample;
    begin
        intSet := [];
        for s in ss do
            include(intSet, ord(s));

        result := integer(intSet);
    end;

    function IntegerToSampleSet(mask : integer) : TSampleSet;
    var intSet : TIntegerSet;
        b : byte;
    begin
        intSet := TIntegerSet(mask);
        result := [];
        for b in intSet do
            include(result, TXSample(b));
    end;

var xs : TSampleSet;
    mask : integer;
begin
    xs := [xsType2, xsType6 .. xsType8];

    mask := SampleSetToInteger(xs);     //integer mask
    xs := IntegerToSampleSet(mask);
end.
Angrist answered 4/3, 2012 at 10:4 Comment(0)
F
2

Set variables can be saved successfully to a TStream descendant. Here's an example.

Just create a new vcl forms application, add two TButton components to it and fill in the OnClick events for each button as illustrated in the example below.

This was created in XE4 so the uses clause might be different for other versions of Delphi but that should be trivial to change by removing the namespace indicators before each unit in the uses clause. Saving a set type variable with articulated values is possible to a binary file and easily with Delphi. In other words,

Also suggested is taking a look at the TypInfo unit if you have the source or just using the functions provided which make dissecting Set types down to their text representation fairly simple though no example is provided here. That is suggested if you want to include saving to a config or ini file or in a persistence format that is text editable.

The one below is the simplest one that I know of. Looking at the binary output of a set type saved to a stream like the one below implies that it is saved in the smallest possible bitmapped representation for the set based on its size. The one below maps to one byte on disk (the value is 5) which means that each value must be mapped to a power of 2 (seThis = 1, seThat = 2, seTheOther = 4) just like manually created constant bitmasked values. The compiler likely enforces that it follows rules that forces set to retain their ordinality. This example was tested an works in Delphi XE4.

Hope that helps.

Brian Joseph Johns

unit Unit1;

interface

uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
     Vcl.StdCtrls;

type
  TSomeEnum = (seThis, seThat, seTheOther);
  TSomeEnumSet = set of TSomeEnum;

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var

  Form1: TForm1;
  SomeSetVar: TSomeEnumSet;
  SomeBoolean: Boolean;
  SomeInt: Integer;

implementation

{$R *.dfm}


procedure TForm1.Button1Click(Sender: TObject);
begin
  SomeSetVar := [seThis, seTheOther];
  SomeBoolean := True;
  SomeInt := 31415;

  with TFileStream.Create('SetSave.bin',fmCreate or fmOpenWrite or fmShareCompat) do
  try
    Write(SomeSetVar,SizeOf(SomeSetVar));
    Write(SomeBoolean,SizeOf(Boolean));
    Write(SomeInt,SizeOf(Integer));
  finally
    Free;
  end;
  SomeSetVar := [];
  SomeInt := 0;
  SomeBoolean := False;

end;

procedure TForm1.Button2Click(Sender: TObject);
var
  ResponseStr: string;
begin
  with TFileStream.Create('SetSave.bin',fmOpenRead or fmShareCompat) do
  try
    Read(SomeSetVar,SizeOf(SomeSetVar));
    Read(SomeBoolean,SizeOf(Boolean));
    Read(SomeInt,SizeOf(Integer));
  finally
    Free;
  end;

  ResponseStr := 'SomeSetVar = ';
  if (seThis in SomeSetVar) then
    ResponseStr := ResponseStr + 'seThis ';

  if (seThat in SomeSetVar) then
    ResponseStr := ResponseStr + 'seThat ';

  if (seTheOther in SomeSetVar) then
    ResponseStr := ResponseStr + 'seTheOther ';

  ResponseStr := ResponseStr + ' SomeBoolean = ' + BoolToStr(SomeBoolean);

  ResponseStr := ResponseStr + ' SomeInt = ' + IntToStr(SomeInt);

  ShowMessage(ResponseStr);

end;

end.
Famine answered 3/1, 2014 at 0:43 Comment(0)
F
1

With a little help from RTTI it can be achieved in a generic way:

program SetConverter;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  System.RTTI, System.SysUtils;

type
  SetConverter<T> = class abstract
  strict private
    class var FRttiContext: TRttiContext;
  public
    class function ToInteger(aSet: T): Integer;
    class function FromInteger(aValue: Integer): T;
  end;

  { SetConverter<T> }

class function SetConverter<T>.FromInteger(aValue: Integer): T;
var
  ResultValues: TIntegerSet;
  ReturnType: TRttiType;
  SetValues: TIntegerSet;
  EnumSet: T absolute SetValues;
begin
  ReturnType := FRttiContext.GetType(Self).GetMethod('FromInteger').ReturnType;

  if not((ReturnType is TRttiSetType) and (TRttiSetType(ReturnType).ElementType is TRttiEnumerationType)) then
    Exit;

  SetValues := TIntegerSet(aValue);
  Result := EnumSet;
end;

class function SetConverter<T>.ToInteger(aSet: T): Integer;
var
  RttiParameter: TRttiParameter;
  ResultValues: TIntegerSet;
  SetValues: TIntegerSet;
  EnumSet: T absolute SetValues;
  EnumType: TRttiEnumerationType;
  SetType: TRttiSetType;
  i: Integer;
begin
  Result := 0;
  RttiParameter := FRttiContext.GetType(Self).GetMethod('ToInteger').GetParameters[0];
  if not(RttiParameter.ParamType is TRttiSetType) then
    Exit;

  SetType := RttiParameter.ParamType as TRttiSetType;

  if not(SetType.ElementType is TRttiEnumerationType) then
    Exit;

  EnumType := SetType.ElementType as TRttiEnumerationType;

  EnumSet := aSet;

  ResultValues := [];

  for i := EnumType.MinValue to EnumType.MaxValue do
    if i in SetValues then
      Include(ResultValues, i);

  Result := Integer(ResultValues);
end;

type
  TXSample = (xsType1, xsType2, xsType3, xsType4, xsType5, xsType6, xsType7, xsType8);
  TSampleSet = set of TXSample;

var
  Before, After: TSampleSet;
  i: Integer;
begin
  Before := [xsType2, xsType6 .. xsType8];
  i := SetConverter<TSampleSet>.ToInteger(Before);
  After := SetConverter<TSampleSet>.FromInteger(i);

  WriteLN('Before = After: ' + (Before = After).ToString(TUseBoolStrs.True));
  Readln;
end.
Fuddyduddy answered 31/1, 2022 at 12:39 Comment(0)
D
0

You can use this unit to convert set to int. if you need more settoint functions you can add yours by looking code below.

Set may take only 1 byte memory space. So you can obtain yourSet size and get result as modula of this result.

example: your set size: 1 byte you can get result -->

Result := pINT^ mod maxVal

You should obtain maxval by calculating maxvalue of variable type.

maxVal = Power( 2, (8*sizeof(MySet)-1) )

    unit u_tool;

interface
uses Graphics;

type
  TXSample = (xsType1, xsType2, xsType3, xsType4, xsType5, xsType6, xsType6, xsTyp7, xsType8); // up to FXSample30;
  FXSample = Set of TXSample;

  function FXSampleToInt(FXSample: FXSample ): Integer;
  function IntToFXSample(Value: Integer): FXSample;


  function FontStyleToInt(FontStyle: TFontStyles ): Integer;
  function IntToFontStyle(Value: Integer): TFontStyles;

implementation


function FXSampleToInt(FXSample: FXSample ): Integer;
var
  pInt: PInteger;
begin
  pInt := @FXSample;
  Result := pInt^;
end;

function IntToFXSample(Value: Integer): FXSample;
var
  PFXSample: ^FXSample;
begin
  PFXSample := @Value;
  Result := PFXSample^;
end;





function FontStyleToInt(FontStyle: TFontStyles ): Integer;
var
  pInt: PInteger;
begin
  pInt := @FontStyle;
  Result := pInt^;
end;

function IntToFontStyle(Value: Integer): TFontStyles;
var
  PFontStyles: ^TFontStyles;
begin
  PFontStyles := @Value;
  Result := PFontStyles^;
end;






end.
Dinny answered 4/12, 2014 at 9:48 Comment(0)
M
0

Or we can make compiler forget about the types completly and then define what it should see (in case we know in compile-time what it sould see). This solution is so awful as it can be written on just one line.

type
  // Controls.TCMMouseWheel relies on TShiftState not exceeding 2 bytes in size 
  TShiftState = set of (ssShift, ssAlt, ssCtrl,
                        ssLeft, ssRight, ssMiddle, 
                        ssDouble, ssTouch, ssPen, 
                        ssCommand, ssHorizontal); 

var 
  Shifts : TShiftState;
  Value :  Integer;
begin
  Shifts := TShiftState((Pointer(@Value))^):

  Value  := (PInteger(@Shifts))^;

  if ssShift in TShiftState((Pointer(@Value))^) then 
     Exit;
end;

It happens that unused (top) bits are set (or not) but it has no influence on set operations (in, =, +, -, * .. ).

This line in Delphi:

Shifts := TShiftState((Pointer(@Value))^);

is like this in Assembler (Delphi XE6):

lea eax,[ebp-$0c]
mov ax,[eax]
mov [ebp-$06],ax

On Delphi 2007 (where is TShiftState is smaller so Byte can be used) this Assembler:

movzx eax,[esi]
mov [ebp-$01],al
Mundy answered 20/4, 2016 at 14:53 Comment(0)
A
0

Simplest solution - proceeding the set directly as numeric variable. The "absolute" is a keyword:

procedure Foo(FXSample: TFXSample);
var
  NumericFxSample: Byte absolute FXSample;
begin
WriteLn(YourTextFile, NumericFxSample);//numeric value from a set
end;

If your type is wider than 8 bits you need to use wider numeric type like word (up to 16 items in a set) or dword.

Accessary answered 20/6, 2016 at 19:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.