!pr0
18-Digit Arithmetic, Part 4................Bob Sander-Cederlof

This month we will look at two output conversion routines.  The first one always prints in exponential form, while the second one allows setting a field width and number of fractional digits.  The routines are written so that the output string may either be printed or fed to an Applesoft string variable.

Let's assume that the value to be printed has already been loaded into the DP18 accumulator, DAC.  Lines 1230-1270 describe DAC as a 12-byte variable.  The exponent is in the first byte, DAC.EXPONENT.  It has a value from $00 to $7F:

       $00 means the whole number is zero
       $01 means the power of ten exponent is -63
       $3F means 10^-1
       $40 means 10^0
       $41 means 10^1
       $7F means 10^63

The 18 digits of the number, plus two extension digits, are in the next ten bytes in decimal format (each digit takes four bits).  The extension is zeroed when you load a fresh value into DAC, but after some computations it holds two more digits to guard against roundoff and truncation problems.

The sign of the number is stored in DAC.SIGN:  if the value in DAC.SIGN is from $00 to $7F, the number is positive; if from $80 to $FF, the number is negative.

If you have been following the DP18 series from the beginning, and typing in all the code (or getting it from the Quarterly Disks), then you will realize that to integrate each installment takes some work.  In order to print the sections separately, and have them separately readable, I must repeat some variable declarations.  The listing this month refers to two previously printed subroutines, DADD and MOVE.YA.ARG.  These are simply equated to $FFFF in lines 1030 and 1040, so that the code will assemble.  If you really want it to work, you have to remove those two lines and include the code for the subroutines.  The fact that three installments have already been printed also somewhat restricts me, because even if I see possible improvements I must be careful not to contradict the code you already have.


Quick Standard Format Conversion

The subroutine QUICK.FOUT which begins on line 1600 converts the contents of DAC to a string in FOUT.BUF in the format

       sd.dddddddddddddddddEsxx

The first s is the sign, which is included only if negative.  The d's are a series of up to 18 significant digits (trailing zeroes will not be included).  The letter E is always included, to signify the power-of-ten exponent field.  The letter s after the E is the sign of the exponent:  it is always included, and will be either + or -.  The xx is a two-digit exponent, and both digits will always be included.  The decimal point will be included only if there are non-zero digits after it.  If the number is exactly zero, the string in FOUT.BUF will be simply "0".  Here are some more examples:

       value           string
       1000            "1E+03"
       .001            "1E-03"
       -262564478.5    "-2.625644785E+08"

Two processes are involved in converting from DAC to FOUT.BUF.  One is the analysis of the DAC contents; the other is the process of storing sequential characters into FOUT.BUF.  The latter process is handled in most cases by the subroutine at lines 3720-3820.  Entry at STORE.CHAR stores the contents of the A-register in the next position in FOUT.BUF, and increments the position pointer (INDEX).  Entry at STORE.DIGIT first converts the value in the A-register to an ASCII digit by setting the high nybble to "3".  (The digits 0-9 are $30-$39 in ASCII.)

QUICK.FOUT begins by setting INDEX, the FOUT.BUF position pointer, to 0.  At lines 1630-1700 the special case of the value in DAC being exactly zero is tested and handled.  If the value in DAC is zero, then DAC.EXPONENT will be zero.  (This is a convention throughout DP18, to simplify making values of zero and testing for them.)  If the value is zero, ASCII zero is stored in FOUT.BUF, followed by a terminating $00 byte.

If the value is not zero, the next job is to check the sign of the value.  Lines 1710-1740 insert a minus sign in FOUT.BUF if the value is negative.

Lines 1760-1910 pull out the 18 digits of the mantissa from DAC.HI through DAC.HI+8.  The extension digits are ignored.  The code here looks an awfully lot like a routine to convert from hex to ASCII, ignoring the possible hex digits A-F.  That is because the digits are four bits each, and ARE like hex digits.  Lines 1830-1860 insert the decimal point after the first digit.

Lines 1930-2020 look at the formatted number in FOUT.BUF and trim off the trailing zeroes.  If all digits after the decimal point are zero, the decimal point is trimmed off too.  If you would rather that QUICK.FOUT always printed exactly 18 digits, trailing zeroes and all, you could cut out these lines.

Lines 2040-2290 format the exponent field.  First the letter E is installed in FOUT.BUF.  Then lines 2060-2120 install the exponent sign.  There is a little adjustment here due to the fact that the value in DAC is in the form ".DDDD" times a power of ten, and we are converting to "D.DDD" times a power.  That means the exponent in DAC.EXPONENT is one larger than we will print.  The DEY at line 2080 adjusts for this offset.

Lines 2130-2180 get the absolute value of the exponent by removing the $40 bias and taking the 2's complement if the result is negative.  Lines 2190-2290 convert the binary value of the exponent to two decimal digits, and insert them into FOUT.BUF.  Lines 2300-2310 terminate the FOUT.BUF string with $00.

Once the value has been converted to a string in FOUT.BUF, we can either print it or put it into an Applesoft string variable.  The subroutine QUICK.PRINT which begins at line 1370 calls QUICK.FOUT and then prints the characters from FOUT.BUF.


Fancier Formatted Conversion

The second conversion routine, which begins at line 2350, allows you to specify the number of digits to display after the decimal point, and the number of characters in the output field.  The value will be formatted into the field against the right end, with leading blanks as necessary to fill the field.  The value will be rounded to the number of digits that will be converted.  If you are familiar with the FORTRAN language, you will recognize this as the "Fw.d" format.  W is the width of the field, and D is the number of fractional digits.  Here are some examples:

     W  D   value   string
     12 5   1234.56 "  1234.56000"
     12 1   1234.56 "      1234.6"

As before, the output string will be stored in FOUT.BUF in ASCII code, terminated by a $00 byte.  If the value will not fit into the W.D field you specify, the entire field will be filled with "*" characters.

As listed here, I have set FOUT.BUF as a 41-byte variable.  This means the maximum W is 40, leaving room for the terminating $00 byte.  If you want longer conversions, simply change line 1060.

FOUT expects the W and D parameters to be in the A- and Y-registers, respectively.  Lines 2380-2460 check W and D for legality.  If W is larger than FOUT.BUF.SIZE-1, then it is set to that value.  We don't want to store converted characters beyond the end of FOUT.BUF!  Then if D is larger than W-1, it is pruned back.

Lines 2480-2540 initialize various variables used during the following conversion.  Once again, INDEX will point to the position in FOUT.BUF.  I could probably have economized some in the use of variables by re-using the same variables for different purposes, but I wanted to keep them separate to make it easier to code and debug.

Line 2560 calls ROUND.DAC.D to round the value in DAC to D digits after the decimal point.  This boils down to adding .5 times 10 to the D power to the value in DAC.  ROUND.DAC.D, at lines 3860-4000, does just that.  First the rounding number is built in ARG, then DADD adds ARG to FAC.

Lines 2570-2610 store a minus sign into SIGN.CHAR if the value in DAC is negative.  SIGN.CHAR was initialized to $00 above.  If the sign is negative, line 2590 will increment SIGN.SIZE.  SIGN.SIZE will either be 0 or 1, and will be used later in determining how many leading blanks are needed.  We cannot store the sign character into FOUT.BUF until the leading blanks have been stored.

Lines 2630 to 2710 compute how many digits will be printed before the decimal point (NO.LEADING.DIGITS), and how many zeroes before the first significant digit after the decimal point (NO.LEADING.ZEROES).  If the power-of-ten exponent was negative, there will be no leading digits and some leading zeroes; if positive, there will be some leading digits and no leading zeroes.  For example,

    .2345E-5     .000002345    5 leading zeroes
    .2345E+3     234.5         3 leading digits

What if the exponent is more than 18?  This would mean more digits might be extracted from DAC than exist, so lines 2730-2790 limit NO.LEADING.DIGITS to 18.  NO.INTEGRAL.ZEROES takes up the slack, to print any necessary zeroes between the last significant digit before the decimal point, and the decimal point.  For example, if W=25 and D=2, and the value is .1234E+20, we will get NO.LEADING.DIGITS=18 and NO.INTEGRAL.ZEROES=2:

       "  12340000000000000000.00"

Lines 2810-2870 now calculate the total number of non-blank characters which will be required:  one for sign if the sign is negative, all the leading digits and integral zeroes before the decimal point, one for the decimal point itself, and D fractional digits.  (Just now I noticed that I could have saved two bytes and two cycles by changing line 2810 from CLC to SEC, and eliminating the ADC #1 at line 2860.)

Lines 2890-2920 compute how many significant digits of fraction will be needed.  You specified D digits of fraction, but only DD of them will come from the value in DAC.  This will be less than D if there are any leading zeroes.

Lines 2940-2970 check whether the converted number can fit in a W-wide field.  If not, Lines 3370-3400 fill the field with stars and exit.

Lines 2980-3030 compute how many leading blanks will be needed to right justify the number in the W-field.  There is some hopscotch here because we are going to put "0." in front of numbers that have no integral digits.

At long last, we are ready to begin string characters in FOUT.BUF.  Lines 3050-3070 store the leading blanks.  A subroutine STORE.N.CHARS does the dirty work.  STORE.N.CHARS (lines 3670-3710) expects the character to be stored in the A-register, and the count in the Y-register.  It also expects that the Z-status is set according to the count in Y.  Thus, if the count is zero, the subroutines returns immediately without storing any characters.

STORE.N.DIGITS, at lines 3440-3620, is quite similar to STORE.N.CHARS.  Once again, the count must be in the Y-register and the Z-status should reflect the value in Y.  Digits are picked out of the value in DAC using an index DIGIT.PICKER, and stored into FOUT.BUF using STORE.DIGIT.

Lines 3090-3110 store the sign if it is negative.  Lines 3120-3210 print whatever digits are needed before the decimal point.  This will include leading digits (if any) and integral zeroes (if any), or simply one zero (if neither of the other).

Lines 3230-3320 store the fractional part.  This includes the decimal point, leading fractional zeroes (if any), and fractional digits (if any).

Finally, lines 3340-3350 store a terminating $00 at the end of the string in FOUT.BUF.

A subroutine called FORMAT.PRINT at line 1450 calls FOUT and then prints the contents of FOUT.BUF.  You could now write a higher level routine, if you wish, which would examine the exponent to determine whether the number would fit in a 20-character field.  If not, you could use QUICK.PRINT.  If so, use FOUT with W=40 and D=18, and then truncate leading spaces and trailing zeroes.  This would give you a complete print routine for any numbers, printing them in simple form when they fit and exponential form when they don't.  Indeed, just such a routine already exists in DP18, but will have to wait for a future installment.  FOUT can also be used as the base for a complete PRINT USING facility, and that is also already in DP18 waiting for future installments.
  Meanwhile, enjoy these two conversions, and experiment with your own.
