10

AFAIK there's no built-in function for that. Searching the web I found this function and it works for me, but I prefer not to use it since it's assembly and I can't understand what it is doing. So I wrote this function that also works:

function Cardinality(const PSet: PByteArray;
  const SizeOfSet(*in bytes*): Integer): Integer;
const
  Masks: array[0..7] of Byte = (1, 2, 4, 8, 16, 32, 64, 128);
var
  I, J: Integer;
begin
  Result := 0;
  for I := 0 to SizeOfSet - 1 do
    for J := 0 to 7 do
      if (PSet^[I] and Masks[J]) > 0 then
        Inc(Result);
end;

Now, I want to know if I can rely on this function? Or maybe there's a trick behind the set data type and that's why delphi doesn't have a built-in method for that.

But if my function is reliable then how can I improve it to:

  1. Pass constants to it
  2. Do a type check and make sure that a set is passed to the function
  3. Pass the value instead of its address
  4. Get rid of SizeOfSet parameter

I want to call it like Cardinality(AnySet) instead of Cardinality(@AnySet, SizeOf(TAnySet)).

By the way, I need to compile this in both XE and XE5.

saastn
  • 5,717
  • 8
  • 47
  • 78

2 Answers2

9

You can implement this with generics and RTTI. Like so:

uses
  SysUtils, TypInfo;

type
  ERuntimeTypeError = class(Exception);

  TSet<T> = class
  strict private
    class function TypeInfo: PTypeInfo; inline; static;
  public
    class function IsSet: Boolean; static;
    class function Cardinality(const Value: T): Integer; static;
  end;

const
  Masks: array[0..7] of Byte = (1, 2, 4, 8, 16, 32, 64, 128);

implementation

{ TSet<T> }

class function TSet<T>.TypeInfo: PTypeInfo;
begin
  Result := System.TypeInfo(T);
end;

class function TSet<T>.IsSet: Boolean;
begin
  Result := TypeInfo.Kind=tkSet;
end;

function GetCardinality(const PSet: PByteArray;
  const SizeOfSet(*in bytes*): Integer): Integer; inline;
var
  I, J: Integer;
begin
  Result := 0;
  for I := 0 to SizeOfSet - 1 do
    for J := 0 to 7 do
      if (PSet^[I] and Masks[J]) > 0 then
        Inc(Result);
end;

class function TSet<T>.Cardinality(const Value: T): Integer;
var
  EnumTypeData: PTypeData;
begin
  if not IsSet then
    raise ERuntimeTypeError.Create('Invalid type in TSet<T>, T must be a set');
  Result := GetCardinality(PByteArray(@Value), SizeOf(Value));
end;

Usage:

Writeln(TSet<SomeSet>.Cardinality(Value));
saastn
  • 5,717
  • 8
  • 47
  • 78
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Thanks, but XE doesn't compile it: **E2506 Method of parameterized type declared in interface section must not use local symbol 'GetCardinality'**. So I have to define `GetCardinality` as a class function. Same problem about `Masks`. After those changes it runs and works fine. – saastn Dec 23 '15 at 20:09
  • 2
    Yeah, that's a compiler defect in older compilers. You can inline that function manually if you want. Anyway, the concept is sound. – David Heffernan Dec 23 '15 at 20:34
5

In earlier version of Delphi you can do this :

function Card(ASet: TSet): Integer;
var
  k: TSetElement;
begin
  Result := 0;
  for k in ASet do Inc(Result);
end;
Anonymous
  • 59
  • 1
  • 1
  • Are `TSet` and `TSetElement` general types or types that you defined in your code? – saastn Nov 15 '17 at 11:51
  • those are own types: `type TSetElements=(CardS,CardD,CardC,CardH); //enumerator TSet = set of TSetElements ;` – Max Kleiner Nov 13 '19 at 10:07
  • Without the “function” stuff here, the `for`-`in` procedure is easy to keep in mind and useful in a lot of cases. – Wolf Oct 10 '21 at 12:35