Homework 2 Solution

The biggest design decision that had to be made for this homework was which type of string you would use to represent your numbers. I chose to use Ada.String.Unbounded.Unbounded_String, which I imported into the main namespace so I wouldn't have to type that repeatedly. A good alternative is to use bounded strings whose lengths are dynamically computed at the top of each function. The least desirable solution is to hardcode a single maximum length for all strings. What happens if a user supplies you an input longer than your string type? Any code which used fixed-length strings should at least check for this error condition.

-- use unbounded strings as the representation of arbitrary length/base numbers  
with Ada.Strings.Unbounded;
use Ada.Strings.Unbounded;
with Ada.Strings.Unbounded.Text_IO;
with Ada.Text_IO; 
with Ada.Command_line; 
with Ada.Assertions; 
procedure hw2 is
  -- need this package to read unbounded strings from a file 
  package UIO renames Ada.Strings.Unbounded.Text_IO; 
  -- use Text_IO both for file reading and printing regular strings 
  package IO renames Ada.Text_IO; 
  -- need command_line to get filename 
  package CL renames Ada.Command_line; 

Some of the submissions had very long case statements mapping to/from every character and its numerical value. It's way tidier to use Character'Pos and Character'Val to convert between characters and their ASCII values.

  -- takes a number, returns an equivalent character
  -- Character'Val maps ascii codes to character
  -- Character'Pos maps characters to their ascii codes
  function Nat_to_Char(N : Natural) return Character is 
  begin 
    if N <= 9 then return Character'Val(N + Character'Pos('0'));
    else return Character'Val(N - 10 + Character'Pos('A')); 
    end if; 
  end Nat_to_Char; 

I created an exception 'Invalid_Character' which gets raised whenever a character of an input string is something other than '0'-'9' | 'a'-'f' | 'A' - 'F'. However you handle this sort of error, somewhere in your code you should be validating that all characters fall within the valid range. Further down there is additional verification that the value of each character doesn't exceed the base.

  -- exception that gets raised on characters outside the '0'.. 'F' range
  Invalid_Character : exception;
  -- takes a character representing a number in base 16, returns its value
  function Char_to_Nat(C : Character) return Natural is 
    Ascii_Code : Natural := Character'Pos(C);
  begin 
    case C is
      when 'A' .. 'F' => return 10 + Ascii_Code - Character'Pos('A');
      when 'a' .. 'f' => return 10 + Ascii_Code - Character'Pos('a');
      when '0' .. '9' => return Ascii_Code - Character'Pos('0');
      when others => raise Invalid_Character; 
    end case; 
  end Char_to_Nat;

My implementation of addition might differ slightly from your but the underlying logic should be similar. Traverse the two number strings from least to most significant digits, adding each digit pair and carrying any value greater than the base to the next position.

  
  -- indexes into strings from right to left
  -- returning 0 if you go past the leftmost boundary 
  function Elem_to_Nat(S : Unbounded_String; Index : Positive) return Natural is 
    RevIndex : Integer := Length(S) - Index + 1; 
  begin 
    if RevIndex > 0  then return Char_to_Nat(Element(S, RevIndex));
    else return 0; 
    end if;
  end Elem_to_Nat;
      
  function Reverse_String(S : Unbounded_String) return Unbounded_String is
    N : Natural := Length(S); 
    Rev : Unbounded_String := S; 
  begin 
    for I in Positive Range 1 .. N loop 
      Replace_Element(Rev, I, Element(S, N - I + 1)); 
    end loop; 
    return Rev; 
  end Reverse_String; 
  
  -- to add two arbitrary length integers, loop through the elements of
  -- of strings, adding each pair of elements and carrying result/Base
  -- to the next position
  function Add(X, Y : Unbounded_String; Base : Positive) return Unbounded_String is 
    -- Elem_Sum is where we stick the result of adding two single-digit numbers
    -- may exceed the base so it will get split into one digit of the result and a carry
    Elem_Sum : Natural := 0;
    Carry : Natural := 0;
    Sum : Unbounded_String := Null_Unbounded_String;
  begin 
    -- adds from least to most significant positions, 
    -- appending to a result string in reverse
    for I in Positive range 1 .. Natural'Max(Length(X), Length(Y)) loop
      Elem_Sum := Elem_To_Nat(X, I) + Elem_To_Nat(Y, I) + Carry;
      Append(Sum, Nat_To_Char(Elem_Sum mod Base)); 
      Carry := Elem_Sum / Base; 
    end loop; 
    -- don't forget the last carry
    if Carry > 0 then Append(Sum, Nat_to_Char(Carry)); end if; 
    -- since we've been accumulating from right to left, 
    -- now we reverse the Sum string to get our result 
    return Reverse_String(Sum); 
  end Add; 

To multiply I add X to itself as many times as the value of Y. I implement this as recursion where each subsequent recursive call gets a Y string 1 less in value. I'm not totally happy with either this code or algorithm, email me if yours came out cleaner.

  Zero : Unbounded_String := To_Unbounded_String("0"); 
  One : Unbounded_String := To_Unbounded_String("1"); 

  -- subtract 1 from the arbitrary length natural encoded as a string 
  function Sub1(X : Unbounded_String; Base : Natural) return Unbounded_String is
    N : Natural := Length(X);
    Result : Unbounded_String := X; 
    CurrDigit : Natural; 
  begin
    if X = One then return Zero; end if; 
    -- scan from right to left until you find a non-zero position, 
    -- then decrement it 
    for I in reverse 1 .. N loop
       CurrDigit := Char_to_Nat(Element(X, I)); 
       -- if a digit is 0, we're going to borrow from further right 
       -- and replace it with last single digit number 
       if CurrDigit = 0 then Replace_Element(Result, I, Nat_to_Char(Base-1));
       elsif CurrDigit = 1 and I = 1 then Delete(Result, 1, 1); exit; 
       else
         -- replace current character with char representation 
         -- of the digit - 1
         Replace_Element(Result, I, Nat_to_Char(CurrDigit - 1)); 
         exit; 
       end if;
    end loop; 
    return result; 
  end Sub1; 

  -- multiply by recursively adding X 
  -- every recursive call takes a value of Y which is one smaller 
  function Mult(X, Y: Unbounded_String; Base : Positive) return Unbounded_String is
  begin 
    if Y = Zero or X = Zero then return Zero; 
    elsif Y = One then return X;
    else return Add(X, Mult(X, Sub1(Y, Base), Base), Base); 
    end if; 
  end Mult; 

No character's value in a string should exceed the base (for example, "023" is not a binary string). Something like this simple loop should happen after each string gets loaded.

  -- check every character of a string to ensure that it is within the appropriate range 
  procedure Validate_Base(X : Unbounded_String; Base : Positive) is
  begin 
    for I in 1 .. Length(X) loop 
      Ada.Assertions.Assert(Char_to_Nat(Element(X, I)) < Base, "character greater than base");       
    end loop; 
   end Validate_Base; 
  

The body of the main procedure should be easy to understand. The only exotic thing going is that I use the Command_line package to read in the input filename. Many of you hardcoded your the filename as something like "input.txt", which is fine.


  File : IO.File_Type;
  BaseStr : Unbounded_String := Null_Unbounded_String;
  Base : Natural; 
  X : Unbounded_String := Null_Unbounded_String; 
  Y : Unbounded_String := Null_Unbounded_String; 

begin
   -- make sure the filename has been given as a commandline arg
  Ada.Assertions.Assert(CL.Argument_Count /= 0, "file name not given"); 
   -- also assert that no additional args were given
  Ada.Assertions.Assert(CL.Argument_Count < 2, "too many arguments"); 
  IO.Put("Reading triplets from file: ");
  IO.Put_Line(CL.Argument(1));
  IO.Open (File => File, Mode => IO.In_File, Name => CL.Argument(1));
                     
   -- loop through all triplets in the file   
  loop 
    -- read base 
    BaseStr := UIO.Get_Line(File);  
    Base := Natural'Value(To_String(BaseStr));
    Ada.Assertions.Assert(Base <= 16, "invalid base"); 
    exit when Base = 0; 
     
    -- print "Base b"
    IO.Put("Base "); 
    UIO.Put_Line(BaseStr); 
	 
    -- read two numbers 
    X := UIO.Get_Line(File);
    Y := UIO.Get_Line(File);

     -- ensure inputs are well formed 
    Validate_Base(X, Base); 
    Validate_Base(Y, Base); 

    -- print "x + y = sum" 
    UIO.Put(X);
    IO.Put(" + "); 
    UIO.Put(Y); 
    IO.Put(" = "); 	 
    UIO.Put_Line(Add(X, Y, Base)); 

    -- print "x * y = product"
    UIO.Put(X);
    IO.Put(" * "); 
    UIO.Put(Y); 
    IO.Put(" = "); 	 
    UIO.Put_Line(Mult(X, Y, Base)); 

    exit when IO.End_Of_File(File); 
  end loop; 
  IO.Close(File => File);
end hw2;