Computing: Free Pascal Programming

Special data types: Variants, variant records, and variant arrays.

1. Variant records.

I don't remember if Borland Pascal (Turbo Pascal), that I used with my first PC long, long years ago, included the data type Variant, but I remember that it was possible to define variant records, i.e. records that have several structures (different kinds of items), depending on the value of one of their items. The classic example is an employee record, where beside common data items such as ID, name, and first name, there is a variable part concerning their salary, depending on the pay type (some employees being payed monthly, whereas others are payed hourly).

In Pascal, the variable parts of a record are declared within a CASE statement, the "case variable" being the record item, on which the variable part of the record depends (the pay type, in our case). To note, that the usage of the "case variable" in the CASE statement automatically makes it an item of the record; thus, you must not declare it as regular part of the record structure.

Here is my Pascal code to implement the employee record:
    type
        Paytype = (monthly, hourly);
    var
        Employee: record
            ID: Integer;
            Name, Firstname: string;
            case Payclass: Paytype of
                monthly: (Monthlyrate: Real);
                hourly: (RatePerHour: Real; RegHours, Overtime: Integer);
        end;

Note: In this example, I used the custom enumeration type Paytype (enumerations are a standard Pascal feature); you may use a character or integer code instead, if you like. Important to note, that the CASE statement used within a record declaration is not terminated with END (as is the statement used within the instructions part of the program)!

Here is the code of a small command line program, that shows how to create "employee" records, and how to calculate the employees' salary.
    program employees;
    type
        TPaytype = (monthly, hourly);
        TEmployee = record
            ID: Integer;
            Name, Firstname: string;
            Department, Phone: Integer;
            case Paytype: TPaytype of
                monthly: (MonthlyRate: Real);
                hourly: (RatePerHour: Real; RegHours, Overtime : Integer);
        end;
    var
        I: Integer;
        Salary: Real;
        Fullname: string;
        AllEmployees: array[1..2] of TEmployee;
    begin
        with AllEmployees[1] do begin
            ID := 211;
            Name := 'Feuerstein'; Firstname := 'Fred';
            Department := 3; Phone := 304;
            Paytype := monthly;
            MonthlyRate := 4500;
        end;
        with AllEmployees[2] do begin
            ID := 217;
            Name := 'Feuerstein'; Firstname := 'Wilma';
            Department := 3; Phone := 305;
            Paytype := hourly;
            RatePerHour := 22.50; RegHours := 184; Overtime := 15;
        end;
        Writeln('Salaries for this month:');
        for I := 1 to 2 do begin
            Fullname := AllEmployees[I].Name + ' ' + AllEmployees[I].Firstname;
            if AllEmployees[I].Paytype = monthly then
                Salary := AllEmployees[I].MonthlyRate
            else
                Salary := AllEmployees[I].RatePerHour * (AllEmployees[I].RegHours + AllEmployees[I].Overtime);
            Writeln(' ', Fullname:25, ': ', Salary:0:2);
        end;
        Writeln; Write('ENTER to terminate '); Readln;
    end.

Important: It's the responsibility of the programmer that the correct pay type is set in the record. Also, it is mandatory to test the "case variable" when accessing the record to retrieve data from its variable part. In fact, FPC doesn't generate any validation code at all, and accessing non-existing data will (at least mostly) not result in a runtime error, but in the retrieval of erroneous values. Thus, when I calculated the salary of Wilma as monthly, I got 25.5 (the hourly rate); when I calculated the salary of Fred as hourly, I got 0 (probably because RegHours and Overtime are zero, instead of being undefined).

2. Variants.

Free Pascal includes the data type Variant, that allows variables to be assigned values of different types. Thus, a Variant variable may have, among others, values that are either Integer (and derivatives), Real, Double, Char, Boolean, and string. Variants may not be assigned arrays, or records.

This may largely simplify things in certain situations. Suppose, for example, that we want to write a procedure that swaps two variables (i.e. the first variable will get the value of the second, and vice-versa). Without Variant, we would have to write several procedures, one for each data type. Declaring the arguments of the procedure as type Variant, we may pass numbers, characters, Boolean, or strings to the same procedure.

Here is the code of my "Swap" procedure:
    procedure Swap(var A, B: Variant);
    var
        C: Variant;
    begin
        C := A; A := B; B := C;
    end;

Note: The code concerning the Variant data type is part of the unit Variants. If you build your project without including this unit, you'll get the warning message Implicit uses of Variants unit. To be sure to avoid all problems, add USES VARIANTS; to your program.

Here is a small program to test the "Swap" procedure:
    program test_swap;
    uses
        Variants;
    var
        I1, I2, R1, R2, C1, C2, S1, S2: Variant;
    // Procedure to swap two variables
    procedure Swap(var A, B: Variant);
    var
        C: Variant;
    begin
        C := A; A := B; B := C;
    end;
    // Main program
    begin
        I1 := 1; I2 := 2;
        R1 := 0.1; R2 := 0.2;
        C1 := 'a'; C2 := 'b';
        S1 := 'first string'; S2 := 'second string';
        Writeln('Testing the usage of "variants".');
        Write('Testing "Swap" procedure for Integer ', I1, ' and ', I2, '; result = ');
        Swap(I1, I2); Writeln(I1, ' and ', I2);
        Write('Testing "Swap" procedure for Real ', R1, ' and ', R2, '; result = ');
        Swap(R1, R2); Writeln(R1, ' and ', R2);
        Write('Testing "Swap" procedure for Char ', C1, ' and ', C2, '; result = ');
        Swap(C1, C2); Writeln(C1, ' and ', C2);
        Write('Testing "Swap" procedure for string ', S1, ' and ', S2, '; result = ');
        Swap(S1, S2); Writeln(S1, ' and ', S2);
        Writeln; Write('ENTER to terminate '); Readln;
    end.

Have you noticed that I have declared all variables in the main program as Variant? Couldn't we declare I1 and I2 as Integer, R1 and R2 as Real, etc? We can't! If we do so, the build aborts with the error message Call by var has to match exactly. For Integer I1 and I2, we would get LongInt, where Variant is expected.

This seems to be a little problem, because in a real world program, your variables will probably be declared belonging to a "regular" data type. But, there is a simple work-around: Assign your Integer, Real, etc to a Variant before you call Swap and then assign the resulting Variant to the Integer variables again. Example, with I1 and I2 being declared as Integer, and V1 and V2 as Variant:
    Write('Testing "Swap" procedure for Integer ', I1, ' and ', I2, '; result = ');
    V1 := I1; V2 := I2;
    Swap(V1, V2);
    I1 := V1; I2 := V2;
    Writeln(I1, ' and ', I2);

Whereas assigning a "regular" data type to a Variant is always ok, the assignment of a Variant to a "regular" data type variable may cause problems. What happens, for example, if the Variant contains a Real and we assign it to an Integer variable? Or, if the Variant contains a string? In the first case, the program terminates normally, the Integer variable containing the value 0 (why?). In the second case, the program aborts with the runtime error EVariant Error: Invalid variant type cast (note, that if you run the program by double-clicking it, instead of running it from the Command Prompt window, you will not see this error, because the window closes when the runtime error occurs).

Runtime errors may be caught by a TRY ... EXCEPT construct. There are several error conditions defined in the Variants unit, I'm not sure however if they work as they should (EVariantTypeCastError was not triggered when I tried to assign a Variant containing a string to an Integer variable). On the other hand, the generic error condition ON E: Exception DO works fine. Note, that you'll have to include the unit SysUtils in your program in order to use it.

Arrays of Variant.

As I said, it is not possible to assign an array to a Variant variable, but nothing forbids that the elements of an array are of type Variant. Here is the code of a simple bubble sort program; its "Sort" procedure may be used to sort an array of Integer, Real, Char, as well as string.
    program bubblesort;
    uses
        Variants;
    const
        ArrI: array of Integer = (3, 5, 1, 9, 2, 7, 0, 8, 4, 6);
        ArrC: array of Char = ('c', 'k', 'i', 'j', 'b', 'l', 'a', 'g', 'd', 'f', 'e', 'h');
        ArrS: array of string = ('New York', 'Chicago', 'Los Angeles', 'Washington', 'San Fransisco', 'Las Vegas');
    var
        I: Integer;
        ArrV: array of Variant;
    // Bubble sort of an array
    procedure Sort(var Arr: array of Variant);
    var
        I, J: Integer;
        V: Variant;
    begin
        for I := 0 to Length(Arr) - 2 do begin
            for J := I + 1 to Length(Arr) - 1 do begin
                if Arr[I] > Arr[J] then begin
                    V := Arr[I]; Arr[I] := Arr[J]; Arr[J] := V;
                end;
            end;
        end;
    end;
    // Display of an array
    procedure Display(var Arr: array of Variant);
    var
        I: Integer;
    begin
        for I := 0 to Length(Arr) - 1 do
            Write(Arr[I], ' ');
        Writeln;
    end;
    // Main program
    begin
        SetLength(ArrV, Length(ArrI));
        for I := 0 to Length(ArrI) - 1 do
            ArrV[I] := ArrI[I];
        Sort(ArrV); Display(ArrV);
        SetLength(ArrV, Length(ArrC));
        for I := 0 to Length(ArrC) - 1 do
            ArrV[I] := ArrC[I];
        Sort(ArrV); Display(ArrV);
        SetLength(ArrV, Length(ArrS));
        for I := 0 to Length(ArrS) - 1 do
            ArrV[I] := ArrS[I];
        Sort(ArrV); Display(ArrV);
        Writeln; Write('ENTER to terminate '); Readln;
    end.

Note: The array elements have to be assigned individually; what I mean is that it is not possible to assign the array of Integer, Char, etc directly to the array of Variant. If you do so, you'll get the compiler error message Incompatible types.

The Variants unit contains a whole bunch of functions and procedures (cf. Reference for unit 'Variants' at the Free Pascal documentation website for details). Worth to mention here, the possibility to return the actual type of the value contained in a Variant variable. This may be done, using the functions VarType and VarTypeAsText. For example, lets add the following lines after the last call to "Sort" in the program above
    Writeln(VarType(ArrV[0]));
    Writeln(VarTypeAsText(VarType(ArrV[0])));
The value's type, in this case, would be (displayed as) "256" resp. "String" (note the uppercase "S"; why?).

3. Variant arrays.

In the section before, I described arrays of Variant, which are data structures of type array, the array elements being of type Variant, the values of the elements being of any data type that may be assigned to a Variant variable (Integer, Real, etc). A variant array is something completely different. It's an array data structure of type Variant, the array elements being of the type that is indicated when the array is created (these arrays are declared as Variant and then have to be created in a similar way than you do for objects), and the values of the elements must be of this data type. There are several ways to create a variant array, as explained in the following paragraphs.

Creating a variant array using VarArrayOf.

The syntax of VarArrayOf is as follows:
    V := VarArrayOf(Values);
where V is a variable of type Variant and Value is an array of Variant.

VarArrayOf creates a variant array with elements of type varVariant. The array has as many values as there are elements in Values and the element values are copied from Values. Important to note that Values must be an array of Variant (not an array of Integer, etc)!

Whereas arrays of Variant may be really useful, variant arrays are structures that are very rarely used in real life. That's at least, what I assume. In fact, I did not find a practical case to give as example program here. Thus, some code, that does not really make sense, but that illustrates how to create and use a variant array. The "Display" procedure of the sample program displays the name and the age of a woman, together with the names of her children. The name is passed as a string, the age as an Integer, and the children as a Variant. This Variant is used here to make it possible to call the procedure with, as 3rd argument, the string "none" (if the woman has no children), with a string indicating a name (if the woman has one single child), or with an array of string values (if the woman has several children). Here is the code:
    program vararrays1;
    uses
        Variants;
    const
        Names: array[1..4] of string = ('Mrs Smith', 'Mrs Jones', 'Mrs Marple', 'Mrs Flintstone');
        Ages: array[1..4] of Integer = (45, 39, 61, 28);
        AllChildren: array[1..4] of array of string = (
            ('Kyra', 'Melissa', 'Jonathan'),
            ('Angelina'),
            (),
            ('Bart', 'Lisa')
        );
    var
        I, J: Integer;
        TheseChildren: array of string;
        TheseChildrenV: array of Variant;
        Children: Variant;
    // "Display" procedure
    procedure Display(Name: string; Age: Integer; Children: Variant);
    var
        I: Integer;
    begin
        Writeln ('Her name is ', Name, ' and she is ', Age, ' years old.');
        if VarTypeAsText(VarType(Children)) = 'String' then begin
            if Children = 'none' then
                Writeln('She has no children.')
            else
                Writeln('Name of her child: ', Children, '.');
        end
        else begin
            Write('Names of her children: ');
            for I := VarArrayLowBound(Children, 1) to VarArrayHighBound(Children, 1) do begin
                Write(Children[I]);
                if I < VarArrayHighBound(Children, 1) then
                    Write(', ')
                else
                    Writeln('.');
            end;
        end;
    end;
    // Main program
    begin
        for I := 1 to 4 do begin
            TheseChildren := AllChildren[I];
            if Length(TheseChildren) = 0 then
                Children := 'none'
            else if Length(TheseChildren) = 1 then
                Children := TheseChildren[0]
            else begin
                SetLength(TheseChildrenV, Length(TheseChildren));
                for J := 0 to Length(TheseChildren) - 1 do
                    TheseChildrenV[J] := TheseChildren[J];
                Children := VarArrayOf(TheseChildrenV);
            end;
            Display(Names[I], Ages[I], Children);
        end;
        Write('ENTER to terminate '); Readln;
    end.

The screenshot shows the program output.

Sample program output

As the "children" argument of the "Display" procedure may either be a string, or an array, it's obvious that its data type must be Variant. Assigning the literal 'none' or an element of the array of string containing the children names for this woman is no problem. Assigning several children as an array is somewhat more complicated. First, it is not possible to directly assign an array to a Variant variable; we'll have to use a variant array instead. If "Children" is a Variant, and TheseChildren is an array of string (containing the children of this woman), trying Children = VarArrayOf(TheseChildren) results in the error message Incompatible types. The VarArrayOf function requires an array of Variant (does not accept an array of string). Resolving this by using the declaration AllChildren: array[1..4] of array of Variant = ( ... ) is not possible, as typed constants of classes or interfaces are not allowed. And finally, declaring TheseChildren as an array of Variant and trying to assign AllChildren[I] to it, doesn't work either, because array of Variant and array of string are not compatible in assignments. So, what we have to do, is to create two arrays: an array of string (TheseChildren), used to retrieve the array of this woman's children from the two-dimensional array AllChildren, and an array of Variant (TheseChildrenV) where we copy the children to (this has to be done element by element) and that we then use to create the variant array, that we assign to the Variant variable (Children) to be passed to the "Display" procedure. Here are the relevant declarations:
    const
        AllChildren: array[1..4] of array of string = ( ... );
    var
        TheseChildren: array of string;
        TheseChildrenV: array of Variant;
        Children: Variant;
And here is the relevant code:
    TheseChildren := AllChildren[I];
    SetLength(TheseChildrenV, Length(TheseChildren));
    for J := 0 to Length(TheseChildren) - 1 do
       TheseChildrenV[J] := TheseChildren[J];
    Children := VarArrayOf(TheseChildrenV);
If you get a runtime error, there are chances that you forgot to set the length of the TheseChildrenV array...

To iterate the elements of a variant array, we need to know the array's bounds. Whereas the lower bound is usually 0, the upper bound may be variable, and thus not known. There are two functions to get the bounds values: VarArrayLowBound and VarArrayHighBound, as used in our "Display" procedure.

Creating a variant array using VarArrayCreate.

VarArrayCreate creates a (single- or multi-dimensional) array with lower and upper bounds as specified; 2 bounds for every dimension of the array are required. All elements of the array are of the same type. The syntax of VarArrayCreate is as follows:
    V := VarArrayCreate(Bounds, VarType);
where V is a variable of type Variant, Bounds is an array (of Integer), defining the lower and upper bounds of the variant array, and VarType is a value of type tvartype, defining the data type of the elements.

Examples:
    V1 := VarArrayCreate([0, 9], varSingle));
    V2 := VarArrayCreate([1, 10], varDouble);
    V3 := VarArrayCreate([0, 1, 0, 9], varInteger);
    V4 := VarArrayCreate([0, 9], varVariant);
    V5 := VarArrayCreate([0, 9], varString);
V1 is a 0-based one-dimensional array with single precision real elements, V2 is a 1-based one-dimensional array with double precision real elements. V3 is a 2×10 two-dimensional array with Integer elements. V4 is a one-dimensional array with Variant elements. V5 does not work: trying to create this array, you'll get the runtime error Variant array cannot be created; simple work-around: create a variant array of Variant instead.

Some further code, that does not really make sense, but that illustrates how to create a variant array using VarArrayCreate. The "Display" procedure of the sample program displays the name and age of a woman, together with the ages of her children. The name is passed as a string, the age as an Integer, and the children ages as a Variant. This Variant is used here to make it possible to call the procedure with, as 3rd argument, the string "n/a" (if the woman has no children), with an integer (indicating the age of a single child), or with an array of Integer (if the woman has several children). Here is the code:
    program vararrays2;
    uses
        Variants;
    const
        Names: array[1..4] of string = ('Mrs Smith', 'Mrs Jones', 'Mrs Marple', 'Mrs Flintstone');
        Ages: array[1..4] of Integer = (45, 39, 61, 28);
        AllChildrenAges: array[1..4] of array of Integer = (
            (20, 23, 28),
            (12),
            (),
            (5, 2)
        );
    var
        I, J: Integer;
        TheseChildrenAges: array of Integer;
        ChildrenAges: Variant;
    // "Display" procedure
    procedure Display(Name: string; Age: Integer; ChildrenAges: Variant);
    var
        I: Integer;
    begin
        Writeln ('Her name is ', Name, ' and she is ', Age, ' years old.');
        if VarTypeAsText(VarType(ChildrenAges)) = 'String' then
            Writeln('She has no children.')
        else if VarTypeAsText(VarType(ChildrenAges)) = 'Integer' then
            Writeln('Age of her child: ', ChildrenAges, '.')
        else begin
            Write('Ages of her children: ');
            for I := VarArrayLowBound(ChildrenAges, 1) to VarArrayHighBound(ChildrenAges, 1) do begin
                Write(ChildrenAges[I]);
                if I < VarArrayHighBound(ChildrenAges, 1) then
                    Write(', ')
                else
                    Writeln('.');
            end;
        end;
    end;
    // Main program
    begin
        for I := 1 to 4 do begin
            TheseChildrenAges := AllChildrenAges[I];
            if Length(TheseChildrenAges) = 0 then
                ChildrenAges := 'n/a'
            else if Length(TheseChildrenAges) = 1 then
                ChildrenAges := TheseChildrenAges[0]
            else begin
                ChildrenAges := VarArrayCreate([0, Length(TheseChildrenAges)], varInteger);
                ChildrenAges := TheseChildrenAges;
            end;
            Display(Names[I], Ages[I], ChildrenAges);
        end;
        Write('ENTER to terminate '); Readln;
    end.

A variant array, created with VarArrayOf is always an array of Variant, whereas an array created with VarArrayCreate may have elements of "regular" data types (but not of type string). In our example, the children ages are integers, thus we create an array with elements of type varInteger. Whereas the array created with VarArrayOf is created from another array (that must be an array of Variant) and its elements are copied from this array, the array created with VarArrayCreate is empty, and we have to fill it after it has been created. Copying the elements from an array of Integer to a variant array with varInteger elements may be done by a simple assignment. Here are the relevant declarations in our program above:
    const
        AllChildrenAges: array[1..4] of array of Integer = ( ... );
    var
        TheseChildrenAges: array of Integer;
        ChildrenAges: Variant;
And here is the relevant code:
    TheseChildrenAges := AllChildrenAges[I];
    ChildrenAges := VarArrayCreate([0, Length(TheseChildrenAges)], varInteger);
    ChildrenAges := TheseChildrenAges;
It's obviously shorter and simpler than all these data manipulation when using VarArrayOf...

Creating a variant array using DynArrayToVariant.

A third way to create a variant array is to use the procedure DynArrayToVariant that converts a dynamic array D to a variant array V. It uses the type information in TypeInfo (compiler intrinsic) to calculate the number of dimensions, as well as the array lengths and the type of the elements. The dynamic arrays used may only contain values of basic data types. If there is no data, an empty Variant will be returned.

This seems to be the simplest way to create a variant array (at least if there is already a filled-in dynamic array available). Unfortunately, I did not succeed to figure out how this works, and I did not find any related code samples on the Internet. If someone has a (correctly working) example, please, contact me by email.


If you find this text helpful, please, support me and this website by signing my guestbook.