Bounded strings using ‘advanced’ records

There was an interesting question on StackOverflow recently, asking whether it is possible to define a string type whose instances have a minimum and maximum length. More exactly, there was an interesting answer to that question, Andreas Rejbrand chipping in with a record-based solution that leverages operator overloading.

While very stimulating, on reflection, I think there is the odd issue with the solution suggested. In particular, instances of the type should be explicitly created, but there’s nothing enforcing that, and the behaviour of the + operator defined is perhaps not very intuitive – if you look back at the original poster’s subsequent edit to his own question, you’ll see him complaining about exceptions that are caused by his assumption that the result of the + operator will take on the bounds of what it is assigned to, rather than of either of the operands (in Andreas’ implementation, the result takes on the bounds of the left hand operand).

These points in mind, here’s my quick attempt at a slightly refined variant – plainly, it’s still indebted to its inspiration though:

unit BoundedString;

interface

uses
  System.SysUtils;
  
{$IFDEF DEBUG}
  {$RANGECHECKS ON}
{$ENDIF}
  
type
  TBoundedString = record
  public type
    TLength = 1..High(Integer);
  strict private
    FMinLength, FMaxLength: TLength;
    FValue: string;
    procedure CheckInitialized;
    function GetMinLength: TLength;
    function GetMaxLength: TLength;
    function GetValue: string;
    procedure SetValue(const S: string);
  public
    constructor Create(AMinLength, AMaxLength: TLength); overload;
    constructor Create(AMinLength, AMaxLength: TLength; const AValue: string); overload;
    class operator Add(const A, B: TBoundedString): string; overload;
    class operator Add(const A: TBoundedString; const B: string): string; overload;
    class operator Add(const A: string; const B: TBoundedString): string; overload;
    class operator Implicit(const S: TBoundedString): string;
    class operator Equal(const A, B: TBoundedString): Boolean;
    class operator NotEqual(const A, B: TBoundedString): Boolean;
    property MinLength: TLength read GetMinLength;
    property MaxLength: TLength read GetMaxLength;
    property Value: string read GetValue write SetValue;
  end;
  
implementation
  
resourcestring
  SNotInitialized = 'A TBoundedString instance requiries explicit creation before use';
  SStringTooSmall = 'String too small for the TBoundedString instance';
  SStringTooBig = 'String too big for the TBoundedString instance';

constructor TBoundedString.Create(AMinLength, AMaxLength: TLength);
begin
  FMinLength := AMinLength;
  FMaxLength := AMaxLength;
  FValue := StringOfChar(' ', AMinLength);
end;

constructor TBoundedString.Create(AMinLength, AMaxLength: TLength;
  const AValue: string);
begin
  Create(AMinLength, AMaxLength);
  SetValue(AValue);
end;

procedure TBoundedString.CheckInitialized;
begin
  if FValue = '' then 
    raise EInvalidOpException.CreateRes(@SNotInitialized);
end;

function TBoundedString.GetMinLength: TLength;
begin
  CheckInitialized;
  Result := FMinLength;
end;

function TBoundedString.GetMaxLength: TLength;
begin
  CheckInitialized;
  Result := FMaxLength;
end;

function TBoundedString.GetValue: string;
begin
  CheckInitialized;
  Result := FValue;
end;

procedure TBoundedString.SetValue(const S: string);
begin
  CheckInitialized;
  if Length(S) < FMinLength then 
    raise ERangeError.CreateRes(@SStringTooSmall);
  if Length(S) > FMaxLength then
    raise ERangeError.CreateRes(@SStringTooSmall);
  FValue := S;
end;

class operator TBoundedString.Add(const A, B: TBoundedString): string;
begin
  Result := A.Value + B.Value;
end;

class operator TBoundedString.Add(const A: TBoundedString; const B: string): string;
begin
  Result := A.Value + B;
end;

class operator TBoundedString.Add(const A: string; const B: TBoundedString): string;
begin
  Result := A + B.Value;
end;

class operator TBoundedString.Equal(const A, B: TBoundedString): Boolean;
begin
  Result := A.Value = B.Value;
end;

class operator TBoundedString.NotEqual(const A, B: TBoundedString): Boolean;
begin
  Result := A.Value <> B.Value;
end;

class operator TBoundedString.Implicit(const S: TBoundedString): string;
begin
  Result := S.Value;
end;

end.

Aside from a bit of renaming, I’ve (a) deliberately left out the ability to assign a string directly to a TBoundedString (instead, the Value property must be set or the constructor [re-]called), (b) enforced calling Create before use, and (c) changed the result type of the + operator overload to string rather than TBoundedString. The last amendment allows the code desired by the StackOverflow questioner to now work:

var
  Str1, Str2, Str3: TBoundedString;
begin
  Str1 := TBoundedString.Create(2, 5, 'pp');
  Str2 := TBoundedString.Create(2, 5, 'aaaa');
  Str3 := TBoundedString.Create(2, 10, Str1 + Str2);
  WriteLn(Str3.Value)
end.

It works pretty nicely I think, and yet… I can’t help thinking that the ‘advanced’ record implementation introduced back in D2006 is only half-finished. While you can create useful things with it, it frequently can’t ‘stand alone’ and requires assitance from a managed type of some sort – in the present example’s case, I rely on the fact a string is initialised to nil (”), which allows me to overload the FValue field’s purpose to be both an ‘initialised or not initialised’ flag and contain the actual data once initialised.

On Delphi 2009’s release back in 2008, Allen Bauer wrote

During the development of Delphi 2009, there were several language features that were dropped in favor of generics and anonymous methods. One such feature was what we called “managed records.” This allowed for some very interesting things where you could now implement a constructor, destructor and several assignment class operators on a record that would be automatically called when the record came into scope, went out of scope, and was assigned to something or something was assigned to it (like another instance of itself).

‘Managed’ records – sounds good! Of course, I’d like Delphi generics taken to another level too, and a few steps developing anonymous methods in the direction of C#-style lambdas would be great as well…

Advertisements

2 thoughts on “Bounded strings using ‘advanced’ records

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s