Number Format
Volume Number: 3
Issue Number: 11
Column Tag: Assembly Language Lab
Formatted Output for Numbers 
By Mike™ Scanlin, San Diego, CA
On most systems, programmers of high level languages have an advantage over
assembly language programmers in that a lot of the nitty gritty detail work has been
done for them by what ever compiler or interpretter they’re using. An example of this
is in the outputting of numbers. High level language programmers usually take it for
granted that they can output a number in just about any format they want to. Pascal’s
writeln and C’s printf make it a trivial task to change the field width of both integers
and reals. While it’s true that assembly programmers can make use of _NumToString
or the SANE formatter, we still need to play with the resulting strings to get nice
formatted numbers. Until now.
CONVERTING INTEGERS
The basic idea when converting an integer into its string equivalent is to first
break it down into its digits and then convert each digit into its ASCII equivalent. The
only tricky part is that a number is represented as binary internally and we need its
decimal equivalent.
There are at least 2 different ways to approach the problem of extracting base 10
digits from a binary number. The _NumToString routine that Apple provides in the
system file (see listing 1) uses binary coded decimal (BCD) arithmetic to calculate the
digits and then calls a separate subroutine to build the string two digits at a time (the
least significant bytes of registers D1-D5 are used to store 2 BCD digits each). The
main problem with this algorithm is that it runs in more or less constant time (the
number 1 takes as long to convert as 2147483648 = 2^31) and isn’t very efficient
except for very large numbers (about 7 or more digits).
Another way to get digits one by one is to subtract by successively smaller
powers of 10, starting with 10^9 (since it is the largest power of 10 that can be
represented by a 32 bit integer). Count how many times you can subtract each power
of 10 from the number and then convert that number (which will be in the range 0..9)
to its ASCII equivalent. If you use multi-word compares and subtracts, this method can
be used to convert any size integers (like 64 or 128 bits) into strings.
My own NumToString routine (Shown in the program listing) uses the subtract
method. How does the subtract method compare to the real _NumToString? As for size,
_NumToString is 118 bytes (after all of the trap related instructions are removed)
and my routine is 120 bytes. Pretty close. As for speed, time trials on 10,000 random
32 bit signed integers showed my routine to be faster by about 2% -- no big deal. But
for time trials on 10,000 16 bit signed integers my routine was faster by 32% and
for 10,000 8 bit signed integers my routine was faster by 45% (to be fair,
_NumToString was timed after it had been isolated so there was no overhead for trap
calling). The reason the smaller numbers showed so much improvement is because my
routine doesn’t spend much time on little digits (0,1,2...) and virtually no time on
leading zeros. If you have a time critical application that makes a lot of calls to
_NumToString, it may be worthwhile to use my routine instead. Of course, you could
just patch my routine over the existing _NumToString in the system file to speed up
all applications that use _NumToString -- no, wait, I didn’t just say that (and I’m not
responsible for the consequences. Is there any reason that wouldn’t work?).
Fig. 1 Demo Program formats numerical output
CONVERTING FIXED POINT REALS
When you use DIVS or DIVU, the 32 bit result is made up of 16 bits of quotient
and 16 bits of remainder. The FixPtToString routine uses these two pieces to convert a
fixed point number to a string. Since the quotient is just an integer, we can use
NumToString to convert it. Then add a decimal point and convert the remainder. But in
order to convert the remainder into a fractional number, we need to know the original
divisor. We also need to specify how many digits we want after the decimal point.
For each digit you want after the decimal point, the routine multiplies the
remainder by 10 and then divides the it by the divisor, getting a new remainder in the
process. It adds the digit to the string and then repeats the process for the next digit.
However, there is a limited number of times that this can be done accurately (since we
only have 16 bits of remainder to begin with). The number of digits that can be
accurately calculated depends on the magnitude of the divisor, but 5 or 6 digits should
be safe.
Note that the number you pass to FixPtToString doesn’t have to be the result of a
divide instruction. You can output any arbitrary fixed point number you want by using
the same basic idea. If you wanted an integer part bigger than 16 bits, you could output
the number in a two part process. FixPtToString could be modified to be
FractionToString by eliminating instructions 3 (EXT.L D0) thru 17 (BEQ.S @6)
inclusive. Then it will tack on whatever fraction you pass it to what ever string you
pass it. For instance, to output the number 1864723135.24226 (.24226 =
47/194):
MOVE.L stringPtr(A5),A0
MOVE.L #1864723135,D0 ;quotient
JSR NumToString
MOVE #47,D0 ;remainder
SWAP D0 ;no need to set quot
MOVE #194,D1 ;divisor
MOVE #5,D2 ;5 digits accuracy
JSR FractionToString
MOVE.L A0,-(SP)
_DrawString
FORMATTING
Now that we can get integers and reals into strings we need to be able to set field
widths. Also, an option to add commas would be nice. The FormNumString routine does
both of these. You pass it a pointer to a format string of the form: [‘,’][q[‘.’[r]]]
where [ ] denotes something optional and ‘.’ and ‘,’ denote a constant character. Q and r
are string variables in the range [‘0’..’99'] and represent how many spaces should be
allocated for the quotient (including sign and commas) and remainder (not including
decimal point). Notice that everything is optional, so the empty string is a legal one,
but one that won’t do much (i.e. any) formatting. Also, the word “string” here does not
mean a pascal string; i.e. there is no length byte. The nested brackets mean that you
can’t have a ‘.’ if a q was not provided and you can’t have an r if a ‘.’ wasn’t provided. If
q is too small to contain the number, space is used as needed. If it is too big, the
number will be padded with spaces. If r is bigger than any existing remainder the
number might have, zeros are appended after the decimal point (which doesn’t do much
for the accuracy of the extra digits). If r is smaller than any existing remainder, then
digits are just dropped. No attempt at rounding is made.
Another use for this routine is to reformat the output string from the SANE
formatter. The SANE formatter can format any SANE data type into a fixed sytle
number (see the Apple Numerics Manual for everything you ever wanted to know
about SANE). So, you could use SANE to do all of your calculations in floating point and
have your result formatted into a fixed style string and then use FormNumString to add
commas, pad with spaces and delete decimal places (or add zeros) to make all your
numbers uniform.
Fig. 2 Program supports easy updating with a picture
PUTTING IT ALL TOGETHER
The 3 routines NumToString, FixPtToString and FormNumString have been
pieced together to form DrawNumsInAString which can be used to output complete
sentences containing any number of formatted numbers. For instance, you could pass it
the string “result = \f,8.4.” along with the fixed point number 123456.789 and it
will output “result = 12,3456.7890.” Each number you want formatted in the input
string will begin with a ‘\’ character and be followed by an ‘i’ (for longints) or an ‘f’
(for fixed points). After that comes the FormNumString style format codes for the
number. The only peculiar part of using the routine is that the numbers you want
formatted have to be pushed on the stack in reverse of their occuring order in the
string. For instance, if your input string looks like “x = \i4 y = \i4 z = \i4” then
you would do this:
MOVE.L z,-(SP) ;3rd parameter
MOVE.L y,-(SP) ;2nd
MOVE.L x,-(SP) ;1st
PEA inputString
JSR DrawNumsInAString
The reason for passing the parameters that way is (1) because we don’t know
how many will be present in advance, and (2) so that they can be taken off the stack in
the order needed.
DEMO PROGRAM
The FormatDemo program shows how all of this comes together. It is a bare bones
application that demonstrates how the routines presented here might be used. A click in
the window will generate another set of random numbers to format. Desk accessories
are supported and you can see a simple technique for providing window updating
without an update event. Whenever a content click is detected, a set of addition and
division problems are displayed in formatted output using the formatting routines
discussed. After displaying the numbers, a quickdraw picture is taken of the window
output using copybits on the portRect of the window. The appropriate field in the
window record is set with the pointer to this picture so that when the window needs
updating, it is updated automatically from the picture. Figures 1 and 2 show the demo
program in operation with a desk accessory showing this easy update method. A good
reference book for this and all assembly language techniques is Dan Weston’s classic
The Complete Book of Macintosh Assembly Language Programming, vol. 1 and 2, from
Scott, Foresman and Company.
{1}
Listing 1. The Apple Way
; NOTE: This code is Apple Computer’s. Only
; the comments are mine.
;==========
;==========
; convert a 32 bit longint into a pascal string
; input: D0 longint
; A0 points to a space of at least 12 bytes
; output: A0 points to pascal string
MOVEM.L D0-D6/A1,-(SP)
MOVEQ #0,D1 ;init digits
MOVEQ #0,D2