17.5 Very Long Cardinals--The Type Decimal

One of the problems we have when manipulating the type REAL is that numbers stored in this form are subject to being rounded off. In working with dollar amounts, rounding off errors are simply not acceptable, so many computer languages or operating environments provide some means for expressing dollar figures as sequences of digits.

Numbers that are stored as sequences of exact digits are said to be of type Decimal.

If this is done, and we have 472 + 231, a special procedure is required as in Section 7.6 to add the digits one column at a time starting from the right-hand-side and get the sequence 703.

Subtraction and multiplication present their own challenges, as does printing such quantities out, for one may store them initially as strings of digits, but when it comes to printing them, one would probably want to enter those digits into a format defined in another string and output "$7.03". (In this case, the format string used is "$#!##" where each # is a digit and the ! indicates the location of the decimal point in the result. See the examples later for details.

A model string used to specify a format for Decimal data I/O is called a format string or a picture or a mask. The purpose of a picture is to indicate where the punctuation marks are in the output string.

Various notations have different provisions for handling such types of data and for performing these operations. In some versions of Pascal, the predecessor of Modula-2, there is a built-in facility to define long integers of any specified number of digits, merely by stating in brackets after the TYPE definition the number of digits for that Integer type. COBOL, Fortran, and some BASICs have the ability to reformat such numbers into strings which can be written out as dollar amounts, social security numbers, or in any other desired fashion.

As may be guessed because of the treatment of the problem of multiplying such quantities in Chapter 7, no such data type or ability is built-in to Modula-2. Modules to implement such types are provided with some versions of Modula-2 and if such a module is available to the reader, there are some straightforward exercises on its use at the end of this chapter. If it is not, the challenge is to write it, using some of the methods of Chapter 7 and later chapters. Only a portion of that work will be shown here; the rest is left as exercises for the reader.

NOTE: Unlike the situation with complex numbers, there is no provision in ISO standard Modula-2 for such a data type. The user who needs such a facility is at the mercy of her own ability to write it or the vendor's to provide it.

The module below is one possibility for a definition of such an ADT. It can be modified for Canadian, American, or European style numeric output by changing the constants decPoint and separator and by supplying a different currency symbol in the picture string.

DEFINITION MODULE Decimals; 
(* by R. Sutcliffe
  last modified 1996 11 05 *)

CONST 
  MaxDigits = 19; 
  decPoint = '.'; 
  separator = ' '; 

TYPE
  Digit = [0..9]; 
  DecRange = [0 .. MaxDigits - 1];
  DecState = (allOK, NegOvfl, PosOvfl, Invalid); 
  DecHandler = PROCEDURE  (DecState); 
  CompareResults = (less, equal, greater);
  Decimal = 
    RECORD
      state : DecState;
      isNeg : BOOLEAN;
      number : ARRAY DecRange OF Digit;
    END;

CONST
  zero = Decimal {allOK, FALSE, {0 BY MaxDigits}};

PROCEDURE SetHandler (handler: DecHandler); 
PROCEDURE Abs (dec : Decimal): Decimal; 
PROCEDURE Add (dec1, dec2: Decimal): Decimal; 
PROCEDURE Sub (dec1, dec2: Decimal): Decimal; 
PROCEDURE Mul (dec1, dec2: Decimal): Decimal; 
PROCEDURE Div (dec1, dec2: Decimal): Decimal; 
PROCEDURE Remainder (): Decimal;
PROCEDURE Compare (dec1, dec2: Decimal): CompareResults; 
PROCEDURE Neg (dec: Decimal): Decimal; 
PROCEDURE Status (dec: Decimal): DecState; 

END Decimals.

For reasons similar to those given in the initial discussions in the last section, the numeric type is here implemented transparently. An opaque implementation would require regular procedures and variable parameters to return results of numeric operations.

The module Decimals exports the type Decimal, which can be thought of as an 19-digit long integer. Provision is made to store an error state and a sign for each such entity. Of course, Decimal should be treated as an opaque type, as though the details were not available in the definition module. Decimals also exports an apparatus for error handling that consists of a type DecState that defines the error values, a Status enquiry procedure to discover the state of any individual item, and a Handler type to define the type of an error handler procedure that a client can attach using SetHandler. This handler defaults to a procedure that does nothing at all, but a program can define a procedure taking the DecState parameter, and set it as desired. The procedure Remainder is intended to fetch the stored remainder of the last division performed.

WARNING: The implementation shown below is minimal and incomplete. In particular there are a minimum of comments. Completing it is the subject of some of the exercises at the end of the chapter.

IMPLEMENTATION MODULE Decimals; 
(* by R. Sutcliffe
  last modified 1996 11 05 *)
  
VAR 
  remainder : Decimal;
  theHandler : DecHandler;

PROCEDURE DefaultHandler (theError : DecState);
(* does nothing *)
END DefaultHandler;

(* exported procs *)

PROCEDURE SetHandler (handler: DecHandler); 
BEGIN
  theHandler := handler;
END SetHandler;

PROCEDURE Abs (dec : Decimal): Decimal; 
BEGIN
  dec.isNeg := FALSE;
  RETURN dec;
END Abs;

PROCEDURE Add (dec1, dec2: Decimal): Decimal; 
VAR
  count, temp, carry : CARDINAL;
  result : Decimal;
BEGIN
  result := zero;
  carry := 0;
  (* if both pos or both neg, just add the digits up *)
  IF ((dec1.isNeg) AND (dec2.isNeg)) OR NOT ((dec1.isNeg) OR (dec2.isNeg))
    THEN
      FOR count := 0 TO MaxDigits - 1
        DO
          temp := carry + dec1.number[count] + dec2.number[count];
          result.number[count] := temp MOD 10;
          carry := temp DIV 10;
        END;
      (* attach the common sign *)
      result.isNeg := dec1.isNeg;
      IF carry # 0
        THEN
          IF result.isNeg
            THEN
              result.state := PosOvfl
            ELSE
              result.state := NegOvfl
            END;
        END;
    ELSE (* one is neg, the other pos so find difference *)
      IF Compare (Abs (dec1), Abs (dec2)) = greater
        THEN
          FOR count := 0 TO MaxDigits - 1
            DO
              DEC (dec1.number[count], carry);
              IF dec1.number[count] >= dec2.number[count]
                THEN
                  result.number[count] := dec1.number[count] - dec2.number[count];
                  carry := 0;
                ELSE
                  result.number[count] := 10 + dec1.number[count] - dec2.number[count];
                  carry := 1;
                END;
            END;
          (* attach sign of larger in absolute value *)
          result.isNeg := dec1.isNeg;
        ELSIF Compare (Abs (dec1), Abs (dec2)) = less THEN
          FOR count := 0 TO MaxDigits - 1
            DO
              DEC (dec1.number[count], carry);
              IF dec2.number[count] >= dec1.number[count]
                THEN
                  result.number[count] := dec2.number[count] - dec1.number[count];
                  carry := 0;
                ELSE
                  result.number[count] := 10 + dec2.number[count] - dec1.number[count];
                  carry := 1;
                END;
            END;
            (* attach sign of larger in absolute value *)
          result.isNeg := dec2.isNeg;
        END;
    END;
  (* always call error handler before concluding *)
  theHandler (result.state);
  RETURN result;
END Add;

PROCEDURE Sub (dec1, dec2: Decimal): Decimal; 
BEGIN
  RETURN Add (dec1, Neg (dec2));
END Sub;

PROCEDURE Mul (dec1, dec2: Decimal): Decimal; 
(* exercise *)
END Mul;

PROCEDURE Div (dec1, dec2: Decimal): Decimal; 
(* exercise *)
END Div;

PROCEDURE Remainder (): Decimal;
BEGIN
  RETURN remainder;
END Remainder;

PROCEDURE Compare (dec1, dec2: Decimal): CompareResults; 
VAR
  count : INTEGER;
BEGIN 
  count := MaxDigits - 1;
  WHILE (count > 0) AND (dec1.number[count] = dec2.number[count])
    DO
      DEC (count);
    END;
  IF count < 0
    THEN
      RETURN equal
    ELSIF dec1.number[count] < dec2.number[count] THEN
      RETURN less
    ELSE
      RETURN greater;
    END;
END Compare;

PROCEDURE Neg (dec: Decimal): Decimal; 
BEGIN
  dec.isNeg := NOT dec.isNeg;
  RETURN dec;
END Neg;

PROCEDURE Status (dec: Decimal): DecState; 
BEGIN
  RETURN dec.state;
END Status;

BEGIN
  theHandler := DefaultHandler;
END Decimals.

Naturally, there have to be procedures for getting data into and out of the internal form. In this case, these are not located in the ADT definition module, but in two other places. First, one can define fairly straightforward input and output for Decimal quantities.

DEFINITION MODULE DecimalIO;

(* by R. Sutcliffe
  modified 1996 11 04 *)
	
IMPORT IOChan;
FROM Decimals IMPORT
  Decimal;

PROCEDURE ReadDecimal (cid : IOChan.ChanId; VAR dec : Decimal);
PROCEDURE WriteDecimal (cid : IOChan.ChanId; dec : Decimal; width : CARDINAL);

END DecimalIO.

IMPLEMENTATION MODULE DecimalIO;

(* by R. Sutcliffe
  modified 1996 11 04 *)
  
IMPORT IOChan, TextIO, IOResult;
FROM Decimals IMPORT
  Decimal, MaxDigits, DecRange, zero, DecState, decPoint;
FROM CharClass IMPORT
  IsNumeric;
FROM WholeIO IMPORT
  WriteCard;
FROM IOResult IMPORT
  ReadResults;

FROM STextIO IMPORT WriteChar;  
IMPORT SWholeIO;

TYPE
  DecString = ARRAY DecRange OF CHAR;
  
(* exported procs *)

PROCEDURE ReadDecimal (cid : IOChan.ChanId; VAR dec : Decimal);
VAR 
  temp : DecString;
  count, len : CARDINAL;
  ch : CHAR;
  res : IOResult.ReadResults;
  
BEGIN
  count := 0;
  IOChan.Look (cid, ch, res);
  IF (res = allRight)
    THEN
      dec := zero; (* initialize it *)
      dec.isNeg := (ch = "-")
    END;
  IF (ch = "-") OR (ch = "+")
    THEN
      IOChan.SkipLook (cid, ch, res);
    END;
  WHILE (count < MaxDigits) AND (res = allRight)
    DO (* skips over all non numerics *)
      IF (IsNumeric (ch))
        THEN
          temp [count] := ch;
          INC (count);
        END;
      IOChan.SkipLook (cid, ch, res);
    END;
  IF (res = allRight) OR (res = endOfLine)
    THEN 
      len := count - 1;
      WHILE count > 0 
        DO 
          DEC (count); 
          dec.number[len - count] := ORD (temp [count]) - ORD ("0");
        END; (* while *)
      dec.state := allOK;
    END; (* if *)
END ReadDecimal;

PROCEDURE WriteDecimal (cid : IOChan.ChanId; dec : Decimal; width : CARDINAL);
VAR
  count, scount : CARDINAL;
  started : BOOLEAN;
BEGIN
  started := FALSE;
  FOR count := MaxDigits-1 TO 0 BY -1
    DO
      IF (NOT started) AND ((dec.number [count] # 0) OR (count = 0))
        THEN
          started := TRUE;
          IF dec.isNeg AND (width > 1)
            THEN
              DEC (width);
            END; (* if dec *)
          IF width = 0
            THEN
              WriteChar (" ");
            ELSIF width > count + 1 THEN
              FOR scount := 1 TO width - count - 1
                DO
                  WriteChar (" ");
                END;
            END;
          IF dec.isNeg
            THEN
              WriteChar ("-");
            END; (* if dec *)
        END;
      IF started OR (count = 0)
        THEN
          WriteCard (cid, dec.number [count], 1);
        END
    END (* for *)
END WriteDecimal;

END DecimalIO.

As was the case with the module ComplexIO earlier in the chapter, the corresponding modules for the standard channels are much easier.

DEFINITION MODULE SDecimalIO;

(* by R. Sutcliffe
  modified 1996 11 04 *)
FROM Decimals IMPORT
  Decimal;

PROCEDURE ReadDecimal (VAR dec : Decimal);
PROCEDURE WriteDecimal (dec : Decimal; width : CARDINAL);

END SDecimalIO.

IMPLEMENTATION MODULE SDecimalIO;

(* by R. Sutcliffe
  modified 1996 11 04 *)
FROM Decimals IMPORT
  Decimal;
IMPORT StdChans, DecimalIO;

PROCEDURE ReadDecimal (VAR dec : Decimal);
BEGIN
  DecimalIO.ReadDecimal (StdChans.InChan(), dec);
END ReadDecimal;

PROCEDURE WriteDecimal (dec : Decimal; width : CARDINAL);
BEGIN
  DecimalIO.WriteDecimal (StdChans.OutChan(), dec, width);
END WriteDecimal;

END SDecimalIO.

The rest of the problem of moving data to and fro between Decimal and other formats is solved in yet another module that employs format or picture strings. Note that the conversion to Decimal from strings just scans the string looking for and collecting numeric digits. An alternate method is to specify an input picture, and scan it along with the input string to ensure that the correct format is used.

DEFINITION MODULE DecimalStr;
(* by R. Sutcliffe
  last modified 1996 11 05 *)

FROM Decimals IMPORT
  Decimal;

PROCEDURE StrToDec (string: ARRAY OF CHAR): Decimal; 
(* This procedure extracts the digits from any string and converts these into a decimal number.  A leading sign is correctly interpreted, but all other non numeric characters are simply ignored. *)

PROCEDURE DecToStr (dec: Decimal; picture: ARRAY OF CHAR; VAR result: ARRAY OF CHAR); 
(* Formats the Decimal according to the picture.  Characters with special meaning are:
  #  a leading blank or a digit
  9  a leading zero or a digit
  !  the decimal character defined in the module Decimals (commonly "." or ",")
  =  the sign (+ or -)
  ,  the separator defined in the module Decimals (commonly "." or "," or " ")
  all other characters in the picture string are entered into the result literally *)
 
END DecimalStr.

Notice that StrToDec is set up as a function, but DecToStr is a regular procedure that returns its result in a variable parameter.

NOTE: The use of special characters in the mask or picture, and the exact meaning given to these varies from one implementation to another. This usage is rather typical, but not identical to any particular product.

Example 1: The number 34235678945 placed into the picture "$###,###,##9!99" would result in the string "$342 356 789.45". If placed instead into the picture "=999,999,999,999" it would result in the string "+ 34 235 678 945". (The definition module is compiled with space as the separator rather than the American comma.)

Example 2: If the string "-1.2345" is read by ReadDecimal only the digits and sign are stored, so the resulting Decimal value placed either into the picture "##!99" or the picture ##.99" would result in the string "123.45". (The extra digit takes up some room at the beginning.) If placed instead into the picture "=##99999!9" it would result in the string "- 01234.5". There are two spaces before the leading zero because there is room for eight figures provided by the mask and only five are needed.

Example 3: If the string "23" is read by ReadDecimal and the resulting Decimal value placed into the picture "##99!99999" the result string is " 00.00023" but if into the picture "##99.##999" the result string is " 00. 023" which is probably not too useful, but is according to the picture.

Here is the implementation module for DecimalStr. Once again the commenting is minimal so that the reader may add this apparatus.

IMPLEMENTATION MODULE DecimalStr;
(* by R. Sutcliffe
  last modified 1996 11 05 *)

FROM CharClass IMPORT
  IsNumeric;
FROM Decimals IMPORT
  Decimal, MaxDigits, zero, decPoint, separator;
  
PROCEDURE StrToDec (string: ARRAY OF CHAR): Decimal; 
(* This procedure extracts the digits from any string and converts these into a decimal number.  A leading sign is correctly interpreted, but all other non numeric characters are simply ignored. *)
VAR
  temp : Decimal;
  counts, countd : CARDINAL;
BEGIN
  temp := zero;
  counts := LENGTH (string);
  countd := 0;
  WHILE (counts > 0) AND (countd < MaxDigits)
    DO
      DEC (counts);
      IF IsNumeric (string[counts])
        THEN
          temp.number[countd] := ORD (string [counts]) - ORD ("0");
          INC (countd);
        END;
    END;
  IF string [0] = "-"
    THEN
      temp.isNeg := TRUE;
    END;
  RETURN temp;
END StrToDec;

PROCEDURE DecToStr (dec: Decimal; picture: ARRAY OF CHAR; VAR result: ARRAY OF CHAR); 
(* Formats the Decimal according to the picture.  Characters with special meaning are:
  #  a leading blank or a digit
  9  a leading zero or a digit
  !  the decimal character defined in the module Decimals (commonly "." or ",")
  =  the sign (+ or -)
  ,  the separator defined in the module Decimals (commonly "." or "," or " ")
  all other characters in the picture string are entered into the result literally *)

VAR
  counts, countd, countr, maxs, maxr, picDigits, pad : CARDINAL;
  ch : CHAR;
  decDone : BOOLEAN;
  
BEGIN
  decDone := FALSE;
  maxs := LENGTH (picture);
  maxr := HIGH (result);
  picDigits := 0;
  FOR counts := 0 TO maxs - 1
    DO
      ch := picture [counts];
      IF (ch = "#") OR (ch = "9")
        THEN
          INC (picDigits)
        END
    END; 
  counts := 0;
  countd := MaxDigits;
  countr := 0;
  WHILE (countd > 0) AND (dec.number [countd-1] = 0)
    DO
      DEC (countd)
    END;
  IF picDigits > countd
    THEN
      pad := picDigits - countd;
    ELSE
      pad := 0;
    END;
  (* special case zero *)
  IF countd = 0
    THEN
      INC (countd);
    END;
  ch := picture [counts];   
  WHILE (counts < maxs) AND (countd > 0) AND (countr < maxr)
    DO
      IF (ch = "#") OR (ch = "9")
        THEN
          IF pad = 0
            THEN
              DEC (countd);
              result [countr] := CHR (dec.number[countd] + ORD ("0"));
            ELSE   (* fill in spaces or zeros from # and 9 places not used in dec *)
              IF (ch = "#")
                THEN
                  result [countr] := " ";
                ELSIF (ch = "9") THEN
                  result [countr] := "0";
                END;
              DEC (pad);
            END;
          IF countd < picDigits
            THEN
              INC (counts);
            END;
        ELSIF (ch = "!") THEN
          result [countr] := decPoint;
          INC (counts);
        ELSIF (ch = ",") THEN
          result [countr] := separator;
          INC (counts);
        ELSIF (ch = "=") THEN
          IF dec.isNeg
            THEN
              result [countr] := "-"
            ELSE
              result [countr] := "+"
            END;
          INC (counts);
        ELSE
          result [countr] := ch;
          INC (counts);
        END;
      INC (countr);
      ch := picture [counts];
    END;
  WHILE (counts < maxs) AND (countr < maxr)
    DO (* copy any stuff left in picture; must be literals *)
      result [countr] := ch;
      INC (counts);
      INC (countr);
      ch := picture [counts];
    END;
  IF (countr < maxr)
    THEN
      result [countr] := 0C;
    END; 
END DecToStr;

END DecimalStr.

As indicated in the examples already discussed, a program can read Decimal quantities in the form of strings (perhaps in picture form), assign them to variables of type Decimal, manipulate them, and then print them out using pictures (perhaps of a different form than in the way they were entered). Here is an example:

Similarly, one could use this functionality to save and print Social Security (Insurance) or credit card numbers in a form with spaces or dashes at appropriate places.


Contents