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.