1

I'm making a Delphi VCL application. There is a class TStudent where I have two static functions: one which returns last name from an array of TStudent and another one which returns the first name of the student. Their code is something like:

class function TStudent.FirstNameOf(aLastName: string): string;
var i : integer;
begin
  for i := 0 to Length(studentsArray) - 1 do begin
    if studentsArray[i].LastName = aLastName then
    begin
       result := studentsArray[i].FirstName;
       Exit;
    end;
  end;
  result := 'no match was found';
end;

class function TStudent.LastNameOf(aFirstName: string): string;
var i : integer;
begin
  for i := 0 to Length(studentsArray) - 1 do begin
    if studentsArray[i].FirstName = aFirstName then
    begin
       result := studentsArray[i].LastName;
       Exit;
    end;
  end;
  result := 'no match was found';
end;

My question is how can I avoid writing almost same code twice. Is there any way to pass the property as parameter of the functions.

13
  • How do you know of which student the name will be returned when using these functions? Commented Jun 10, 2014 at 8:46
  • 2
    Why hardcode the name to look for and what about duplicates? Commented Jun 10, 2014 at 8:49
  • I have a global array: studentsArray where I search for coincidence Commented Jun 10, 2014 at 8:50
  • 1
    Still the naming of the functions are confusing, GetFirstName results in the last encountered last name. Commented Jun 10, 2014 at 8:54
  • 1
    LastNameOf and FirstNameOf would be better naming of the functions. Commented Jun 10, 2014 at 9:05

5 Answers 5

4

You can use an anonymous method with variable capture for this linear search. This approach gives you complete generality with your predicate. You can test for equality of any field, of any type. You can test for more complex predicates for instance an either or check.

The code might look like this:

class function TStudent.LinearSearch(const IsMatch: TPredicate<TStudent>; 
  out Index: Integer): Boolean;
var
  i: Integer;
begin
  for i := low(studentsArray) to high(studentsArray) do 
  begin
    if IsMatch(studentsArray[i]) then
    begin
      Index := i;
      Result := True;
      exit;
    end;
  end;

  Index := -1;
  Result := False;
end;

Now all you need to do is provide a suitable predicate. The definition of TPredicate<T>, from the System.SysUtils unit, is:

type
  TPredicate<T> = reference to function (Arg1: T): Boolean;

So you would code your method like this:

class function TStudent.GetFirstName(const LastName: string): string;
var 
  Index: Integer;
  IsMatch: TPredicate<TStudent>;
begin
  IsMatch := 
    function(Student: TStudent): Boolean
    begin
      Result := Student.LastName=LastName;
    end;

  if not LinearSearch(IsMatch, Index) then
  begin
    raise ...
  end;
  Result := studentsArray[Index].FirstName;
end;

And likewise for GetLastName.

If your Delphi does not support anonymous methods then you won't be able to use variable capture and will have to find a more convoluted approach using of object method types. However, the basic idea will be much the same.

Sign up to request clarification or add additional context in comments.

Comments

3

I haven't tested it, but I believe this could be one solution.

uses TypInfo;

class function TStudent.GetProperty( propertyName: string, searchValue : Variant ) : Variant ;
var i : integer;
begin
  for i := 0 to Length(studentsArray) - 1 do begin
    if GetPropValue( studentsArray[i], propertyName ) = searchValue 
       result :=  GetPropValue( studentsArray[i], propertyName );
  end;
  // your code in case of not finding anything

end;

3 Comments

This uses RTTI and compels the user to publish the properties. It also lacks static type checking. That might be fine, but it's worth stating those points up front. FWIW, I would not give up static type checking so readily.
It was the simplest solution, required less coding which came to my mind. And yes, You are right that in this case the properties shall be published. Thanks. :)
You could use Extended RTTI instead (D2010+ only). That not only avoids the published restriction, but is also not restricted to properties either, it works with class members as well.
2

If you are using Delphi 2010 or later, you could use Extended RTTI:

uses
  ..., Rtti;

type
  TStudent = class
  public
    FirstName: String;
    LastName: String;

    class function GetNameOf(const aFieldToFind, aNameToFind, aFieldToReturn: string): string;
   end;

class function TStudent.GetNameOf(const aFieldToFind, aNameToFind, aFieldToReturn: string): string;
var
  i : integer;
  ctx: TRttiContent;
  StudentType: TRttiType;
  Field: TRttiField;
  Value: TValue;
begin
  ctx := TRttiContext.Create;
  StudentType := ctx.GetType(TStudent);
  Field := StudentType.GetField(aFieldToFind);

  for i := 0 to Length(studentsArray) - 1 do
  begin
    if Field.GetValue(@studentsArray[i]).AsString = aNameToFind then
    begin
      Result := StudentType.GetField(aFieldToReturn).GetValue(@studentsArray[i]).AsString;
      Exit;
    end;
  end;
  Result := 'no match was found';
end;

Then you can call it like this:

FirstName := TStudent.GetNameOf('LastName', 'Smoe', 'FirstName');

LastName := TStudent.GetNameOf('FirstName', 'Joe', 'LastName');

Comments

1

If you restructure the TStudent record a little, everything gets easier. Instead of having multiple string fields with different names, declare an array of strings with an enumeration range.

Give the enumeration meaningful names and add a search function where the search field and result field can be specified.

Type   
  TStudentField = (sfFirstName,sfLastName);  // Helper enumeration type

  TStudent = record
    Field: array[TStudentField] of String;
    class function SearchNameOf(searchField: TStudentField; 
      const aSearchName: string; resultField: TStudentField): string; static;
  end;

Here is a test example:

program ProjectTest;

{$APPTYPE CONSOLE}

Type   
  TStudentField = (sfFirstName,sfLastName);

  TStudent = record
    Field: array[TStudentField] of String;
    class function SearchNameOf(searchField: TStudentField; const aSearchName: string; resultField: TStudentField): string; static;
  end;

var
  studentsArray : array of TStudent;

class function TStudent.SearchNameOf(searchField: TStudentField; const aSearchName: string; resultField: TStudentField): string;
var
  i : integer;
begin
  for i := 0 to Length(studentsArray) - 1 do begin
    if (studentsArray[i].Field[searchField] = aSearchName) then
    begin
      Result := studentsArray[i].Field[resultField];
      Exit;
    end;
  end;
  result := 'no match was found';
end;

begin
  SetLength(studentsArray,2);
  studentsArray[0].Field[sfFirstName] := 'Buzz';
  studentsArray[0].Field[sfLastName] := 'Aldrin';
  studentsArray[1].Field[sfFirstName] := 'Neil';
  studentsArray[1].Field[sfLastName] := 'Armstrong';
  WriteLn(TStudent.SearchNameOf(sfFirstName,'Neil',sfLastName));
  ReadLn;
end.

Comments

-3

You could use a several properties with index specifier backed by single getter function just as you do for regular array properties:

  TDefault = class(TObject)
  private
    class function GetProp(const FindWhat: string; FindWhere: Integer): string;
        static;
  protected
    /// <remarks>
    ///   You don't really need this one. I've added it for an illustration
    ///   purposes.
    /// </remarks>
    class property Prop[const FindWhat: string; FindWhere: Integer]: string read GetProp;
  public
    class property A[const FindWhat: string]: string index 0 read GetProp;
    class property B[const FindWhat: string]: string index 1 read GetProp;
  end;

{ ... }

class function TDefault.GetProp(const FindWhat: string; FindWhere: Integer): string;
begin
  case FindWhere of
    0: Result := 'Hallo!';
    1: Result := 'Hello!';
  end;
  Result := Result + ' ' + Format('searching for "%s"', [FindWhat]);
end;

As you see, the class properties are just the same as instance properties.

And I must say its a pretty bad idea to perform a search in the property getter.

1 Comment

I will probably add the search when there will be a stable revision of the question.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.