Jump to content
bravesofts

Introducing TRange<T>

Recommended Posts

unit API.Utils;

interface

uses
  System.SysUtils,
  System.Types,
  System.Generics.Defaults;

type
  TRange<T> = class
  public
    // Check if a value is within the range [aMin, aMax] using a custom comparer
    class function IsIn(const aValue, aMin, aMax: T; const aComparer: IComparer<T>): Boolean; overload; static;

    // Check if a value is within the range [aMin, aMax] using the default comparer
    class function IsIn(const aValue, aMin, aMax: T): Boolean; overload; static;
  end;

implementation

{ TRange<T> }

class function TRange<T>.IsIn(const aValue, aMin, aMax: T; const aComparer: IComparer<T>): Boolean;
begin
  Result := (aComparer.Compare(aValue, aMin) >= 0) and (aComparer.Compare(aValue, aMax) <= 0);
end;

class function TRange<T>.IsIn(const aValue, aMin, aMax: T): Boolean;
begin
  Result := IsIn(aValue, aMin, aMax, TComparer<T>.Default);
end;

end.

to put this  Super class in test i build a new console project:
this unit here act as my objects:

unit API.Objects.Comparer;

interface
uses
  System.Types,
  System.Generics.Defaults;

type
  ICustomRecord = interface; // Forward
  ICustomRecordUpdate = interface
    function Edit(const aName: string; const aValue: Integer): ICustomRecord;
  end;
  ICustomRecord = interface
    function GetName: string;
    function GetValue: Integer;
    function GetCustomRecordUpdate: ICustomRecordUpdate;

    property Name: string read GetName;
    property Value: Integer read GetValue;

    property New: ICustomRecordUpdate read GetCustomRecordUpdate;
  end;

  IProduct = interface; // Forward
  IProductUpdate = interface
    function Edit(const aID: Integer; const aPrice: Currency): IProduct;
  end;
  IProduct = interface
    function GetID: Integer;
    function GetPrice: Currency;
    function GetIProductUpdate: IProductUpdate;

    property ID: Integer read GetID;
    property Price: Currency read GetPrice;

    property New: IProductUpdate read GetIProductUpdate;
  end;

  IClient = interface; // Forward
  IClientUpdate = interface
    function Edit(const aName: string; const aAge: Integer): IClient;
  end;
  IClient = interface
    function GetName: string;
    function GetAge: Integer;
    function GetIClientUpdate: IClientUpdate;

    property Name: string read GetName;
    property Age: Integer read GetAge;

    property New: IClientUpdate read GetIClientUpdate;
  end;

// Compare Custom Records <Helper function>
function CompareCustomRecord(const R1, R2: ICustomRecord): Integer;

// Compare Products by thier Prices <Helper function>
function CompareProductByPrice(const P1, P2: IProduct): Integer;

// Compare Clients by thier Ages <Helper function>
function CompareClientByAge(const C1, C2: IClient): Integer;

// points comparison <Helper functions>
function ComparePoints(const P1, P2: TPoint): Integer; overload;
function ComparePoints(const P1, P2: TPointF): Integer; overload;

// Returns a custom comparer for TPoint
function PointComparer: IComparer<TPoint>;

function GetTCustomRecord(const aName: string; aValue: Integer): ICustomRecord;
function GetTProduct(aID: Integer; aPrice: Currency): IProduct;
function GetTClient(const aName: string; aAge: Integer): IClient;

implementation
uses
  System.Math;

type
  TCustomRecord = class(TInterfacedObject, ICustomRecord, ICustomRecordUpdate)
  strict private
    fName: string;
    fValue: Integer;
    function GetName: string;
    function GetValue: Integer;
    function GetCustomRecordupdate: ICustomRecordUpdate;
    function Edit(const aName: string; const aValue: Integer): ICustomRecord;
  public
    constructor Create(const aName: string; aValue: Integer);
  end;

  TProduct = class(TInterfacedObject, IProduct, IProductUpdate)
  private
    fID: Integer;
    fPrice: Currency;
    function GetID: Integer;
    function GetPrice: Currency;
    function GetIProductUpdate: IProductUpdate;
    function Edit(const aID: Integer; const aPrice: Currency): IProduct;
  public
    constructor Create(aID: Integer; aPrice: Currency);
  end;

  TClient = class(TInterfacedObject, IClient, IClientUpdate)
  private
    fName: string;
    fAge: Integer;
    function GetName: string;
    function GetAge: Integer;
    function GetIClientUpdate: IClientUpdate;
    function Edit(const aName: string; const aAge: Integer): IClient;
  public
    constructor Create(const aName: string; aAge: Integer);
  end;

function GetTCustomRecord(const aName: string; aValue: Integer): ICustomRecord;
begin
  Result := TCustomRecord.Create(aName, aValue);
end;

function GetTProduct(aID: Integer; aPrice: Currency): IProduct;
begin
  Result := TProduct.Create(aID, aPrice);
end;

function GetTClient(const aName: string; aAge: Integer): IClient;
begin
  Result := TClient.Create(aName, aAge);
end;

{$REGION '  Points Comparer & Helper Functions .. '}
function ComparePoints(const P1, P2: TPoint): Integer;
begin
  if P1.X < P2.X then
    Exit(-1)
  else if P1.X > P2.X then
    Exit(1);

  if P1.Y < P2.Y then
    Exit(-1)
  else if P1.Y > P2.Y then
    Exit(1);

  Result := 0; // Points are equal
end;

function ComparePoints(const P1, P2: TPointF): Integer;
begin
  if P1.X <> P2.X then
    Result := Sign(P1.X - P2.X)
  else
    Result := Sign(P1.Y - P2.Y);
end;

function PointComparer: IComparer<TPoint>;
begin
  Result := TComparer<TPoint>.Construct(
    function(const P1, P2: TPoint): Integer
    begin
      Result := ComparePoints(P1, P2);
    end
  );
end;
{$ENDREGION}

{ Helper CustomRecord function }

function CompareCustomRecord(const R1, R2: ICustomRecord): Integer;
begin
  Result := R1.Value - R2.Value;
end;

{ Helper ProductByPrice function }

function CompareProductByPrice(const P1, P2: IProduct): Integer;
begin
  if P1.Price < P2.Price then
    Result := -1
  else if P1.Price > P2.Price then
    Result := 1
  else
    Result := 0;
end;

{ Helper ClientByAge function }

function CompareClientByAge(const C1, C2: IClient): Integer;
begin
  Result := C1.Age - C2.Age;
end;

{ TCustomRecord }

{$REGION '  TCustomRecord .. '}
constructor TCustomRecord.Create(const aName: string; aValue: Integer);
begin
  fName  := aName;
  fValue := aValue;
end;

function TCustomRecord.GetName: string;
begin
  Result := fName;
end;

function TCustomRecord.GetValue: Integer;
begin
  Result := fValue;
end;

function TCustomRecord.GetCustomRecordupdate: ICustomRecordUpdate;
begin
  Result := Self as ICustomRecordUpdate;
end;

function TCustomRecord.Edit(const aName: string;
  const aValue: Integer): ICustomRecord;
begin
  fName  := aName;
  fValue := aValue;
end;
{$ENDREGION}

{ TProduct }

{$REGION '  TProduct .. '}
constructor TProduct.Create(aID: Integer; aPrice: Currency);
begin
  fID    := aID;
  fPrice := aPrice;
end;

function TProduct.GetID: Integer;
begin
  Result := fID;
end;

function TProduct.GetPrice: Currency;
begin
  Result := fPrice;
end;

function TProduct.GetIProductUpdate: IProductUpdate;
begin
  Result := Self as IProductUpdate;
end;

function TProduct.Edit(const aID: Integer; const aPrice: Currency): IProduct;
begin
  fID    := aID;
  fPrice := aPrice;
end;
{$ENDREGION}

{ TClient }

{$REGION '  TClient .. '}
constructor TClient.Create(const aName: string; aAge: Integer);
begin
  fName := aName;
  fAge  := aAge;
end;

function TClient.GetName: string;
begin
  Result := fName;
end;

function TClient.GetAge: Integer;
begin
  Result := fAge;
end;

function TClient.GetIClientUpdate: IClientUpdate;
begin
  Result := Self as IClientUpdate;
end;

function TClient.Edit(const aName: string; const aAge: Integer): IClient;
begin
  fName := aName;
  fAge  := aAge;
end;
{$ENDREGION}

end.

now here is my dpr console code:

program RangChecker;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.Types,
  DateUtils,
  System.Generics.Defaults,
  API.Utils in 'API\API.Utils.pas',
  API.Objects.Comparer in 'API\API.Objects.Comparer.pas';

var
  gPoint1, gPoint2, gPoint3: TPoint;

  gRec1, gRec2, gRec3: ICustomRecord;
  gRecordComparer: IComparer<ICustomRecord>;

  gProduct1, gProduct2, gProduct3: IProduct;
  gProductComparer: IComparer<IProduct>;

  gClient1, gClient2, gClient3: IClient;
  gClientComparer: IComparer<IClient>;

  gEndDateStr: string;
begin
  gPoint1 := TPoint.Create(1, 2);
  gPoint2 := TPoint.Create(0, 0);
  gPoint3 := TPoint.Create(3, 4);

  gRec1 := GetTCustomRecord('Low', 10);
  gRec2 := GetTCustomRecord('Mid', 20);
  gRec3 := GetTCustomRecord('High', 30);
  gRecordComparer := TComparer<ICustomRecord>.Construct(CompareCustomRecord);

  gProduct1 := GetTProduct(1, 10.0);
  gProduct2 := GetTProduct(2, 20.0);
  gProduct3 := GetTProduct(3, 30.0);
  gProductComparer := TComparer<IProduct>.Construct(CompareProductByPrice);

  gClient1 := GetTClient('Alice', 25);
  gClient2 := GetTClient('Bob', 30);
  gClient3 := GetTClient('Charlie', 35);
  gClientComparer := TComparer<IClient>.Construct(CompareClientByAge);

  with FormatSettings do begin
    ShortDateFormat := 'DD MMMM YYYY';
    CurrencyString := 'DA';
    DecimalSeparator := ',';
    ThousandSeparator := '.';
  end;
  gEndDateStr := DateToStr(Today +10, FormatSettings);

  try
    Writeln('-----------------<< Integer Tests >>--------------------------------');
  {$REGION '  Integer Tests .. '}
    if TRange<Integer>.IsIn(5, 1, 10) then
      Writeln('5 is within the range [1, 10]')
    else
      Writeln('5 is outside the range [1, 10]');

    if TRange<Integer>.IsIn(5, 6, 10) then
      Writeln('5 is within the range [6, 10]')
    else
      Writeln('5 is outside the range [6, 10]');
  {$ENDREGION}

    Writeln('-----------------<< Int64 Tests >>--------------------------------');
  {$REGION '  Int64 Tests .. '}
    if TRange<Int64>.IsIn(5_000_000_000_000_000_001, 5_000_000_000_000_000_000, 5_000_000_000_000_000_010) then
      Writeln('5_000_000_000_000_000_001 is within the range [5_000_000_000_000_000_000, 5_000_000_000_000_000_010]')
    else
      Writeln('5 is outside the range [5_000_000_000_000_000_000, 5_000_000_000_000_000_010]');

    if TRange<Int64>.IsIn(5_000_000_000_000_000_000, 5_000_000_000_000_000_001, 5_000_000_000_000_000_010) then
      Writeln('5_000_000_000_000_000_000 is within the range [5_000_000_000_000_000_001, 5_000_000_000_000_000_010]')
    else
      Writeln('5_000_000_000_000_000_000 is outside the range [5_000_000_000_000_000_001, 5_000_000_000_000_000_010]');
  {$ENDREGION}

    Writeln('-----------------<< Float Tests >>----------------------------------');
  {$REGION '  Float Tests .. '}
    if TRange<Double>.IsIn(7.5, 5.0, 10.0) then
      Writeln('7.5 is within the range [5.0, 10.0]')
    else
      Writeln('7.5 is outside the range [5.0, 10.0]');

    if TRange<Double>.IsIn(7.5, 7.6, 10.0) then
      Writeln('7.5 is within the range [7.6, 10.0]')
    else
      Writeln('7.5 is outside the range [7.6, 10.0]');
  {$ENDREGION}

    Writeln('-----------------<< DateTime Tests >>------------------------------');
  {$REGION '  DateTime Tests .. '}
    if TRange<TDateTime>.IsIn(Today, Today, Today +10) then
      Writeln('Today is within ['+Today.ToString+'] and ['+gEndDateStr+']')
    else
      Writeln('Today is outside ['+Today.ToString+'] and ['+gEndDateStr+']');

    if TRange<TDateTime>.IsIn(Yesterday, Today, Today +10) then
      Writeln('Yesterday is within ['+Today.ToString+'] and ['+gEndDateStr+']')
    else
      Writeln('Yesterday is outside ['+Today.ToString+'] and ['+gEndDateStr+']');
  {$ENDREGION}

    Writeln('-----------------<< String Tests >>--------------------------------');
  {$REGION '  String Tests .. '}
    if TRange<string>.IsIn('hello', 'alpha', 'zulu') then
      Writeln('"hello" is within the range [alpha, zulu]')
    else
      Writeln('"hello" is outside the range [alpha, zulu]');

    if TRange<string>.IsIn('zulu', 'alpha', 'omega') then
      Writeln('"zulu" is within the range [alpha, omega]')
    else
      Writeln('"zulu" is outside the range [alpha, omega]');
  {$ENDREGION}

    Writeln('-----------------<< TPoint Tests >>-----------------------------');
  {$REGION '  TPoint Tests .. '}
    if TRange<TPoint>.IsIn(gPoint1, gPoint2, gPoint3, PointComparer) then
      Writeln('Point(1, 2) is within the range [Point(0, 0), Point(3, 4)]')
    else
      Writeln('Point(1, 2) is outside the range [Point(0, 0), Point(3, 4)]');

    if TRange<TPoint>.IsIn(Point(5, 5), Point(0, 0), Point(3, 4), PointComparer) then
      Writeln('Point(5, 5) is within the range [Point(0, 0), Point(3, 4)]')
    else
      Writeln('Point(5, 5) is outside the range [Point(0, 0), Point(3, 4)]');
  {$ENDREGION}

    Writeln('-----------------<< TCustomRecord Tests >>-----------------------------');
  {$REGION '  TCustomRecord Tests .. '}
    if TRange<ICustomRecord>.IsIn(gRec2, gRec1, gRec3, gRecordComparer) then
      Writeln('Record is within the range')
    else
      Writeln('Record is outside the range');

    gRec2.New.Edit('Mid', 40);
    if TRange<ICustomRecord>.IsIn(gRec2, gRec1, gRec3, gRecordComparer) then
      Writeln('Record is within the range')
    else
      Writeln('Record is outside the range');
  {$ENDREGION}

    Writeln('-----------------<< TProduct Tests >>-----------------------------');
  {$REGION '  TProduct Tests .. '}
    if TRange<IProduct>.IsIn(gProduct2, gProduct1, gProduct3, gProductComparer) then
      Writeln('Product price is within the range')
    else
      Writeln('Product price is outside the range');

    gProduct2.New.Edit(2, 40);
    if TRange<IProduct>.IsIn(gProduct2, gProduct1, gProduct3, gProductComparer) then
      Writeln('Product price is within the range')
    else
      Writeln('Product price is outside the range');
  {$ENDREGION}

    Writeln('-----------------<< TClient Tests >>-----------------------------');
  {$REGION '  TClient Tests .. '}
    if TRange<IClient>.IsIn(gClient2, gClient1, gClient3, gClientComparer) then
      Writeln('Client age is within the range')
    else
      Writeln('Client age is outside the range');

    gClient2.New.Edit('Bob', 40);
    if TRange<IClient>.IsIn(gClient2, gClient1, gClient3, gClientComparer) then
      Writeln('Client age is within the range')
    else
      Writeln('Client age is outside the range');
  {$ENDREGION}

  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;

  Readln;
end.

Output Result:

 

 

photo_2025-01-11_00-29-27.jpg

 

My Github Link

Edited by bravesofts
  • Like 4

Share this post


Link to post

i update the class 

my github Repo here

unit API.Utils;

interface

uses
  System.SysUtils,           // [Exceptions]
  System.Generics.Defaults;  // [IComparer, TComparer]

type
  TRange<T> = class
  public
    // Check if a value is within the range [aMin, aMax] using a custom comparer
    class function IsIn(const aValue, aMin, aMax: T; const aComparer: IComparer<T>): Boolean; overload; static;

    // Check if a value is within the range [aMin, aMax] using the default comparer
    class function IsIn(const aValue, aMin, aMax: T): Boolean; overload; static;
  end;

implementation

{ TRange<T> }

class function TRange<T>.IsIn(const aValue, aMin, aMax: T; const aComparer: IComparer<T>): Boolean;
begin
  case GetTypeKind(T) of
    tkString, tkClass, tkLString, tkWString, tkInterface, tkDynArray, tkUString:
    begin
      if PPointer(@aValue)^ = nil then
        Exit(False);
    end;
    tkMethod:
    begin
      if (PMethod(@aValue)^.Data = nil) or (PMethod(@aValue)^.Code = nil) then
        Exit(False);
    end;
    tkPointer:
      if PPointer(@aValue)^ = nil then
        Exit(False);
  end;

  if not Assigned(aComparer) then
    raise EArgumentNilException.Create('Comparer is not assigned.');

  Result := (aComparer.Compare(aValue, aMin) >= 0) and (aComparer.Compare(aValue, aMax) <= 0);
end;

class function TRange<T>.IsIn(const aValue, aMin, aMax: T): Boolean;
begin
  Result := IsIn(aValue, aMin, aMax, TComparer<T>.Default);
end;

end.

the call test :

program RangeCheckerPrj;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  DateUtils,
  System.Types,
  System.Generics.Defaults,
  API.Utils in 'API\API.Utils.pas',
  API.Objects.Comparer in 'API\API.Objects.Comparer.pas',
  API.RangeCheckerTest in 'API\API.RangeCheckerTest.pas';

begin

  try
    Writeln('-----------------<< Integer Tests >>--------------------------------');
    Writeln(TRangeTester<Integer>.Test(5, 1, 10)); // "5 is within the range [1, 10]"
    Writeln(TRangeTester<Integer>.Test(15, 1, 10)); // "15 is outside the range [1, 10]"

    Writeln('-----------------<< Int64 Tests >>--------------------------------');
    Writeln(TRangeTester<Int64>.Test(5_000_000_000_000_000_001, 5_000_000_000_000_000_000, 5_000_000_000_000_000_010));
    Writeln(TRangeTester<Int64>.Test(5_000_000_000_000_000_000, 5_000_000_000_000_000_001, 5_000_000_000_000_000_010));


    Writeln('-----------------<< Float Tests >>----------------------------------');
    Writeln(TRangeTester<Double>.Test(7.5, 5.0, 10.0));
    Writeln(TRangeTester<Double>.Test(7.5, 7.6, 10.0));


    Writeln('-----------------<< DateTime Tests >>------------------------------');
    Writeln(TRangeTester<TDateTime>.Test(Today, Today, Today +10));
    Writeln(TRangeTester<TDateTime>.Test(Yesterday, Today, Today +10));

    Writeln('-----------------<< String Tests >>--------------------------------');
    Writeln(TRangeTester<string>.Test('hello', 'alpha', 'zulu'));
    Writeln(TRangeTester<string>.Test('zulu', 'alpha', 'omega'));
    Writeln(TRangeTester<string>.Test('b', 'a', 'c')); // "'b' is within the range ['a', 'c']"
    Writeln(TRangeTester<string>.Test('A', 'b', 'c'));
    Writeln(TRangeTester<string>.Test('B', 'a', 'c'));

    Writeln('-----------------<< TPoint Tests >>-----------------------------');
    Writeln(TRangeTester<TPoint>.Test(gPoint1, gPoint2, gPoint3, PointComparer));
    Writeln(TRangeTester<TPoint>.Test(Point(5, 5), Point(0, 0), Point(3, 4), PointComparer));

    Writeln('-----------------<< TCustomRecord Tests >>-----------------------------');
    Writeln(TRangeTester<ICustomRecord>.Test(gRec1, gRec2, gRec3, gRecordComparer));
      gRec1.New.Edit('Mid', 40);
    Writeln(TRangeTester<ICustomRecord>.Test(gRec1, gRec2, gRec3, gRecordComparer));

    Writeln('-----------------<< TProduct Tests >>-----------------------------');
    Writeln(TRangeTester<IProduct>.Test(gProduct1, gProduct2, gProduct3, gProductComparer));
      gProduct1.New.Edit(1, 40);
    Writeln(TRangeTester<IProduct>.Test(gProduct1, gProduct2, gProduct3, gProductComparer));

    Writeln('-----------------<< TClient Tests >>-----------------------------');
    Writeln(TRangeTester<IClient>.Test(gClient1, gClient2, gClient3, gClientComparer));
      gClient1.New.Edit('Alice', 40);
    Writeln(TRangeTester<IClient>.Test(gClient1, gClient2, nil, gClientComparer));

  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;

  Readln;
end.

the Output:

 

TRangeOutput.jpg

Edited by bravesofts
add output Snapshot
  • Like 1

Share this post


Link to post

That's a lot of text and code on your readme page. I must admit I did not read all of it, especially the code.

 

I still have a some questions:

 

Since Delphi cannot put constraints like "can be ordered" or "has an equality operator" on generics, your function seems to work on all types of T. What is the runtime behaviour? For example, what happens when comparing two arbitrary records?

 

Why did you not use DUnit or DUnitX for unit tests and instead rolled your own console application?

 

PS: You committed some unnecessary files (like .exe, .dcu, ...) to your repository. Only the source is necessary. For creating .gitignore files, there are tools like https://gitignore.io/

  • Like 1

Share this post


Link to post

Thank you for your questions and feedback!

5 hours ago, Der schöne Günther said:

That's a lot of text and code on your readme page. I must admit I did not read all of it, especially the code.

I understand—it’s a lot to go through! That was long for a reason: I know many people don’t have much time to browse a repo, so I wrote everything necessary to make it easier for them, especially on mobile devices. Still, I’ll consider shortening it for better clarity. Thanks for the feedback!

5 hours ago, Der schöne Günther said:

I still have a some questions:

 

Since Delphi cannot put constraints like "can be ordered" or "has an equality operator" on generics, your function seems to work on all types of T. What is the runtime behaviour? For example, what happens when comparing two arbitrary records?

My class specifically handles the IsInRange case. In the demo, I used TPoint (a record) and a custom comparer to determine if a point is within a range of two points. While equality or ordering functions aren’t implemented yet, I believe they would work similarly. Adding these is something I plan to explore in future updates and It would be great if you’d like to contribute by adding this functionality—it’d be much appreciated! 😊

5 hours ago, Der schöne Günther said:

Why did you not use DUnit or DUnitX for unit tests and instead rolled your own console application?

To be honest, I’ve never used this functionality before in my life.

I didn’t publish this class to boast or to tell people, “Look how skilled I am in Delphi.” In reality, I’m a very simple and self-taught Delphi developer, and I’m proud of that, of course.

I’m still in the process of learning and discovering what amazing capabilities and skills can be unlocked in Delphi. I might try DUnit or DUnitX in the future, but for now, I’m focused on exploring and growing my understanding of the language.

5 hours ago, Der schöne Günther said:

PS: You committed some unnecessary files (like .exe, .dcu, ...) to your repository. Only the source is necessary. For creating .gitignore files, there are tools like https://gitignore.io/

Like I mentioned before, I’m not as good a programmer as you might think—I’m just a self-taught Delphi enthusiast. Honestly, I think I just love Delphi and its tools, but when it comes to GitHub or any other tools, they’re not really my thing.

I’ve never worked as a programmer or developer in a professional capacity, and tools like Agile or Git aren’t even on my radar. I guess you could call me a jungle developer! 😊

That said, I appreciate the feedback and will try to improve the repo organization in the future.

Thanks for the feedback!

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×