We help IT Professionals succeed at work.

Batch File [challenging and advanced]: Math to resolve "SET /a" 32bits limit

1,034 Views
Last Modified: 2012-02-21
Hi there,

I scripted the following batch file.

It's purpose, is to make space for the new files to be archived to the archive drive.

I do this by comparing the working folder with it's corresponding folder in the mirror drive.

I have two problems here:
-Primero, I just came back from a very cool vacation and I'm brain challanged.
-Secundo, My archive drive is 2TB and the math involved is greater than 2147483647 bytes (32bites).

I need to make this script work considering the DOS 32bits limit.

Please remember. I'm currently brain challanged. So please do not provide ideas, but rather help me by completing my script since I need this kind of pronto.

Thanks for your help,
Rene

@ECHO OFF

SETLOCAL enabledelayedexpansion
MODE CON: COLS=150 LINES=60

REM SETTING VARIABLES
ECHO ^>SETTING VARIABLES
    SET BaseFolder=DOCS\USERSDATA
    SET BaseSource=M:
    SET BaseMirror=N:\M
    SET ArchiveDrive=O:

REM READING FILES TO BE ARCHIVED BY COMPARING THE WORKING FOLDER WITH THE MIRROR FOLDER
    SET TotalFileSize=0
    FOR /F "tokens=2 delims=	" %%A IN ('Robocopy "%BaseMirror%\%BaseFolder%" "%BaseSource%\%BaseFolder%" *.* /MIR /L /NJH /NJS /NS /NDL /XF "*RECYCLER*" "desktop.ini" "Thumbs.db" ".~*.*" "*.*#" "~*.*" "*.swp" "*.dmp" "*.tmp" "pagefile.sys" "hiberfil.sys" /XD "RECYCLER" ^| Findstr -V ^*') DO (
        SET /a TotalFileSize=!TotalFileSize! + %%~zA
    )

REM Reading free space available on archive drive
    FOR /F %%A in ('wmic logicaldisk where "DeviceID='%ArchiveDrive%'" get FreeSpace ^| FINDSTR /R [0123456789]') DO SET FreeSpace=%%A

REM Deleting old archive files up to the required space needed for the new archives
    PUSHD "%ArchiveDrive%"
    IF %FreeSpace% LEQ %TotalFileSize% (
        FOR /F "delims=" %%B IN ('DIR /s /b /od /a-d *.* ^| FINDSTR /i "%ArchiveDrive%"') DO (
            DEL /f /q "%%~fB"
            SET /a FreeSpace=!FreeSpace! + %%~zB
            ECHO !FreeSpace! %TotalFileSize% "%%~fB"
    ) ELSE (
        ECHO There is sufficient storage space available to receive the new archive files.
        ECHO Deleting old archives is not required
    )

ECHO.
PAUSE
EXIT
    
:CleanArchiveDrive
IF %FreeSpace% LEQ %TotalFileSize% EXIT /b
ECHO.
ECHO DONE
ECHO.
PAUSE
EXIT

Open in new window

Comment
Watch Question

This one is on us!
(Get your first solution completely free - no credit card required)
UNLOCK SOLUTION
Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
This is an interesting question for me, but I am flat out right now so likely won't get back to it in the time frame you need Rene.

FWIW though, BAT really is the wrong tool for any number crunching, you should think about VBS or PS1.  In addition there are a number of little utilities some of which I use, that can do this type of math at the command line and return the result in a variable or to the screen.  Leveraging one of those would simplify greatly.

Lastly, you are also going to have to change the IF statements where the possibility of large numbers exists.  The way I have sometimes done this is to pad each number on the right to a certain number of digits, maybe 20 or 30, and then do a string compare rather than a numeric compare.

~bp
Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
@Rene & @Paul,

Here's an example of a routine to add large numbers up to about 17 or 18 digits.  I think it might be a little more efficient than Paul's approach as it doesn't do the math out fully longhand, but rather works with two 9 digit pieces of the larger number if needed.  And if not needed then it does normal SET /A math.  I didn't do any benchmarks, but it should be a little faster.

I'll work up a :LongComp routine later today, timing out right now...
@echo off
setlocal EnableDelayedExpansion

call :LongAdd r 111 222
echo %r%
call :LongAdd r 999999999999 999999999999
echo %r%
goto :EOF

:LongAdd [result] [value1] [value2]
  if %~2 LEQ 999999999 if %~3 LEQ 999999999 (
    set /A %~1=%~2+%~3
    goto :EOF
  )
  setlocal
  set _Value1=000000000000000000%~2
  set _Value2=000000000000000000%~3
  set /A _Value1Left=1%_Value1:~-18,9%-1000000000
  set /A _Value2Left=1%_Value2:~-18,9%-1000000000
  set /A _Value1Right=1%_Value1:~-9%-1000000000
  set /A _Value2Right=1%_Value2:~-9%-1000000000
  set /A _LeftSum=%_Value1Left%+%_Value2Left%
  set /A _RightSum=%_Value1Right%+%_Value2Right%
  set /A _Carry=%_RightSum%/1000000000
  set /A _LeftSum=%_Value1Left%+%_Value2Left%+%_Carry%
  if %_LeftSum% GTR 0 (
    set _Result=000000000%_RightSum%
    set _Result=%_LeftSum%!_Result:~-9!
  ) else (
    set _Result=%_RightSum%
  )
  endlocal & set %~1=%_Result%
  goto :EOF

Open in new window

~bp

Author

Commented:
Thanks guys! Both of your versions works.

My last challange her is to make the following work:
IF %FreeSpace% LEQ %TotalFileSize% EXIT /b

Thanks and cheers,
Rene
Test your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016
Commented:
This one is on us!
(Get your first solution completely free - no credit card required)
UNLOCK SOLUTION

Author

Commented:
@Bill:
Considering  set "_Value1=%_Value1:~-18%", the IF command seems to also have the 32bits limit:

if 2147483647 LSS 2147483648 echo ok
if 2147483646 LSS 2147483647 echo ok

Thanks and cheers,
Rene
Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
The IF statement is fine in LongComp, since I am doing string compares rather than numeroc compares.

~bp

Author

Commented:
@Bill:
I did not know we could GTR LSS GEQ... string compare.
Very nice.

Author

Commented:
Thanks Bill and Paul for your good work.

You both saved me a lot of time :)

Point split:

Paul:
Working math script:150

Bill:
Working math Script:150
Working compare script:100
Completing my script:100


Thanks a million and Cheers,
Rene

Author

Commented:
@Bill
Oups... I just found a bug.

Try this:
IF "22" GTR "1999999999999" ECHO ok

Open in new window

Author

Commented:
@Bill
Sorry for the "Oups..."

There is no bug.

Now I know why you padded the numbers on the left with "0" before comparing!

Cheers,
Rene
Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
:-)
Thank you ReneGe

I didn't add any more as bill was at hand to help out... :)

Author

Commented:
@Paul:

I know :)

Also, I found your approach very interresting and educational.

Thanks pal!
Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
This exercise took me back to my assembler days, working with carry registers and the like.  Neat stuff...

~bp

Author

Commented:
I've done assembler arround 30 years ago with Z80, Motorola 6809, 68HC705 and familly. I don't remember much of it but I remember it was fun!!

Cheers
On the subject of 'eductational'... I realised a few problems with my code above (http:#37566187) such as:

(1) What happens to the final 'carry' if there is one? (See line 51 below).

(2) There is no need to reset the carry in line 32 (above). (See lines 42~45 below).

(3) The choice of name for variables are misleading (the remnants of an edited previous version).

(4) Correct localisation of variables and parameter passing.

For those reasons, I have edited the code and made the necessary amendments. Here's the whole thing with a test program which CALLs the :ADD function.
@echo off

set /a augend=%1
set /a addend=%2

call :add sum %augend% %addend%

echo [%sum%]
exit /b


:: -------------------------------------------------------------------------
:: ADD
:: -------------------------------------------------------------------------
:add
  setlocal

  set /a augend=%~2
  set /a addend=%~3

  set "sum="
  set "carry="

  :loop
    set "digit="

    if defined carry (
      set /a digit=1
      set "carry="
    )

    if defined augend (
      set /a digit+=%augend:~-1,1%
      set "augend=%augend:~0,-1%"
    )

    if defined addend (
      set /a digit+=%addend:~-1,1%
      set "addend=%addend:~0,-1%"
    )

    if %digit% gtr 9 (
      set /a carry=1
      set "digit=%digit:~-1,1%"
    )

    set "sum=%digit%%sum%"
  if defined augend goto loop
  if defined addend goto loop

  if defined carry set "sum=1%sum%"
  
  endlocal & set /a %1=%sum%
goto :eof

Open in new window

Author

Commented:
Inspired by the work of both of you, here is my current version.

All I have left to do, is to incorporate it in my archive script.

Thanks again and cheers,
Rene

@ECHO OFF

SETLOCAL EnableDelayedExpansion

SET Val1=1
SET Val2=9999999999999999999999999999999999999999999999

REM ADDING THE NUMBERS
   CALL :Add Sum Val1 Val2

REM COMPARING THE NUMBERS
   CALL :NbCompare NbCompare Val1 Val2


ECHO %Sum% [%NbCompare%]

ECHO.
PAUSE
EXIT


:Add
SET Add_Carry=0
SET Add_Digit=0
SET Add_Nb1=!%2!
SET Add_Nb2=!%3!

REM DEFINING THE NUMBERS LENGTH
   CALL :NbLength %2
   CALL :NbLength %3
   
   :Add1
   SET /a Add_Digit+=1

   REM IF DIGIT POSITION IS GREATER THAN BOTH NUMBERS LENGTH
   IF %Add_Digit% GTR !NbLength_%2! IF %Add_Digit% GTR !NbLength_%3! (
      IF %Add_Carry% == 1 SET %1=1!%1!
      EXIT /b
   )

   REM DEFINING DIGITS VALUE AT CURRENT POSITION
      IF %Add_Digit% GTR !NbLength_%2! (SET Add_Nb_%2=0) ELSE (SET Add_Nb_%2=!Add_Nb1:~-%Add_Digit%,1!)
      IF %Add_Digit% GTR !NbLength_%3! (SET Add_Nb_%3=0) ELSE (SET Add_Nb_%3=!Add_Nb2:~-%Add_Digit%,1!)
   
   REM SUMMING BOTH DIGITS + CARRY
      SET /A Add_Digit_Sum=!Add_Nb_%2! + !Add_Nb_%3! + %Add_Carry%
   
   REM IF SUM IS GREATER THAN 9
      IF %Add_Digit_Sum% GEQ 10 (
         SET Add_Digit_Sum=%Add_Digit_Sum:~-1,1%
         SET Add_Carry=1
      ) ELSE (
         SET Add_Carry=0
      )

   SET %1=%Add_Digit_Sum%!%1!
   GOTO Add1


:NbLength
SET NbLength_Digit=0
IF "%NbLength_Max%" == "" SET NbLength_Max=0

   :NbLength1
   SET NbLength_Number=!%1!
   
   IF "!NbLength_Number:~0,%NbLength_Digit%!" == "!%1!" (
      SET NbLength_%1=%NbLength_Digit%
      
      REM DEFINING MAX LENGTH NUMBER
         IF %NbLength_Max% LEQ %NbLength_Digit% SET NbLength_Max=%NbLength_Digit%
      
      EXIT /b
   )

SET /a NbLength_Digit+=1
GOTO :NbLength1


:NbCompare
REM DEFINING THE NUMBERS LENGTH
   CALL :NbLength %2
   CALL :NbLength %3

REM DEFINING 0 PADDINGS
   SET /a NbCompare_PadNb1=%NbLength_Max% - !NbLength_%2!
   SET /a NbCompare_PadNb2=%NbLength_Max% - !NbLength_%3!
   FOR /L %%A in (1,1,%NbCompare_PadNb1%) DO SET NbCompare_Pad1=0!NbCompare_Pad1!
   FOR /L %%A in (1,1,%NbCompare_PadNb2%) DO SET NbCompare_Pad2=0!NbCompare_Pad2!

REM COMPARING NUMBERS
   IF "%NbCompare_Pad1%!%2!" GTR "%NbCompare_Pad2%!%3!" SET %1=1
   IF "%NbCompare_Pad1%!%2!" EQU "%NbCompare_Pad2%!%3!" SET %1=0
   IF "%NbCompare_Pad1%!%2!" LSS "%NbCompare_Pad2%!%3!" SET %1=-1
EXIT /b

Open in new window

Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
Well, at the risk of rocking the boat, I still think that's the wrong approach for this problem.  Keep in mind that my AddLong routine worked with 18 digit numbers, but optimized by only have to do 2 nine digit number calculations to generate a sum.  That's compared to 18 calculations using the single digit approach you have used.

Also, bear in mind that you are working with disk space numbers.  So lets look at the order of magnitude of an 18 digit number of bytes of disk space.
Kilobyte  = 1000
Megabyte  = 1000000 bytes
Gigabyte  = 1000000000 bytes
Terabyte  = 1000000000000 bytes
Petabyte  = 1000000000000000 bytes
Exabyte   = 1000000000000000000 bytes
Zettabyte = 1000000000000000000000 bytes

Open in new window

(I recognize that these are in reality multiples of 1024 not 1000, but this gives us the rough idea).  So, "limiting" our scale to 18 digit numbers puts us just below Exabyte size, which is 1000000000 gigabytes.  I'm feeling like that's probably good enough for disk space scales that you are likely to be dealing with.

As I type this I realize that I probably should have allowed for a 19 digit result, but not sure it makes a lot of difference in your application.

That being said, if you are intent on the slower single digit approach, then you could handle the calcs on each digit in a shorter manner in my opinion.

So replace:
   REM IF SUM IS GREATER THAN 9
      IF %Add_Digit_Sum% GEQ 10 (
         SET Add_Digit_Sum=%Add_Digit_Sum:~-1,1%
         SET Add_Carry=1
      ) ELSE (
         SET Add_Carry=0
      )

Open in new window

with:
      SET /A Add_Carry=%Add_Digit_Sum% / 10
      SET /A Add_Digit_Sum=%Add_Digit_Sum% %% 10

Open in new window

~bp

Author

Commented:
@bill

I just enjoyed doing it this way, but I aggree with you all the way.

I will be passing through a lot of files so I may have to get back to your method if it takes to long.

Finally... I get %%

set /A a=123456789 %% 100
will output
89

:)

Author

Commented:
@Bill
Well thinking about it, i'm still puzzled about %%
I now know what it does but I wonder if I could wonder why.
Is it something that can be explained? If yes, would you mind explaining it to me?

Thanks and cheers,
Rene
Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
% is the modulo division function in the SET /A.   It basically divides the first number by the second, and then only returns the remainder.  Pretty straight forward, but for some tasks it can be very useful.  Keep in mind if you think about:

A = B % C

then A will always have a value between 0 and C-1.

http://en.wikipedia.org/wiki/Modular_arithmetic

http://rutherglen.science.mq.edu.au/math106s206/ex/moduloarithmetic.pdf

Let me know if you have additional questions.

~bp
(just to say i'm in the throw of replying however, on testing my code, i hit a snag... will report back very soon)

Author

Commented:
@Paul:
Only if you feel like it, but son't worrry. I'm good. What you already accomplished help me a lot.

Thanks again and cheers,
Rene

Author

Commented:
@Bill
I'll have fun reading about "Modular Arithmetics". Thanks for the links.

Cheers,
Rene
The modulus of a number is old hat. Even as far back as BASIC there was the MOD function ie:
10 LET a = 5
20 LET b = 2
30 LET $c = $a MOD $b.
40 PRINT "Result: " + $a + " MOD " + $b + " is " + $c

Open in new window

Result:   5 MOD  2 is  1

(Hahaha! I don't know if you noticed I included an extra space infront of the integers... That's because BASIC also added a leading space unless the number was preceded by it's sign - typically the minus sign)

Basically, it's the same as a remainder after a division.

DIV was the other function i.e., 6 DIV 2 equals 3, and so does 7 DIV 2. With DIV, the remainder is ignored which is why MOD was almost always used before doing a DIV.

(Hey! I typed all that in from memory - even the code.... Blimey! Those were the days!)
(I started writing this early this morning. Due to some very fishy goings on and all day spent analysing some strange behaviour, I have not been able to finish it, but here's what I had up until then)

ReneGe

I agree with billprew, you're going about it the wrong way, but not for all the reasons bill says.

Firstly, bill is right to point out the different magnitudes from kilobyte to Zettabyte. In fact, it seems to put into perspective how just 12 digits meets your needs. This is based on the unlikelyhood of any system being equiped with drives larger than a few terabytes let alone a 1000 of them or more.

As bill points out, in your particular application, splitting a number across two smaller numbers certainly does have it's advantages however, it's tricky and it requires complex string handling as indicated in his code. What I mean by that is it's not immediately apparent what's going on in his program and attempting to modify it requires a very thorough understanding.

On the otherhand, my code in http:#37575700 is almost perfect and will handle numbers far larger than a zetabyte. If I had to make one change to that program, it would be line 25 ie, set "digit=0" instead of set "digit=" - which seems to make better sense especially when you get down to line 33.

The program doesn't rely on any padding, leading zeros or indeed a need to know the lengths of either the augend or the addend. By the way, 'augend' and 'addend' are the proper mathematical terms describing the first number and the second number in addtion while 'sum' is the result. See http://www.factmonster.com/ipka/A0881931.html.

The reason why my code does not rely on paddnig nor needing to know the lengths of either the augend or the addend is because processing is carried out from right-to-left - which is how we are used to doing it anyway. It's up to line 48 and line 49 to continue 'building' the next digit (to the left) until there are no more digits to process in either the augend or the addend. It's simple by design. By the way, line 48 and line 49 could just as well have been combined as follows:
if defined augend (
  goto loop
) else (
  if defined addend goto loop
)

Open in new window

The other thing I noticed about your program is the lack of localisation.

You must realise that although I declare both 'augend'and 'addend' in both the main program and the function :Add, they are in fact separate variables. Changing the value of augend in the function :Add has no direct effect on the augend in the main program - augend in the function :Add is local and therefore, only visible to that function.

You may have wondered why I chose to mix the parameter types in my CALL :Add statement on line 6, ie:

    call :add sum %augend% %addend%

This was deliberate for the following two reasons:

(1) The value in sum is directly addressed by the function :Add so it made sense to pass this parameter by name and,

(2) Because both augend and addend are read-only, there was no need to pass them by name. By passing values, it gives rise to the following possibility:

    call :add sum %1 %2

    call :add sum !sum! %%A

(Think about it....)

The other thing my program does is process just the right-most digit of both augend and addend in the same iteration of the loop so I don't need to keep track of one or the other also, I don't need to know their lengths nor have to use padded zeros because all of that logic is handled by the simple IF DEFINED mechanism. It's simple and works well.

I was thrown aback when I noticed how much longer your code is than mine. And like bill's, there are parts that are just too complex and require a deeper knowledge of what's going on.

I tried to keep my code simple and easy to read. If you were to use just the :Add function in your own program I think it would meet your needs without any alteration. This is how I would do it:
@echo off
setlocal enabledelayedexpansion
    
set bytes=0

for /r c:\ %%a in (*.txt) do (
  call :add bytes !bytes! %%~za
)

echo Total bytes is: %bytes%

Open in new window

(Okay... I've had no end of problems with the function :Add. Every so often, it would pick up a negative value and fail to perform a 'carry'. This is really odd behaviour and I am still trying to identify the cause)
Here's the whole thing in a nutshell.

(There were some edits and slight mods, the program was tested on over 3000 mp3 files scattered across a hard drive. It works flawlessly)
:: =========================================================================
:: Demonstrate use of :ADD
:: Recursively search for MP3 files & return total size
::
:: Change Line 12: start folder & filespec
:: Can remove Line 14
:: =========================================================================
@echo off
  setlocal enabledelayedexpansion

  set total=0

  for /r c:\ %%a in (*.mp3) do (
    call :add total %%~za
    title [!total!][%%a]
  )

  echo %total% bytes
exit /b


:: -------------------------------------------------------------------------
:: ADD
:: -------------------------------------------------------------------------
:add
  setlocal

  set "sum="
  set augend=!%1!
  set addend=%2

  :process_digit
    set digit=%carry%
    set carry=0

    if defined augend (
      set /a digit+=%augend:~-1,1%
      set augend=%augend:~0,-1%
    )

    if defined addend (
      set /a digit+=%addend:~-1,1%
      set addend=%addend:~0,-1%
    )

    if %digit% gtr 9 (
      set carry=1
      set digit=%digit:~-1,1%
    )

    set sum=%digit%%sum%

  if defined augend goto process_digit
  if defined addend goto process_digit

  if %carry% equ 1 set sum=1%sum%
  endlocal & set %1=%sum%
goto :eof

Open in new window

Will also work with the following mod:

    Line 16:     call :add total !total! %%~za

    Line 31:     set augend=%2
    Line 32:     set addend=%3

(And that's my final take on this issue. It's been interesting!)
Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
@Paul,

This seems a little risky to me.

    set digit=%carry%

On entry into the sub you are assuming that the carry variable does not already exist in the environment, which while typically true, is not guaranteed.  If the current script, or global environment contained a pre-existing carry variable I suspect your routine could be affected.

Probably want to set that to zero outside of the loop, on entry to the sub.

~bp
Blimey! Accidentally edited it out while removing the debugging code. What a clanger.

Thanks for pointing it out. As you rightly say, it's crucial to reset the carry on entry otherwise there's a risk of ending up with a wrong result.

Best to slip it in at line 29.
:: =========================================================================
:: Demonstrate use of :ADD
:: Recursively search for MP3 files & return total size
::
:: Change Line 12: start folder & filespec
:: Can remove Line 14
:: =========================================================================
@echo off
  setlocal enabledelayedexpansion

  set total=0

  for /r f:\music\ %%a in (*.mp3) do (
    call :add total !total! %%~za
    title [!total!][%%a]
  )

  echo %total% bytes
exit /b


:: -------------------------------------------------------------------------
:: ADD
:: -------------------------------------------------------------------------
:add
  setlocal

  set "sum="
  set carry=0
  set augend=%2
  set addend=%3

  :process_digit
    set digit=%carry%
    set carry=0

    if defined augend (
      set /a digit+=%augend:~-1,1%
      set augend=%augend:~0,-1%
    )

    if defined addend (
      set /a digit+=%addend:~-1,1%
      set addend=%addend:~0,-1%
    )

    if %digit% gtr 9 (
      set carry=1
      set /a digit%%=10
    )

    set sum=%digit%%sum%

  if defined augend goto process_digit
  if defined addend goto process_digit

  if %carry% equ 1 set sum=1%sum%

  endlocal & set %1=%sum%
goto :eof

Open in new window

Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
So, I said to myself, "self, what would Paul do in this situation?".  Ah, of course, he'd run some tests.  So I did.  I prepared a small test BAT file that included my last approach to this, as well as Paul's latest approach, and put them through several test scenarios of 100 to 1000 iterations each, recording the elapsed time, and the result value.  

The easy part of this was the result value, both approaches yielded the same results for each scenario, so that was a good thing.  However, as you can see from the data below, my approach, while my code may have been characterized as "... are parts that are just too complex and require a deeper knowledge of what's going on.", I hope this demonstrates why I took the approach I did.  Both approaches are valid and have their pros and cons, but when I have a routine that will be executed many times for a basic capability I often will trade off performance for readability.

(all time in seconds)Benchmark results
@echo off
setlocal EnableDelayedExpansion

echo 1,1,100
timer /nologo
set total=1
for /L %%A in (1,1,101) do call :Add total !total! %%A
timer /nologo /s
echo %total%

timer /nologo
set total=1
for /L %%A in (1,1,101) do call :LongAdd total !total! %%A
timer /nologo /s
echo %total%

echo 1,1,1000
timer /nologo
set total=1
for /L %%A in (1,1,1001) do call :Add total !total! %%A
timer /nologo /s
echo %total%

timer /nologo
set total=1
for /L %%A in (1,1,1001) do call :LongAdd total !total! %%A
timer /nologo /s
echo %total%

echo 1000,1000,1000
timer /nologo
set total=1000
for /L %%A in (1000,1,2000) do call :Add total !total! %%A
timer /nologo /s
echo %total%

timer /nologo
set total=1000
for /L %%A in (1000,1,2000) do call :LongAdd total !total! %%A
timer /nologo /s
echo %total%

echo 1000000000000,1000,1000
timer /nologo
set total=1000000000000
for /L %%A in (1000,1,2000) do call :Add total !total! %%A
timer /nologo /s
echo %total%

timer /nologo
set total=1000000000000
for /L %%A in (1000,1,2000) do call :LongAdd total !total! %%A
timer /nologo /s
echo %total%

echo 999999999999999,999999999,1000
timer /nologo
set total=999999999999999
for /L %%A in (999999999,1,1000000999) do call :Add total !total! %%A
timer /nologo /s
echo %total%

timer /nologo
set total=999999999999999
for /L %%A in (999999999,1,1000000999) do call :LongAdd total !total! %%A
timer /nologo /s
echo %total%

goto :EOF

:Add
  setlocal

  set "sum="
  set carry=0
  set augend=%2
  set addend=%3

  :process_digit
    set digit=%carry%
    set carry=0

    if defined augend (
      set /a digit+=%augend:~-1,1%
      set augend=%augend:~0,-1%
    )

    if defined addend (
      set /a digit+=%addend:~-1,1%
      set addend=%addend:~0,-1%
    )

    if %digit% gtr 9 (
      set carry=1
      set /a digit%%=10
    )

    set sum=%digit%%sum%

  if defined augend goto process_digit
  if defined addend goto process_digit

  if %carry% equ 1 set sum=1%sum%

  endlocal & set %1=%sum%
goto :eof

:LongAdd [result] [value1] [value2]
  setlocal
  set "_Value1=000000000000000000%~2"
  set "_Value2=000000000000000000%~3"
  set /A "_Value1Left=1%_Value1:~-18,9%-1000000000"
  set /A "_Value2Left=1%_Value2:~-18,9%-1000000000"
  set /A "_Value1Right=1%_Value1:~-9%-1000000000"
  set /A "_Value2Right=1%_Value2:~-9%-1000000000"
  set /A "_LeftSum=%_Value1Left%+%_Value2Left%"
  set /A "_RightSum=%_Value1Right%+%_Value2Right%"
  set /A "_Carry=%_RightSum%/1000000000"
  set /A "_LeftSum=%_Value1Left%+%_Value2Left%+%_Carry%"
  if %_LeftSum% GTR 0 (
    set "_Result=000000000%_RightSum%"
    set "_Result=%_LeftSum%!_Result:~-9!"
  ) else (
    set "_Result=%_RightSum%"
  )
  endlocal & set "%~1=%_Result%"
  goto :EOF

Open in new window

~bp
billprew

Funnily enough, I had already conducted my own performance tests.

The results (which I omitted in my previous posts) are as follows:

    billprew's code:       Average = 15.71 seconds
    paultomasi's code: Average = 61.14 seconds

(Tests taken reading filesizes of 3,257 MP3 files in 329 nested folders totalling 14,123,484,508 bytes)
 
The reason why I have held off on these results is because I have looked at your method and given some thought to rewriting it using dynamic lengths rather than predefined fixed lengths.

If I succeed in completing this then I will post back.

My method above is of academic interest. Your method is far more efficient. Your method is limited to 18 digits whereas mine has a far greater (although untested) range. One thing's for sure though, my method gets progressively slower to execute.

If I had to choose either method as part of a working function, I would undoubtedly choose yours however, I feel there is room for improvement.

BTW, I like your graphics... complete with drop-shadow. Nice one. Lol.
Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
@Paul,

Thanks for the conversation, quite enjoyable.

I was actually pondering this over the weekend too, like you I think there is always room for improvement, but decided to put the time into more pressing tasks on the home front.

But I was thinking, it might be neat (and even useful) to have a "library" of BAT routines related to long integer math, as well as decimal number math.  At least the 4 basic (add, subtract, multiply, divide) and if we wanted to get fancy exponential, modulos, etc.  And some compare routines of course.

It's only a gleam in my eye right now, but optimizing / improving on what we have here would be a first step. I might even work myself up to an article if it proved worthy.

I use a small tool called WinSnap for screen captures, nothing too fancy but I latched on to it years ago and just stay with it. For most of what I need it seems to get the job done.

~bp

Author

Commented:
I have an idea. What if we combine both approaches.
On paul's approach, instead of summing blocks of single digits, we sum blocks of 9 digits.
This way, we will get Bill's speed and Paul's unlimmited summing.

For example: 12345987654321987654321987654321 + 1
12345987654321987654321987654321 + 00000000000000000000000000000001
987654321 + %carry% + 000000001
987654321 + %carry% + 000000000
987654321 + %carry% + 000000000
12345 + %carry% + 00000
bill

I'm all up for contributing towards building a library of batch file routines / functions.

I'm sure among us we already have many useful functions.

Like you, I'm constrained by time and other responsibilities.

I think a good starting place is the :ADD function... Let's stew on it for a few days or so...

BTW, one area I've been experimenting in is passing values back via function names for example:
set a=123
set b=456

call :Sum a b

echo %sum%

Open in new window

Other areas I've been looking at are:

    - Function identifies whether it's an external or embedded function

    - Function identifies whether it was started from the command line or called from another batch file

    - Overloading functions

    - Self-modifying functions

    - Macros

    - Extended ASCII graphics

    - Embedded assembly code functions (mostly 32-bit)

    - Menu generators

    - Custom input routines (extended keys)

    - Colour

    - Batch file text based database

    - Recursion and multidimensional arrays (word searches)

Just to name a few... :)

Author

Commented:
Good idea Bill!!

We never know where a question could lead...

Since you are both a lot better scripters than me, my contribution should be more in providing divergent ideas with some scripting along the way.

@Paul
What do you think about my idea in http:#37589744

Cheers,
Rene
bill
Just wondering if you've done any DOS API stuff...

Also, do you know of a DOS-based character recognition utility program which can return text from an image?

ReneGe
We'd have to be careful with the definition 'unlimited'... There's always a limit.

Author

Commented:
@Paul: Yeaaaa :)
ReneGe

>> "What do you think about my idea in http:#37589744"

The bit I don't like is this:

>> "+ 00000000000000000000000000000001"

but you're thinking in the right direction...

Author

Commented:
@Paul
Oh yes, I just remembered, leading 0 makes the value octal not decimal.
Thanks

Author

Commented:
In the subs, I was making complicated variable names to make sure they would not be duplicated in the batch file. And then I realized you used setlocal/endlocal ;)

Here is my version of Paul's approach summing in chunks of 9 digits.

Thanks for everything. I learned a lot with this question.

Cheers,
Rene

@ECHO OFF
SETLOCAL EnableDelayedExpansion

SET Val1=99
SET Val2=99

Call :add Total %Val1% %Val2%
ECHO %Val1%
ECHO +
ECHO %Val2%
ECHO ---------------------------------------------
ECHO %Total%
PAUSE
EXIT

:Add
SETLOCAL
SET Carry=0
SET Val1=%2
SET Val2=%3

   :Add1
   IF defined Val1 (
      SET digit1=%Val1:~-9%
      SET Val1=%Val1:~0,-9%
   ) ELSE (SET digit1=0)

   IF defined Val2 (
      SET digit2=%Val2:~-9%
      SET Val2=%Val2:~0,-9%
   ) ELSE (SET digit2=0)

   SET /a DigitSum=%digit1% + %digit2% + %Carry%

   SET /a Carry=%DigitSum% / 1000000000
   SET DigitSum=%DigitSum:~-9%

   SET sum=%DigitSum%%sum%

   IF defined Val1 GOTO Add1
   IF defined Val2 GOTO Add1

   ENDLOCAL & SET %1=%sum%
   GOTO :eof

Open in new window

Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
@Rene,

Good first stab.  Run my test timing script cases through it though, when I did one gave the wrong answer, and the last one errored out.  I'm sure you'll track it down.

~bp
Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:

@Paul,

Yes, in a "prior lifetime" I certainly used int 21, if that's what you mean?

I haven't used these OCR utils, but maybe worth taking a look:

http://www.simpleocr.com/
http://code.google.com/p/tesseract-ocr/
http://jocr.sourceforge.net/


~bp

Author

Commented:
@Bill

Paul's approach summing in chunks of 9 digits and yours = Both ran at 78ms

Cool stuff...
Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
@Rene,

Yes, but there are some bugs to be squashed...

~bp

Author

Commented:
I just found a flaw in my latest version.

If one of the two numbers to be added contains a quantity numbers of a multiple of 9 digits, it does not carry %carry%.

999999999
999999999999999999
999999999999999999999999999

SET Val1=999999999
SET Val2=1
TOTAL=000000000
Insted of: 1000000000

Have any ideas?

Cheers,
Rene
Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
As a clue, notice this line in Pauls script:

  if %carry% equ 1 set sum=1%sum%

~bp

Author

Commented:
Correction: It does not carry the last %carry%.

Author

Commented:
@Bill:
You got it right!!

Thanks

Author

Commented:
@Bill:
Was this the bug that needed to be squashed...?
Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
Likely one, not sure both.  Try the tests and see how it goes.

~bp

Author

Commented:
@Bill
Here you go...

I tried to figure out the error messages but... :(

Add-2-big-numbers-BP-Tests.log

@echo off
setlocal EnableDelayedExpansion

SET LogFile=%~dpn0.log
IF EXIST "%LogFile%" DEL "%LogFile%"

echo 1,1,100
echo PAUL
timer /nologo >NUL
set total=1
for /L %%A in (1,1,101) do call :AddPaul total !total! %%A
FOR /F "delims=" %%A in ('timer /nologo /s') DO SET Timer=%%A
ECHO [%total%] [%Timer%]

echo BILL
timer /nologo >NUL
set total=1
for /L %%A in (1,1,101) do call :AddBill total !total! %%A
FOR /F "delims=" %%A in ('timer /nologo /s') DO SET Timer=%%A
ECHO [%total%] [%Timer%]
ECHO -------------------------------

echo 1,1,1000
echo PAUL
timer /nologo >NUL
set total=1
for /L %%A in (1,1,1001) do call :AddPaul total !total! %%A
FOR /F "delims=" %%A in ('timer /nologo /s') DO SET Timer=%%A
ECHO [%total%] [%Timer%]

echo BILL
timer /nologo >NUL
set total=1
for /L %%A in (1,1,1001) do call :AddBill total !total! %%A
FOR /F "delims=" %%A in ('timer /nologo /s') DO SET Timer=%%A
ECHO [%total%] [%Timer%]
ECHO -------------------------------

echo 1000,1000,1000
echo PAUL
timer /nologo >NUL
set total=1000
for /L %%A in (1000,1,2000) do call :AddPaul total !total! %%A
FOR /F "delims=" %%A in ('timer /nologo /s') DO SET Timer=%%A
ECHO [%total%] [%Timer%]

echo BILL
timer /nologo >NUL
set total=1000
for /L %%A in (1000,1,2000) do call :AddBill total !total! %%A
FOR /F "delims=" %%A in ('timer /nologo /s') DO SET Timer=%%A
ECHO [%total%] [%Timer%]
ECHO -------------------------------

echo 1000000000000,1000,1000
echo PAUL
timer /nologo >NUL
set total=1000000000000
for /L %%A in (1000,1,2000) do call :AddPaul total !total! %%A
FOR /F "delims=" %%A in ('timer /nologo /s') DO SET Timer=%%A
ECHO [%total%] [%Timer%]

echo BILL
timer /nologo >NUL
set total=1000000000000
for /L %%A in (1000,1,2000) do call :AddBill total !total! %%A
FOR /F "delims=" %%A in ('timer /nologo /s') DO SET Timer=%%A
ECHO [%total%] [%Timer%]
ECHO -------------------------------

SET TEST=Yes
echo 999999999999999,999999999,1000
echo PAUL
timer /nologo >NUL
set total=999999999999999
for /L %%A in (999999999,1,1000000999) do (
   echo %%A
   call :AddPaul total !total! %%A
)
FOR /F "delims=" %%A in ('timer /nologo /s') DO SET Timer=%%A
ECHO [%total%] [%Timer%]

echo BILL
timer /nologo >NUL
set total=999999999999999
for /L %%A in (999999999,1,1000000999) do call :AddBill total !total! %%A
FOR /F "delims=" %%A in ('timer /nologo /s') DO SET Timer=%%A
ECHO [%total%] [%Timer%]

goto :EOF

:AddPaul [1=Result Var Name] [Val1] [Val2]
SETLOCAL
SET Carry=0
SET Val1=%2
SET Val2=%3
IF "%Test%" == "1" (
   echo VAL1=%Val1%
   echo VAL2=%Val2%
   ECHO.
)

   :Add1
   IF defined Val1 (
      SET digit1=%Val1:~-9%
      SET Val1=%Val1:~0,-9%
   ) ELSE (SET digit1=0)

   IF defined Val2 (
      SET digit2=%Val2:~-9%
      SET Val2=%Val2:~0,-9%
   ) ELSE (SET digit2=0)

   SET /a DigitSum=%digit1% + %digit2% + %Carry%

   SET /a Carry=%DigitSum% / 1000000000
   SET DigitSum=%DigitSum:~-9%

   SET sum=%DigitSum%%sum%

   IF defined Val1 GOTO Add1
   IF defined Val2 GOTO Add1
   
   if %carry% equ 1 set sum=1%sum%

   ENDLOCAL & SET %1=%sum%
   GOTO :eof
   

:AddBill [result] [value1] [value2]
  setlocal
  set "_Value1=000000000000000000%~2"
  set "_Value2=000000000000000000%~3"
  set /A "_Value1Left=1%_Value1:~-18,9%-1000000000"
  set /A "_Value2Left=1%_Value2:~-18,9%-1000000000"
  set /A "_Value1Right=1%_Value1:~-9%-1000000000"
  set /A "_Value2Right=1%_Value2:~-9%-1000000000"
  set /A "_LeftSum=%_Value1Left%+%_Value2Left%"
  set /A "_RightSum=%_Value1Right%+%_Value2Right%"
  set /A "_Carry=%_RightSum%/1000000000"
  set /A "_LeftSum=%_Value1Left%+%_Value2Left%+%_Carry%"
  if %_LeftSum% GTR 0 (
    set "_Result=000000000%_RightSum%"
    set "_Result=%_LeftSum%!_Result:~-9!"
  ) else (
    set "_Result=%_RightSum%"
  )
  endlocal & set "%~1=%_Result%"
  goto :EOF

Open in new window

Author

Commented:
@Bill
I got one.

SET ValX=1000000008
SET digitX=%ValX:~-9%
digitX = 000000008 = Octal
Problem1:8 and 9 octal does not exist
Problem2:OCTAL summation could not result in the desired DEC results.

Now, I must find a way to fix this.

Have any ideas?
Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
@Rene,

This is probably only a problem in the SET /A statements, that try to treat that value as a number.  For example:

set a=08
echo %a%

is fine, but:

set /A b=%a%+1

will throw the error.

The only way that can be worked around I believe is to remove leading zeros from the STRING before the SET /A operation.  So we would likely need a "remove leading zeros" routine.

~bp
Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
@Rene,

Here's a possible approach to the leading zero removal.
@echo off
set "Z=000001234500000"
call :LZtrim %Z%
echo [%LZtrim%]
exit /b

REM Remove leading zeros from the left side of a string
:LZtrim
  set LZtrim=%*
:LZtrim2
  if "%LZtrim:~0,1%" NEQ "0" (
    if "%LZtrim%" EQU "" set LZtrim=0
    goto :EOF
  )
  set LZtrim=%LZtrim:~1%
  goto :LZtrim2

Open in new window

~bp

~bp

Author

Commented:
Thanks Bill :)

Author

Commented:
Guys, inspired by Paul's approach, I did this sub that displays numbers in an easy to read format.

I just felt like sharing :)

Cheers,
Rene

@ECHO OFF
SETLOCAL EnableDelayedExpansion

FOR /L %%A in (1,1,3) DO FOR /L %%B in (1,1,9) DO (
   SET Val=%%B!Val!
   CALL :DisplayNumber Number !Val!
   ECHO !Number!
)

PAUSE
EXIT

:DisplayNumber [1=Result Var Name] [Val] 
SETLOCAL
SET Nb=%2
SET UnitNb=0
SET Unit=[UN-DEFINED UNIT]
   :DisplayNumber1
   IF defined Nb SET digit=%Nb:~-3%
   SET /a UnitNb+=1
   SET Result2=!Result1!
   SET Result1=!digit!

   IF DEFINED Result2 (SET Result=%Result1%.%Result2%) ELSE (SET Result=%Result1%)
   SET Nb=%Nb:~0,-3%
   
   IF DEFINED Nb GOTO DisplayNumber1

   FOR /F "tokens=%UnitNb% delims=," %%A IN ("B,KB,MB,GB,TB,PB,EB,ZB") DO SET Unit=%%A
   ENDLOCAL & SET %1=%Result% %Unit%
   GOTO :eof

Open in new window

Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
@Rene,

Hey, that could be useful!

A couple of thoughts if you wanted to turn it up a notch.

(1)  Add a parm that allows the caller to specify the desired units.  So for example if they pass "MB" then the result is always in terms of MB, even if it means things like:

0.000123 MB
1,234.567 MB


(3) Add an option for inserting commas, or not on the left of the decimal point.

(4) Add an option for specifying the number of decimal places to display.  And of course, to make it more useful, and interesting, the number should be rounded to this many positions, not just truncated.

These new options should be optional, either as separate parms, or one parm with commas in it or something separating the sub options.

~bp

Author

Commented:
@Bill:
These are good ideas.
I'll create new questions so that your suggestions are team built.

Cheers,
Rene

Author

Commented:
@Bill:
Could you please explain: SET LZtrim=%*

Thanks,
Rene
Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
SET LZtrim=%*

will assign to the LZtrim variable all the parms passed on the call to the subroutine.  Truth be told, that really should probably just be

SET LZtrim=%~1

in this case, but I borrowed it from my space trim routine, which needed that.  Here's what it looks like:
@echo off
set "S=   123 456   "
call :Trim %S%
echo [%Trim%]

exit /b

:Trim
  set Trim=%*
  exit /b

Open in new window

~bp

Author

Commented:
Thanks
Info.

PC graphics card died since I last commented. Using a temporary PC at the mo, and is my first revisit to EE.

Nice to see this discussion had progressed with some good suggestions.

Author

Commented:
@Paul

Nice to hear from you. Seems like CPR is overdue here ;)

See you soon pal!

Cheers,
Rene
Bill PrewTest your restores, not your backups...
CERTIFIED EXPERT
Expert of the Year 2019
Top Expert 2016

Commented:
Unlock the solution to this question.
Join our community and discover your potential

Experts Exchange is the only place where you can interact directly with leading experts in the technology field. Become a member today and access the collective knowledge of thousands of technology experts.

*This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

OR

Please enter a first name

Please enter a last name

8+ characters (letters, numbers, and a symbol)

By clicking, you agree to the Terms of Use and Privacy Policy.