Constructing and evaluating numerals

Numbers and numerals

In a way, working in a programming language makes it easy to distinguish between a number (a value of a type like integer or real in Pascal) and a numeral expressing that number (which would be a character string in Pascal). If you keep this distinction in mind, you won't find it surprising that there can be many different numerals for the same number (for instance, `35.2', `+0035.200', and `3.52 x 10^1' are all numerals for the number 35.2), and that the same numeral can stand for different numbers in different systems of numeration (for instance, `11100' stands for the number eleven thousand one hundred in decimal numeration, but for the number twenty-eight in binary numeration).

What tends to confuse people is that Pascal itself requires the programmer to use decimal numerals when referring to numbers inside programs. Moreover, the predefined input procedures expect decimal numeration to be used at the keyboard and in any text files from which numbers are to be read in, during the execution of the program; similarly, whenever numbers are written out to the monitor or to a text file, they are expressed in decimal numeration. However, these features of Pascal are completely conventional and are simply designed to cater to the arbitrary preferences of the many programmers and users who happen to have ten fingers. Computers, which have no fingers, use a completely different method of representing numbers; Pascal conceals this from programmers by giving no indication of how complicated the input and output procedures for integers and reals really are.

Evaluating a numeral

To get some idea of what is involved, let's consider first a procedure that takes a string of digit characters and determines what number it expresses in decimal numeration:

procedure evaluate (var numeral: string; var value: integer);
var
  index: integer;
begin
  value := 0;
  for index := 1 to strlen (numeral) do
    value := value * 10 + ord (numeral[index]) - ord ('0')
end;
Here I've used the non-standard HP Pascal string type for the numeral, not specifying the number of characters in the string, so as to be able to use the same procedure for numerals of any number of digits. HP Pascal requires that string parameters of unspecified length be passed by reference rather than by value. Inside the procedure, I've used the non-standard strlen procedure to determine the number of characters in the string.

The parameter value constantly contains the numeric value of the part of the numeral that the procedure has so far inspected. It is initialized to 0, and then the procedure traverses the numeral from left to right. As it encounters each new digit, it multiplies the value of the previous part of the numeral by ten (since each digit in that previous part of the numeral is now one position further to the left and consequently has a positional value ten times as great) and adds the value of the new digit, which can be determined by measuring its distance from the digit 0 in the character set.

The appearance of the numeric literal 10 in this procedure suggests that it could be usefully generalized by parameterizing it for different bases. The same method can be used to evaluate binary and octal numerals, for instance. HP Pascal provides a mechanism, the default_parms option, for making this additional parameter optional. The idea is that the procedure could be invoked either with three parameters (in which case the third parameter should be the base -- 2 for a binary numeral, 8 for an octal one, and so on) or with two (in which case the default base of 10 would be used. Here's how it looks in practice:

procedure evaluate (var numeral: string; var value: integer;
                    base: integer)
option default_parms (base := 10);
var
  index: integer;
begin
  value := 0;
  for index := 1 to strlen (numeral) do
    value := value * base + ord (numeral[index]) - ord ('0')
end;
Of course, if you use this mechanism, your program will not be portable; the default_parms option is extremely non-standard Pascal.

Although the procedure shown above demonstrates the basic logic of evaluation, it is not yet suitable for general use, because it works only under certain conditions that it does not enforce. It presupposes that the base is between 2 and 10; that the numeral that it is given is a non-empty string containing only digits that are valid for that base; and that the value of the numeral can be represented in HP Pascal's integer type. The base_conversion module at the end of this handout includes a more elaborate version of evaluate that tests to make sure that these preconditions are met. It also extends the evaluation mechanism to bases in the range from 11 to 36, treating the letters of the alphabet as ``digits'' in these higher bases. The ``digit'' A has the value ten when used in a numeral of one of these higher bases, B has the value eleven, and so on.

Pascal's read and readln procedures perform essentially the same algorithm when reading in the value of an integer variable, because they too have to recover that value from a sequence of characters that is either typed at the keyboard or recovered from a text file.

Expressing a natural number

When it is necessary to express the value of an integer variable in human-readable form, as a sequence of digits, the opposite conversion takes place. It's a little trickier, since the easiest way of recovering the digits (by successive divisions by 10) produces them from right to left, whereas we want to put them into the string from left to right -- if we try to place the rightmost digit first, we won't know what position in the string it should be placed at. The solution is to use a recursive procedure that counts the digits on the way in and places them appropriately on the way out:

procedure express (value: integer; var numeral: string);
var
  length: integer;

  procedure helper (value: integer; digits_so_far: integer;
                    var position: integer);
  begin
    if value < 10 then begin

      position := 1;
      setstrlen (numeral, digits_so_far + 1);
      numeral[position] := chr (ord ('0') + value)
    end
    else begin
      helper (value div base, digits_so_far + 1, position);
      position := position + 1;
      numeral[position] := chr (ord ('0') + value mod 10)
    end
  end;

begin { procedure express }
  helper (value, 0, length)
end;
On each successive call to helper, the first parameter (value) will be ten times smaller, and digits_so_far will be one larger. Eventually value must be reduced to an integer less than ten, and at that point position will be initialized to 1 and the first digit will be placed at the left end of numeral. When a recursive call is finished and control is returned to the next lower level, position is incremented and the next digit of the numeral is appropriately placed.

The call to the HP Pascal procedure setstrlen ensures that all the positions into which digits must be written will be accessible.

Again, this procedure should be reworked to include precondition tests (value should be non-negative, and there should be enough character positions in numeral to hold all the digits of the numeral) and generalized to allow any base from 2 to 36. The base_conversion module shows how this is done.

Converting from one base of numeration to another

Once we have both evaluate and express in generalized form, it becomes trivial to convert any non-negative integer (less than or equal to MAXINT) from one base of numeration to another:

procedure base_convert (var numeral_in: string; input_base: integer;
                        output_base: integer; var numeral_out: string);
var
  value: integer;
begin
  evaluate (numeral_in, value, input_base);
  express (value, numeral_out, output_base)
end;
In other words, see what integer is expressed by the numeral you start with in the original system of numeration, then express that numeral in the desired system of numeration.

A base-conversion module

Here's the promised module that exports the fully developed evaluate, express, and base_convert procedures.

{ This module exports procedures for evaluating a numeral in any base from
  2 to 36, for expressing a non-negative integer as a numeral in any such
  base, and for converting a numeral in one base into an equivalent numeral
  in another base.

  Programmer: John Stone, Grinnell College.
  Date of this version: January 9, 1996.
}

{ The evaluate and express procedures defined below can be invoked with
  either two arguments or three arguments.  The third argument, if present,
  specifies the base of numeration.  If it is absent, decimal numeration is
  assumed.  The following compiler directive makes it possible to define
  procedures with optional parameters. }

$standard_level 'ext_modcal'$

module base_conversion;

export

  const
    MINIMUM_BASE = 2;
    MAXIMUM_BASE = 36;

  { The evaluate procedure determines the value of a given numeral.  The
    numeral must contain no characters other than digits which are valid in
    the specified base, and the value of the numeral should not exceed
    MAXINT.  The base must be in the range from 2 to 36; if it is not
    supplied, a default value of 10 is assumed. } 

  procedure evaluate (var numeral: string; var value: integer;
                      base: integer) 
  option default_parms (base := 10);

  { The express procedure constructs a numeral, in a specified base of
    numeration, that denotes a given integer value.  The value must be
    non-negative, and the caller must supply a string of sufficient
    maximum length to hold all of the digits of the numeral constructed.
    The base must be in the range from 2 to 36; if it is not supplied, a
    default value of 10 is assumed. }

  procedure express (value: integer; var numeral: string; base: integer)
  option default_parms (base := 10);

  { Given a numeral (numeral_in) that expresses a value in one specified
    base of numeration (input_base), the base_convert procedure constructs
    a numeral (numeral_out) that expresses the same value in another
    specified base (output_base).  The input numeral must contain no
    characters other than digits which are valid in the specified base, and
    the value it denotes must not exceed MAXINT.  Both bases must be in the
    range from 2 to 36. }

  procedure base_convert (var numeral_in: string; input_base: integer;
                          output_base: integer; var numeral_out: string);

implement

  import
    stderr;

  const

    { frequently used ASCII codes }

    ORD_ZERO = 48 { = ord ('0') in ASCII };
    ORD_CAPITAL_A = 65 { = ord ('A') in ASCII };
    ORD_LOWER_CASE_A = 97 { = ord ('a') in ASCII };

    { The following constants are more or less arbitrary integers
      signifying various kinds of exceptions that can occur within this
      module. }

    FIRST_EXCEPTION_CODE = 1;
    NULL_NUMERAL_EXCEPTION = 1;
    BAD_DIGIT_EXCEPTION = 2;
    INTEGER_OVERFLOW_EXCEPTION = 3;
    STRING_OVERFLOW_EXCEPTION = 4;
    NEGATIVE_EXCEPTION = 5;
    BASE_RANGE_EXCEPTION = 6;
    EXCEPTION_EXCEPTION = 7;
    LAST_EXCEPTION_CODE = EXCEPTION_EXCEPTION;

  { The numeral_exception procedure, which is not exported, is invoked
    whenever one of the preconditions for the successful execution of a
    procedure is found to be false.  It prints out an appropriate
    explanation of the exception just before the program is halted. }

  procedure numeral_exception (exception_code: integer);
  begin
    if (exception_code < FIRST_EXCEPTION_CODE) or
           (LAST_EXCEPTION_CODE < exception_code) then
      exception_code := EXCEPTION_EXCEPTION;
    write (stderr, 'Exception #', exception_code : 1,
           ' in module NUMERALS: ');
    case exception_code of
      NULL_NUMERAL_EXCEPTION:
        writeln (stderr, 'null string presented as numeral in procedure ',
                 'EVALUATE');
      BAD_DIGIT_EXCEPTION:
        writeln (stderr, 'non-digit occurring in numeral in procedure ',
                 'EVALUATE');
      INTEGER_OVERFLOW_EXCEPTION:
        writeln (stderr, 'value of numeral exceeds MAXINT in procedure ',
                 'EVALUATE');
      STRING_OVERFLOW_EXCEPTION:
        writeln (stderr, 'length of numeral exceeds maximum length of ',
                 'string in procedure EXPRESS');
      NEGATIVE_EXCEPTION:
        writeln (stderr, 'negative number as argument to procedure ',
                 'EXPRESS');
      BASE_RANGE_EXCEPTION:
        writeln (stderr, 'base out of range (', MINIMUM_BASE : 1, ' .. ',
                 MAXIMUM_BASE : 1, ') in procedure EVALUATE, procedure ',
                 'EXPRESS, or procedure BASE_CONVERT');
      EXCEPTION_EXCEPTION:
        writeln (stderr, 'argument out of range in procedure ',
                 'NUMERAL_EXCEPTION.');
    end
  end;

  procedure evaluate (var numeral: string; var value: integer;
                      base: integer) 
  option default_parms (base := 10);

  var
    index: integer;
      { counts off the digits of the numeral, from left to right }
    new_digit_value: integer;
      { the value of the digit currently being inspected }

    { The valid_digit function determines whether a given character is a
      valid digit in a given base of numeration.  For bases larger than
      ten, letters of the alphabet are used, and they may be either capital
      or lower-case letters, with the same values (A = a = ten, B = b =
      eleven, and so on) in either case. }

    function valid_digit (ch: char; base: integer): Boolean;
    begin
      if base <= 10 then
        valid_digit := ('0' <= ch) and (ch < chr (ORD_ZERO + base))
      else
        valid_digit := (('0' <= ch) and (ch <= '9')) or
             (('A' <= ch) and (ch < chr (ORD_CAPITAL_A + base - 10))) or
             (('a' <= ch) and (ch < chr (ORD_LOWER_CASE_A + base - 10)))
    end;

    { The digit_value function recovers the numerical value of a given
      digit (or of a letter being used as a digit). }
   
    function digit_value (ch: char): integer;
    begin
      if ('0' <= ch) and (ch <= '9') then
        digit_value := ord (ch) - ORD_ZERO
      else if ('A' <= ch) and (ch <= 'Z') then
        digit_value := ord (ch) - ORD_CAPITAL_A + 10
      else if ('a' <= ch) and (ch <= 'z') then
        digit_value := ord (ch) - ORD_LOWER_CASE_A + 10
    end;

    { The in_bounds function determines, cautiously, whether the integer
      that would be obtained by multiplying value and base and adding
      new_digit_value the result is within HP Pascal's integer data type
      -- that is, whether it would be less than or equal to MAXINT --
      returning TRUE if the proposed operation is safe and FALSE if it
      would result in an integer overflow. }

    function in_bounds (value: integer; new_digit_value: integer;
                        base: integer): Boolean;
    var
      all_but_last: integer;
    begin
      all_but_last := MAXINT div base;
      if value < all_but_last then
        in_bounds := TRUE
      else if value = all_but_last then
        in_bounds := (new_digit_value <= MAXINT mod base)
      else
        in_bounds := FALSE
    end;

  begin { procedure evaluate }
    assert ((MINIMUM_BASE <= base) and (base <= MAXIMUM_BASE),
            BASE_RANGE_EXCEPTION, numeral_exception);
    assert (0 < strlen (numeral), NULL_NUMERAL_EXCEPTION,
            numeral_exception);
    value := 0;
    for index := 1 to strlen (numeral) do begin
      assert (valid_digit (numeral[index], base), BAD_DIGIT_EXCEPTION,
              numeral_exception);
      new_digit_value := digit_value (numeral[index]);
      assert (in_bounds (value, new_digit_value, base),
              INTEGER_OVERFLOW_EXCEPTION, numeral_exception);
      value := value * base + new_digit_value
    end
  end;

  procedure express (value: integer; var numeral: string; base: integer)
  option default_parms (base := 10);

  var
    length: integer;
      { the number of characters in the completed numeral }
    max_length: integer;
      { the number of character positions available to hold the completed
        numeral }

    { The digit_for function finds and returns a character that can be used
      as a digit denoting a given integer.  It presupposes that the given
      integer is non-negative and less than the current base of
      numeration. } 

    function digit_for (n: integer): char;
    begin
      if n < 10 then
        digit_for := chr (ORD_ZERO + n)
      else
        digit_for := chr (ORD_CAPITAL_A + n - 10)
    end;

    { Given a one-digit number, the helper procedure will first ensure that
      there is enough space in the numeral string to write all of the
      digits of the entire value to be expressed, and then place the digit
      that denotes the one-digit number in the first position of that
      string.  Given a number of more than one digit, the helper procedure
      will call itself recursively to place all but the last digit of that
      number in the numeral string, then add the last digit in its correct
      position. }

    procedure helper (value: integer; digits_so_far: integer;
                      var position: integer);
    begin
      if value < base then begin
        position := 1;
        assert (position <= max_length, STRING_OVERFLOW_EXCEPTION,
                numeral_exception);
        setstrlen (numeral, digits_so_far + 1);
        numeral[position] := digit_for (value)
      end
      else begin
        helper (value div base, digits_so_far + 1, position);
        position := position + 1;
        assert (position <= max_length, STRING_OVERFLOW_EXCEPTION,
                numeral_exception);
        numeral[position] := digit_for (value mod base)
      end
    end;

  begin { procedure express }
    assert (0 <= value, NEGATIVE_EXCEPTION, numeral_exception);
    assert ((MINIMUM_BASE <= base) and (base <= MAXIMUM_BASE),
            BASE_RANGE_EXCEPTION, numeral_exception);
    max_length := strmax (numeral);
    helper (value, 0, length)
  end;

  procedure base_convert (var numeral_in: string; input_base: integer;
                          output_base: integer; var numeral_out: string);
  var
    value: integer;
      { the integer value of the input numeral }
  begin
    assert ((MINIMUM_BASE <= input_base) and (input_base <= MAXIMUM_BASE),
            BASE_RANGE_EXCEPTION, numeral_exception);
    assert ((MINIMUM_BASE <= output_base) and
            (output_base <= MAXIMUM_BASE),
            BASE_RANGE_EXCEPTION, numeral_exception); 
    evaluate (numeral_in, value, input_base);
    express (value, numeral_out, output_base)
  end;

end.

This document is available on the World Wide Web as

http://www.math.grin.edu/~stone/courses/fundamentals/base-conversion.html

created January 9, 1996
last revised January 9, 1996