delphidabbler / code-snippets

Collections of reusable code snippets, mainly in Pascal.
MIT License
27 stars 2 forks source link

Add generic Max, Min, Mid routines to csdb collection. #8

Open delphidabbler opened 2 years ago

delphidabbler commented 2 years ago

There are lots of overloaded Max, Min and Mid routines in the Maths category of the csdb collection, but it's crying out for some generic versions.

⚠️ Will need to check if CodeSnip can handle generic routines first.

delphidabbler commented 1 year ago

Here's an example function

function Max<T>(const A: array of T; Compare: TComparison<T>): T; overload;
begin
  Assert(Length(A) > 0);
  Result := A[Low(A)];
  for var I := Succ(Low(A)) to High(A) do
  begin
    if Compare(A[I], Result) > 0 then
      Result := A[I];
  end;
end;
delphidabbler commented 1 year ago

Here's an example function

function Max<T>(const A: array of T; Compare: TComparison<T>): T; overload;
begin
  Assert(Length(A) > 0);
  Result := A[Low(A)];
  for var I := Succ(Low(A)) to High(A) do
  begin
    if Compare(A[I], Result) > 0 then
      Result := A[I];
  end;
end;

From what I've read generic global functions can't be declared, so it looks as though the generic functions may need to be grouped together in a static class or record, e.g.:

type
  TMinMax = record
    class function Max<T>(const A: array of T; Compare: TComparison<T>): T; overload; static;
    class function Min<T>(const A: array of T; Compare: TComparison<T>): T; overload; static;
    class function Max<T>(const A, B: T; Compare: TComparison<T>): T; overload; static;
    class function Min<T>(const A, B: T; Compare: TComparison<T>): T; overload; static;
  end;

The reason for putting the type parameter on the metho, rather than the records is so that non-generic methods can be included. For example:

type
  TMinMax = record
    class function Max<T>(const A: array of T; Compare: TComparison<T>): T; overload; static;
    // ...
    class function Max(const A: array of Integer): Integer; overload; static;
  end;
delphidabbler commented 1 year ago

⚠️ Will need to check if CodeSnip can handle generic routines first.

Can't declare a function or procedure with generic type - instead need to declare a static class or record with generic methods.

Tested a record with generic methods in CodeSnip and it compiled in Delphi 11.

delphidabbler commented 1 year ago

Could generalise this for TArray<T> by providing a Select method that is passed a selector closure:

type
  TElemSelector = record
  public
    type
      TSelectFn<T> = reference to function(const ACurrent, ACandidate: T; AComparer: IComparer<T>): T;
    class function Select<T>(const A: array of T; ASelector: TSelectFn<T>; AComparer: IComparer<T> = nil): T; static;
    class function Max<T>(const A: array of T; AComparer: IComparer<T> = nil): T; static;
    // ... etc
  end;

class function TElemSelector.Select<T>(const A: array of T; ASelector: TSelectFn<T>; AComparer: IComparer<T> = nil): T; 
begin
  Assert(Length(A) > 0);
  if not Assigned(AComparer) then
    AComparer := TComparer<T>.Default; 
  Result := A[Low(A)];
  for var I := Succ(Low(A)) to High(A) do
  begin
    Result := ASelector(Result, A[I], AComparer);
  end;
end;

class function TElemSelector.Max<T>(const A: array of T; AComparer: IComparer<T>): T;
begin
  Result := Select(
    A, 
    function(const ACurrent, ACandidate: T; AComparer: IComparer<T>): T
    begin
      if AComparer.Compare(ACurrent, ACandidate) > 0 the
        Result := ACurrent
      else
        Result := ACandidate;
    end,
    AComparer
  );
end
delphidabbler commented 1 year ago

Could generalise this for TArray<T> by providing a Select method that is passed a selector closure:

Thinking about this further the Select<T> method doesn't generalise too well, because all the selector fn can do it can do is choose a value based on the comparer, which is pretty much to choose the smallest or greatest, which is what Min & Max do.

delphidabbler commented 1 year ago

Thinking about this further the Select<T> method doesn't generalise too well, because all the selector fn can do it can do is choose a value based on the comparer, which is pretty much to choose the smallest or greatest, which is what Min & Max do.

It would be better to create a differently named record with methods that is more generalised that has array operations based on those of JavaScript. E.g.s forEach(), some(), all() & map().

delphidabbler commented 1 year ago

It's difficult to see how to generalise Mid to an array. What if the array has an even number of elements? Do we average the middle two? If we do and the elements are integral then result may be real.

The easiest way of finding the mid value in an array is to sort the array and pick the middle element if the array length is odd or to average the middle two elements if even. This is the same as taking the median value if the array. This is dealt with in issue #16.

❗ For the above reasons we won't be adding a Mid function for arrays.