🎉 Exercism Research is now launched. Help Exercism, help science and have some fun at research.exercism.io 🎉
Avatar of dgeiger

dgeiger's solution

to Robot Name in the Delphi Pascal Track

Published at Sep 02 2020 · 0 comments
Instructions
Test suite
Solution

Manage robot factory settings.

When a robot comes off the factory floor, it has no name.

The first time you turn on a robot, a random name is generated in the format of two uppercase letters followed by three digits, such as RX837 or BC811.

Every once in a while we need to reset a robot to its factory settings, which means that its name gets wiped. The next time you ask, that robot will respond with a new random name.

The names must be random: they should not follow a predictable sequence. Using random names means a risk of collisions. Your solution must ensure that every existing robot has a unique name.

Testing

In order to run the tests for this track, you will need to install DUnitX. Please see the installation instructions for more information.

Loading Exercises into Delphi

If Delphi is properly installed, and *.dpr file types have been associated with Delphi, then double clicking the supplied *.dpr file will start Delphi and load the exercise/project. control + F9 is the keyboard shortcut to compile the project or pressing F9 will compile and run the project.

Alternatively you may opt to start Delphi and load your project via. the File drop down menu.

When Questions Come Up

We monitor the Pascal-Delphi support room on gitter.im to help you with any questions that might arise.

Submitting Exercises

Note that, when trying to submit an exercise, make sure the exercise file you're submitting is in the exercism/delphi/<exerciseName> directory.

For example, if you're submitting ubob.pas for the Bob exercise, the submit command would be something like exercism submit <path_to_exercism_dir>/delphi/bob/ubob.pas.

Source

A debugging session with Paul Blackwell at gSchool. http://gschool.it

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you may request help from a mentor.

uRobotNameTests.pas

unit uRobotNameTests;

interface
uses
  DUnitX.TestFramework, uRobotName, System.Generics.Collections;

const
  CanonicalVersion = '0.0.0.1';

type
  [TestFixture]
  TRobotNameTest = class(TObject)
  private
    FRobot : TRobot;
    Names : TList<string>;
    Robots : TObjectList<TRobot>;
  public
    [Setup]
    procedure Setup;
    [TearDown]
    procedure TearDown;

    [Test]
//    [Ignore('Comment the "[Ignore]" statement to run the test')]
    procedure name_properly_formated;

    [Test]
    [Ignore]
    procedure is_name_persistent;

    [Test]
    [Ignore]
    procedure is_able_to_reset_name;

    [Test]
    [Ignore]
    procedure each_robot_have_unique_name;
  end;

implementation

uses
  System.RegularExpressions, System.Classes, SysUtils, Windows;

procedure TRobotNameTest.is_name_persistent;
begin
  Assert.AreEqual(FRobot.Name, FRobot.Name);
end;

procedure TRobotNameTest.name_properly_formated;
begin
  Assert.IsTrue(TRegEx.IsMatch(FRobot.Name, '^[A-Z]{2}\d{3}$'), 'expected format''ssddd'', found ' + FRobot.Name);
end;

procedure TRobotNameTest.Setup;
var
  i, j : char;
  k : integer;
begin
  Names := TList<string>.create;
  for i := 'A' to 'Z' do
    for j := 'A' to 'Z' do
      for k := 0 to 999 do
        Names.Add(i + j + format('%.*d', [3, k]));
  Robots := TObjectList<TRobot>.Create;
  FRobot := TRobot.Create;
end;

procedure TRobotNameTest.TearDown;
begin
  Names.DisposeOf;
  Robots.DisposeOf;
  FRobot.DisposeOf;
end;

procedure TRobotNameTest.each_robot_have_unique_name;
var
  i, ind : integer;
  Rob: TRobot;

  Procedure WriteXY( x , y : Integer; s : string);
  var
   LCoord: TCoord;
   begin
     LCoord.X := x;
     LCoord.Y := y;
     SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), LCoord);
     Write(s);
   end;

begin
  for i := 0 to 20000 do
  begin
    Rob := TRobot.Create;
    Assert.IsTrue(Names.BinarySearch(Rob.Name, ind), format('Robot #: %*.d, named %s is not unique',[5, ind, Rob.Name]));
    Names.Delete(ind);
    Robots.BinarySearch(Rob, ind);
    Robots.Insert(ind, Rob);
    WriteXY(2, 9, format('Robot #: %*.d, named %s, is unique',[5, i, Rob.Name]));
  end;
end;

procedure TRobotNameTest.is_able_to_reset_name;
var
  old : string;
  i : Integer;

begin
  Robots.Add(TRobot.Create);
  old := Robots.Last.Name;
  Names.Remove(old);
  for i := 0 to 10 do
  begin
    Names.Add(old);
    old := Robots.Last.Name;
    Robots.Last.Reset;
    Assert.IsTrue(Names.Contains(Robots.Last.Name), 'Robot name after reset is not unique');
    Names.Remove(Robots.Last.Name);
    Assert.AreNotEqual(old, Robots.Last.Name, 'Robot name after reset is not changed');
  end;
end;

initialization
  TDUnitX.RegisterTestFixture(TRobotNameTest);
end.
unit uRobotName;

interface

{$DEFINE UseAvailableNames}

uses
  System.Generics.Collections, System.Math, System.SysUtils;

type

  TRobot = class
    private
{$IFNDEF UseAvailableNames}
      class var UsedNames : TList<string>;
{$ENDIF}

      procedure CreateName;
      function IsNameUnique(TestName: String): Boolean;

    public
      Name: String;

      procedure Reset;
      constructor Create;

    end;

var
  AvailableNames : TList<string>;

implementation

constructor TRobot.Create;
begin
{$IFNDEF UseAvailableNames}
  // Create the list of used names
  UsedNames := TList<string>.create;
{$ENDIF}

  CreateName;
end;

procedure TRobot.CreateName;
var
  Char1, Char2: Char;
  Num: Integer;
  TestName: String;
begin
  repeat
    // Try to get (pseudo) random as possible
    Randomize;

    // Get the random first and seond letters for the name
    Char1 := Chr(RandomRange(Ord('A'), Ord('Z')));
    Char2 := Chr(RandomRange(Ord('A'), Ord('Z')));

    // Now get the integer portion of the name
    Num   := Random(1000);

    // Format the two characters and the integer into a robot name
    TestName := Format('%s%s%3.3d', [Char1, Char2, Num]);

    // and keep doing this until we get a unique robot name
  until IsNameUnique(TestName);

{$IFDEF UseAvailableNames}
  // Remove the robot's name from the available names
  AvailableNames.Remove(TestName);
{$ELSE}
  // Add the robot's name to the used names and make sure the list is sorted
  UsedNames.Add(TestName);
  UsedNames.TrimExcess;
  UsedNames.Sort;
{$ENDIF}

  // Set the robot's name to the unique name generated
  Name := TestName;
end;

function TRobot.IsNameUnique(TestName: String): Boolean;
{$IFNDEF UseAvailableNames}
var
  Index: Integer;
{$ENDIF}
begin
{$IFDEF UseAvailableNames}
  // See if the name is available
  Result := AvailableNames.IndexOf(TestName) <> -1;
{$ELSE}
  // See if the name has been used yet
  // Result := UsedNames.IndexOf(TestName) = -1;
  Result := not UsedNames.BinarySearch(TestName, Index);
{$ENDIF}
end;

procedure TRobot.Reset;
var
  OldName: String;
begin
  // Save the old name so we can remove it after we're done renaming the robot.
  // We'll remove it later to make sure that it is impossible to come up the
  // same name again/
  OldName := Name;

  // Create the robot's new name
  CreateName;

{$IFDEF UseAvailableNames}
  // Add the old name to the available names
  AvailableNames.Add(OldName);
{$ELSE}
  // Remove the old name from the used names
  UsedNames.Remove(OldName);
  UsedNames.TrimExcess;
  UsedNames.Sort;
{$ENDIF}
end;

{$IFDEF UseAvailableNames}
procedure CreateAllNames;
var
  Char1, Char2: Char;
  Num: Integer;
begin
  // I've been having a problem when using a list of used names. There is
  // always a collision, even though the logic seems right to me.
  //
  // If I instead use a list of available names, and reverse adding and
  // removing from the list, it works fine.  I've tried IndexOf() and after
  // wasting a lot of time on trying to figure out why the process was failing,
  // I sorted the list and tried BinarySearch(), with the same (bad) result.

  // Creae the list of available names
  AvailableNames := TList<string>.Create;

  // Use all the letters for the first character
  for Char1 := 'A' to 'Z' do
    // and use them again for the second character.
    for Char2 := 'A' to 'Z' do
      // Now, append the numbers 0 to 999 to create all possible names,
      for Num := 0 to 999 do
        // and add the name to the list of available names.
        //AvailableNames.Add(Char1 + Char2 + format('%.3d', [Num]));
        AvailableNames.Add(format('%s%s%.3d', [Char1, Char2, Num]));
end;
{$ENDIF}

initialization

{$IFDEF UseAvailableNames}
// Initialize the list of all possible robot names
CreateAllNames;
{$ENDIF}

finalization

end.

Community comments

Find this solution interesting? Ask the author a question to learn more.

dgeiger's Reflection

For some reason, I wasn't able to get unique names when using a list of used names. When I instead used a list of available names pre-populated with all possible names and inverted the process of adding/removing a name, it works fine.

If I get time later, I'm going to try to figure out the problem.