## 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