Would you like to make this site your homepage? It's fast and easy...
Yes, Please make this my home page!
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Adding 2 decimal values (using only COMMAND.COM)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here I present a pure batch solution that will add two decimal
values specified at the command line. The invocation syntax of this
demonstration is;
PROGRAM value1 value2
The decimal values can be almost as large as desired (there are
no specific program limits though the DOS command line buffer will
choke at a certain point depending on the version of DOS it is
run in). When the demonstration program has finished the calculation
the result is simply output to the screen. The program can easily
be modified for use as a module in a larger script.
The main features of this program are;
+ runs in DOS 5, 6 and 7
+ uses no external utilities, files, pipes or redirection
excepting one invocation of COMMAND.COM
+ relatively fast timings (around 1minute for 50 digit operands)
+ will accept very large values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Format the 2 values
(a walk-through the 1st block of the implementation)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
First the values are formatted so that each has the same number
of digits with an added leading zero and so that they
are in reverse digit order. This is so that the adder
function block that comes next will be able to parse
the values left to right propagating any carry to the
next digit. The added leading zero ensures that a carry
will not propagate from the last digit.
Formatting example:
N1,N2 values O1,O2 formatted values
N1=123 }become {O1=3210000
N2=456789 } {O2=9876540
First %T% is set up with just enough spaces to overflow the command
line buffer so that '%T%SET XX=%YY%' will be truncated and
assign the first character of YY to XX.
:K0
SET T= %=%
SET T=%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%
%N1% and %N2% hold the numbers to be added. %O1% and %O2% will be
set with the formatted versions. %C% and %R% are cleared for the
next stage. The program takes %N1% and %N2% from the command line
for the purpose of a working demonstration.
SET N1=%1
SET N2=%2
FOR %%_ IN (C R O1 O2) DO SET %%_=
L0 is the main loop point for parsing digits in N1 and N2.
:L0
The next two blocks are exactly symmetrical, each performs the
same function on N1 and N2 respectively. Dx is set with the
first digit of Nx, that digit is deleted from Nx, then if
Dx was nul (Nx digits have been exhausted) a padding zero
is added to Ox else the digit is added to Ox. The padding zero
is added to the right side of Ox because the rightmost digits
of Ox are the most significant. Normal digits (from Nx) are
added at the left side because progress through Nx is toward
the least significant.
%T%SET D1=%N1%
FOR %%%D1% IN (() DO SET N1=%%%N1%
FOR %%_ IN %N1%) DO SET N1=%%_
IF %D1%==( SET O1=%O1%0
IF NOT %D1%==( SET O1=%D1%%O1%
Some explanation/notes for the string splicing functions here
can be found in the article "Getting the current date (reverse
engineering system date format)"
%T%SET D2=%N2%
FOR %%%D2% IN (() DO SET N2=%%%N2%
FOR %%_ IN %N2%) DO SET N2=%%_
IF %D2%==( SET O2=%O2%0
IF NOT %D2%==( SET O2=%D2%%O2%
When both %D1% and %D2% are equal to '(' digits from both values %N1%
and %N2% have been exhausted and are properly formatted in %O1% and
%O2%. Because the loop continues until this condition %O1% and %O2%
will always have one "leading zero" (now on the right because
%O1%,%O2% have most significant digits on the right).
IF NOT %D1%%D2%==(( GOTO L0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Adding 2 single digits
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
At the heart of the routine lies a method to add 2 digit values
and a possible carry (+1). This is done by first converting
the digits into bang counts (a sequence of '!' characters the number
of which is the value of the digit), the bang counts are concatenated
and then the total bang count is converted back into digits.
So the basic idea is;
4 9
converted to bang counts
!!!! !!!!!!!!!
concatenated
!!!!!!!!!!!!!
converted to digits
1,3
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Converting a digit to a bang count
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
One of the key components of the implementation is a function
that converts a single decimal digit to an equivalent number
of bangs. Obviously this could be done with a simple list of
IF/THENs;
SET B=
IF %D%==1 SET B=!
IF %D%==2 SET B=!!
IF %D%==3 SET B=!!!
IF %D%==4 SET B=!!!!
IF %D%==5 SET B=!!!!!
IF %D%==6 SET B=!!!!!!
IF %D%==7 SET B=!!!!!!!
IF %D%==8 SET B=!!!!!!!!
IF %D%==9 SET B=!!!!!!!!!
However line count can be reduced without any significant performance
depreciation by using binary to group the checks and accumalating
the bangs instead;
---------------------------------------------------------------------
8 4 2 1 BANG SUM
---------------------------------------------------------------------
0 0 0 0 0
1 0 0 0 1 +1
2 0 0 1 0 +2
3 0 0 1 1 +1 +2
4 0 1 0 0 +4
5 0 1 0 1 +1 +4
6 0 1 1 0 +2 +4
7 0 1 1 1 +1 +2 +4
8 1 0 0 0 +8
9 1 0 0 1 +1 +8
---------------------------------------------------------------------
The initial psuedo code would be;
bangs=""
IF digit=1 OR digit=3 OR digit=5 OR digit=7 OR digit=9 THEN bangs=bangs+"!"
IF digit=2 OR digit=3 OR digit=6 OR digit=7 THEN bangs=bangs+"!!"
IF digit=4 OR digit=5 OR digit=6 OR digit=7 THEN bangs=bangs+"!!!!"
IF digit=8 OR digit=9 THEN bangs=bangs+"!!!!!!!!"
Of course ORs do not translate well into batch code. You can have
them with FOR;
FOR %%_ IN (1 3 5 7 9) DO IF %D%==%%_ SET B=%B%!
But FOR execution is much slower than using IF/THEN. Of course
it is possible to invert the checks and do an AND;
IF NOT %D%==0 IF NOT %D%==2 ... IF NOT %D%==8 SET B=%B%!
This runs much faster than using FOR but has the problem that
it generates very long lines. While it is acceptable for a batch
to have lines longer than 80 characters (provided they never
become more than 127 characters after variable substitution due
to the limits in DOS6 and earlier) it is highly desirable to
stick within that limit for publishing purposes and for the
purposes of users editing with editors that do not support wrapping
line display. For this reason a macro was created to contract
physical line width;
SET D=IF NOT %D%
%D%==0 %D%==2 %D%==4 %D%==6 %D%==8 SET B=%B%!
The digit value in %D% is replaced with the macro because the
implementation did not require it to be saved (and so keeping
variable count down).
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Converting a bang count to digits
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The reverse operation needs to do a little more work since the
bang count to be converted will be the sum of 2 digits and a
possible carry, in all a total of 19 bangs could be present.
The result should be a single digit (units) plus a carry (tens).
The carry can obviously only be 0 or 1.
Initially the implementation used a method utilising CHOICE
to get the number of bangs into the ERRORLEVEL and then
converting the ERRORLEVEL into a decimal value;
ECHO @|CHOICE /C!!!%B%@>NUL
This sets ERRORLEVEL to 4+the length of %B%
FOR %%_ IN (0 1) DO IF ERRORLEVEL Y%%_0 SET C=%%_
FOR %%_ IN (0 1 2 3 4 5 6 7 8 9) DO IF ERRORLEVEL Y%C%%%_ SET D=%%_
This converts ERRORLEVEL-4 into a decimal value where D=units
and C=tens (carry). It uses a technique involving overflowing
the ERRORLEVEL that you will find explained in other documents
in the batch section. Eventually I will put these techniques into
one paper so they may be easily referenced but for now it is
not important whether you can understand how this works only
that it is criminally slow. The speed hit comes from the pipe
(which causes DOS to create a temporary file) and the invocation
of an external command (which has to be loaded each time around--
remember this function is going to be used for every digit in the
operands).
Instead of using this technique the implementation goes back
to the much more elementary method of simply using a lot
of IF/THEN's.
Literally that works out as more than twenty lines of code;
FOR %%_ IN (C D) DO SET %%_=
IF !%B%==! SET D=0
IF !%B%==!! SET D=1
.
. (etc)
.
IF !%B%==!!!!!!!!!! SET D=9
IF !%D%==! SET C=!
IF !%B%==!!!!!!!!!!! SET D=0
IF !%B%==!!!!!!!!!!!! SET D=1
.
. (etc)
.
IF !%B%==!!!!!!!!!!!!!!!!!!!! SET D=9
(C being a ! for carry=1 and nul for carry=0)
But it is 3 times faster than using CHOICE to do it and those
lines can be cut down into half with very little performance
depreciation. The actual implementation simply uses the symmetry
between the checks for 0-9 bangs and 10-19 bangs to reuse
the same chunk of code.
IF !%B%==!%_% SET D=0
IF !%B%==!!%_% SET D=1
.
.(etc)
.
IF !%B%==!!!!!!!!!!%_% SET D=9
These ten checks can be looped through once with %_% set to
nothing, then again (if no match was found) with %_% set to
ten bangs. If a match is found on the first loop the carry
is set to nothing, else it is set to ! (the carry is
kept as a bang count).
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Add the 2 values
(a walk-through the 2nd and last block of the implementation)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The values now being processed are computer friendly ones in %O1% and
%O2% that start at the left with the least significant digits and
are exactly the same length. Basic operation is simply to
take each digit in turn (left to right) adding them together
with a possible carry (%C%) and accumalating the result (%R%).
%C% has already been cleared by the 1st block of code so that
it is zero for the first time around. %R% has also been cleared
ready to accumalate the result digit string.
Since converting a digit to a bang count needs to happen twice
it is put into a sort of subroutine at :DBANG. The routine ends
with a macro "%RET%" which can be defined by the caller as
GOTO:returnlabel to facilitate continuation at the point after
calling.
The first number %O1% is processed. The next digit is put into %D%
and deleted from it. DBANG is called to convert %D% into bangs and
return to the next line.
:L1
%T% SET D=%O1%
FOR %%%D% IN (() DO SET O1=%%%O1%
FOR %%_ IN %O1%) DO SET O1=%%_
SET RET=GOTO:K2
GOTO DBANG
:K2
DBANG actually adds a bang count to %C%. This is why the carry %C%
is maintained as a bang count; firstly the digit from %O1% is added
to it, next the digit from %O2%.
%T% SET D=%O2%
FOR %%%D% IN (() DO SET O2=%%%O2%
FOR %%_ IN %O2%) DO SET O2=%%_
SET RET=GOTO:L2
Notice where the return point is set. The code could have been
constructed in a little less spaghetti like manner however
one of the goals of the implementation was to keep line
count down. :L2 is the loop head for convert the total (digit1+
digit2+ carry) back to a digit and a carry, the DBANG routine
follows here just before that loop in order that instead
of using another GOTO DBANG we can just fall through into it.
:DBANG
The original algorithm for converting a digit to a bang count
has to be modified a little here due to the following.
The longest line will now be the one checking for 8 or 9;
:: %D%==0 %D%==1 %D%==2 %D%==3 %D%==4 %D%==5 %D%==6 %D%==7 SET C=%C%!!!!!!!!
After variable substitution this is 110 characters long. That
allows %C% to contain up to 17 characters before there is a problem
with DOS6 and earlier truncating the command line due to buffer
size. Unfortunately the implementation actually uses %C% to accumalate
bangs up to a count of 19 (two digit values and a possible carry)
so that line would be unsuitable. For this reason a FOR loop was
used instead.
FOR %%_ IN (8 9) DO IF %D%==%%_ SET C=%C%!!!!!!!!
SET D=IF NOT %D%
%D%==0 %D%==1 %D%==2 %D%==3 %D%==8 %D%==9 SET C=%C%!!!!
%D%==0 %D%==1 %D%==4 %D%==5 %D%==8 %D%==9 SET C=%C%!!
%D%==0 %D%==2 %D%==4 %D%==6 %D%==8 SET C=%C%!
Two variables %_% and %D% are cleared. This is really initialisation
for entry to the loop at :L2 and it's a bit redundant doing it twice
however a control flow problem exists if %D% in particular is to
be cleared before entry to :L2 but after completion of the above
function.
SET _=
SET D=
%RET%
The second entry point to the loop L2 (bang count to digit convertor)
which will be taken if we have to loop a second time. %_% holds the
carry (tens) and %D% is used to add ten bangs to the bang count
check lines.
:L3
SET _=!
SET D=!!!!!!!!!!
The number of bangs in %C% is converted to a digit %D%.
:L2
IF !%C%==%D%! SET D=0
IF !%C%==%D%!! SET D=1
IF !%C%==%D%!!! SET D=2
IF !%C%==%D%!!!! SET D=3
IF !%C%==%D%!!!!! SET D=4
IF !%C%==%D%!!!!!! SET D=5
IF !%C%==%D%!!!!!!! SET D=6
IF !%C%==%D%!!!!!!!! SET D=7
IF !%C%==%D%!!!!!!!!! SET D=8
IF !%C%==%D%!!!!!!!!!! SET D=9
If %D% wasn't set to the digit on the first loop then go through
the checks again but this time carry (now in %_% ) is set to 1
and %D% is set so that we are comparing +10 bangs. The 2nd loop
will always succeed in setting %D% to a digit. Notice %D% is
used doubly here.
IF !%D%==! GOTO L3
Now %C% is set to the carry, ready for the next digits of
the source operands.
SET C=%_%
When the source operands %O1% and %O2% run out of digits, they will
become '(' simultaneously and the operation has been completed.
If we are about to terminate and the last digit is a zero then
we don't add it, this ensures no leading zeros on the result.
If there are more digits then we loop back to :L1.
IF %O1%%D%==(0 SET D=
SET R=%D%%R%
IF NOT %O1%==( GOTO L1
The result is printed and the program ends.
ECHO [RESULT] %R%
:OUT
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ADDING 2 DECIMAL VALUES (full working demonstration listing)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The full working demonstration program listing is almost exactly
the same as the code that has been detailed above, the only difference
is a small header which makes the program execute in another
invocation of COMMMAND.COM only with the environment size set
to 4096 bytes. This is because the program requires around 2k
of environment space and the system default is usually a measly
256 bytes. Adding this "sub-shell" invocation is a common way
of grabbing more environment space for programs that need it without
relying on the user changing their CONFIG.SYS or having to get the
sub-shell manually.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ECHO OFF
%.%
SET .=GOTO:K0
%COMSPEC% /E:4096 /C %0 %1 %2
SET .=
GOTO OUT
:K0
SET T= %=%
SET T=%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%%T%
SET N1=%1
SET N2=%2
FOR %%_ IN (C R O1 O2) DO SET %%_=
:L0
%T%SET D1=%N1%
FOR %%%D1% IN (() DO SET N1=%%%N1%
FOR %%_ IN %N1%) DO SET N1=%%_
IF %D1%==( SET O1=%O1%0
IF NOT %D1%==( SET O1=%D1%%O1%
%T%SET D2=%N2%
FOR %%%D2% IN (() DO SET N2=%%%N2%
FOR %%_ IN %N2%) DO SET N2=%%_
IF %D2%==( SET O2=%O2%0
IF NOT %D2%==( SET O2=%D2%%O2%
IF NOT %D1%%D2%==(( GOTO L0
:L1
%T% SET D=%O1%
FOR %%%D% IN (() DO SET O1=%%%O1%
FOR %%_ IN %O1%) DO SET O1=%%_
SET RET=GOTO:K2
GOTO DBANG
:K2
%T% SET D=%O2%
FOR %%%D% IN (() DO SET O2=%%%O2%
FOR %%_ IN %O2%) DO SET O2=%%_
SET RET=GOTO:L2
:DBANG
FOR %%_ IN (8 9) DO IF %D%==%%_ SET C=%C%!!!!!!!!
SET D=IF NOT %D%
%D%==0 %D%==1 %D%==2 %D%==3 %D%==8 %D%==9 SET C=%C%!!!!
%D%==0 %D%==1 %D%==4 %D%==5 %D%==8 %D%==9 SET C=%C%!!
%D%==0 %D%==2 %D%==4 %D%==6 %D%==8 SET C=%C%!
SET _=
SET D=
%RET%
:L3
SET _=!
SET D=!!!!!!!!!!
:L2
IF !%C%==%D%! SET D=0
IF !%C%==%D%!! SET D=1
IF !%C%==%D%!!! SET D=2
IF !%C%==%D%!!!! SET D=3
IF !%C%==%D%!!!!! SET D=4
IF !%C%==%D%!!!!!! SET D=5
IF !%C%==%D%!!!!!!! SET D=6
IF !%C%==%D%!!!!!!!! SET D=7
IF !%C%==%D%!!!!!!!!! SET D=8
IF !%C%==%D%!!!!!!!!!! SET D=9
IF !%D%==! GOTO L3
SET C=%_%
IF %O1%%D%==(0 SET D=
SET R=%D%%R%
IF NOT %O1%==( GOTO L1
ECHO [RESULT] %R%
:OUT
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~