November 18, 2012

XE3: Adding properties to a set

[Français]
Delphi XE3 has a very interesting new feature: the record helpers.

Despite his name which reference a specific data type (record), a record helper can be applyed to most standard data types as well, include the sets.

Using a record helper, we will be able to add properties and methods to sets.

In this artcile, I will show you how it works using a simple case: adding a property to a set allowing the developer to convert the set to an integer or to assign a set with an integer.

You'll ask: Is it really useful in the real world ?

Yes it is! Here is the story which led me to this feature. I was working on a driver for a input/output card (I/O card). This card was driving an industrial machine. Most I/O card have "registers" which are used to program the hardware feature. Each bit in a register has a specific function such as turning on a relay or starting a moter, or reading a limit switch. Such a register can easily been represented by a Delphi set. Each bit correspond to a value in the enumeration which is the base of the set. When you read or write a register in the I/O card, you don't read or write a set but an integer. You have to add some code to transfrom the integer to/from the set so that inside the whole Delphi program, things are readable and easy to handle.

So this is the story. Of course I won't make things complex here and will translate this real world situation to something easier to grasp for anyone not accustomed to industrial control. I will replace the register by a basket with some fruits.

The basket is somewhat special: it can contain zero or more fruits, but at most one fruit of each kind taken from a fruit list.

Translated to Delphi, this gives:
type
 TFruit = (frApple, frPear, frApricot, frCherry);
 TFruits = set of TFruit;
Having those declarations, we can create variables and handle the set of fruits:
var
  Basket : TFruits;
begin
  Basket := [frCherry, frPear];
  Basket := Panier + [frApple];
  if frPear in Basket then
     Memo1.Lines.Add('Basket contain a pear');
end;
This is actually standard Pascal as it has always existed. You'll find a lot of article on the net which will describe the set usage using Pascal.
But Delphi XE3 has much more features than standard Pascal! The one wi'll speak in a moment is the possibility to add methods and properties to a set. I will show you now how to apply this feature to convert our fruit basket into an integer and the reverse.

Let's be clear: there are a lot of ways doing this kind of conversion. I will use thoe one which is the most respectful of the language because it doesn't make any assumption about how the compiler internal represents a set.

The goal is to be able to write this kind of code:

var
   Basket : TFruits;
   N : Integer;
begin
   Basket := [frCherry, frPear];
   // Convert to an integer
   N := Basket.Integer;
   // Convert from an integer
  Basket.Integer := 5;
end;

Look at the notation: "Basket.Integer". It is just like the Basket variable is a class and this class has a property named Integer. But Basket is not a class, it is a set.

Here is how to do that:

type  
  TFruitsHelper = record helper for TFruits
  strict private
    function  ToInteger : Integer;
    procedure FromInteger(Value : Integer);
  public
    property Integer : Integer read  ToInteger
                               write FromInteger;
  end;

implementation

function TFruitsHelper.ToInteger : Integer;
var
   F : TFruit;
begin
   Result := 0;
   for F := Low(TFruit) to High(TFruit) do begin
     if F in Self then
       Result := Result or (1 shl Ord(F));
   end;
end;

procedure TFruitsHelper.FromInteger(Value: Integer);
var
  F : TFruit;
begin
  Self := [];
  for F := Low(TFruit) to High(TFruit) do begin
    if (Value and (1 shl Ord(F))) <> 0 then
      Self := Self + [F];
  end;
end;


That's it! The code assign a single bit into the integer to each of the enumerated type element. To do that, I have used:

  1. A loop for F := Low(TFruit) to High(TFruit) do used to scan the enumeration items without ever specfying any identifier;
  2. The intrinsic function Ord() which returns the index of the element in the enumerated type (frApple=0, frPear=1 and so on);
  3. The operator shl which shift a number to the left. The shithed number is 1 here. The result is a bit set to 1 at the position in the integer given by the index.
  4. The operator in which tells if a given enumeration element is or isn't present in the set.
  5. The operator and which I use to mask the interger bits to know if one specific bit is 1 or not.
Done...

Notes:
  1. This code assume the set is small enough to be contained in an integer (32 bit). You can easily change it to support 64 bit integer. My initial target was to represent an I/O card register and they almost always fit in an integer.
  2. Internally Delphi compiler is using a single bit to represent an enumeration item inside the set. Knowing that, it is possible to change the code to take advantage of it. It is faster but dependent on the internal implementation and could be broken in a future release. The code could look like this:
  function TFruitsHelper.ToInteger: Integer;
  begin
    {$IF SizeOf(TFruits) = SizeOf(Byte)}
      Result := PByte(@Self)^;
    {$ELSEIF SizeOf(TFruits) = SizeOf(Word)}
      Result := PWord(@Self)^;
    {$ELSEIF SizeOf(TFruits) = SizeOf(Integer)}
      Result := PInteger(@Self)^;
    {$ELSE}
      {$MESSAGE FATAL 'TFruits cannot be represented as an integer'}
    {$IFEND}
  end;
--
François Piette
Embarcadero MVP
http://www.overbyte.be



2 comments:

Arioch, the said...

this looks pretty made up.

helpers are made to add compatibility features to the code you have no control over.

In this example natural choice would be variant packed record with implicit typecasts.

this would also provide for other HW common feature: bitfields, which cannot be naturally mapped onto sets at all.

Eduardo Alcântara said...

It is very useful to enumerate option of an ENUM field in a database table.