Nov 2, 2013

Function: ShowPercentageOfProgress

Back in January I wrote the prototype of percentage calculation routine in batch. It was an extremely sluggish proof of concept. Well, as I returned to reviewing it, I noticed I would change a lot. In the meantime, I have found another method to detect integer overflow, what allowed me to greatly improve performance of this routine. I also changed progress bar drawing routine, in the older one it was called as many times as did the whole percentage calculation procedure. The enhanced one carries out at most 100 calls. Furthermore, the newer one supports one-percent changes the way the progress bar does not need complete redrawing, only 1 byte gets changed. In the older one, you used to need to be cautious with denominator when initializing the function:
1/10 -> 10%, 10 kept as the denominator until it arrives to 10/10
2/10 -> 20%
3/10 -> 30%
301/1001 -> is: 3010% (error, because 10 is still being kept as a denominator), should be: 30,0699(300699)
Newer version abolishes the above behavior, that is, denominator is not kept for further calculations, it is received every time the function is called.

The function can receive any unsigned integer numbers within the range of 0 to 1e+7, whilst the denominator cannot be 0. That means, percents can "go back" as you change values "on the fly", it could be 17% first, and 15% after you increase the denominator during the execution. The picture below presents the same execution flow.

The progress bar is started drawing from 1%.
I worked out two variants of the improved function. The first one shows only whole percents, and the other one - more exact percents with mantissa. Rounding up is not carried out anymore.

With mantissa:

Without mantissa:


I need not add that the one without calculating mantissa is less CPU-expensive, and is faster. As you might have noticed, the "bigger" the fraction is, the shorter the decimal expansion gets to be, to completely disappear at some point.
Both the variants have some kind of demonstration in themselves, just run one of them and type "demo". You can look what does it do below.
The one that calculates mantissa...
@echo off
setlocal EnableDelayedExpansion
prompt $s
echo.
echo Usage:
echo.
echo ShowPercentageOfProgressInWindowTitle ^<stage^> ^<end^> trimtrailingzeros_[yes^|no] updateprogressbar_[yes^|no]
echo.
echo Example: ShowPercentageOfProgressInWindowTitle 7 11 trimtrailingzeros_yes updateprogressbar_no
echo Lack of the updateprogressbar_* parameter defaults to progress bar drawing OFF as well as
echo lack of the trimtrailingzeros_* parameter defaults to trailing zeros trimming OFF.
:loop
echo.
set ch_=%ch%
set ch=&set/p ch= give parameters :
if defined ch_ (for /f "tokens=1,2" %%A in ("%ch_%") do (for /f "tokens=1,2" %%C in ("%ch%") do (if "%%A"=="%%C" (if "%%B"=="%%D" (echo.&echo _PREV_PERCENT == PERCENT -^> to reduce performance expensiveness, progress bar&echo state stays halted until the routine is given with a different fraction)))))
::for /l %%G in (0,17034,9998958) do (call:ShowPercentageOfProgressInWindowTitle %%G 9998958 updateprogressbar_yes)
if /i "%ch%"=="demo" (call:demo) else (call:ShowPercentageOfProgressInWindowTitle %ch%)
if "%errorlevel%"=="1" (echo.&echo Too few parameters.)
if "%errorlevel%"=="2" (echo.&echo Integer overflow, or divisor ^>1e+7, or progress is over 100%%.)
if "%errorlevel%"=="3" (echo.&echo Division by zero detected.)
goto loop
goto:eof
:demo
::counts size of all files in system32 root dir...
for %%A in (%windir%\system32\*.*) do (set/a exact_size+=%%~zA)
::floors bytes
set _exact_size=%exact_size:~0,-3%
:: ...and re-enumerates the directory to gradually add next files' sizes...
for %%A in (%windir%\system32\*.*) do (
set/a var+=%%~zA
:: ...to show the behaviour of the function
call:ShowPercentageOfProgressInWindowTitle !var:~0,-3! %_exact_size% updateprogressbar_yes
)
exit/b0
:UpdateProgressBar
setlocal EnableDelayedExpansion
if not defined _prev_percent (goto upb_)
for /f "tokens=1 delims=," %%A in ("%percent%") do (set p_base=%%A)
for /f "tokens=1 delims=," %%A in ("%_prev_percent%") do (set pp_base=%%A)
set/a diff=%p_base%-%pp_base%
if "%diff%"=="0" (goto upb_endproc)
::if there is a difference of 1 in whole percents between next calls of this function, the string-substitution feature is used just to replace ": " with "::", not to redraw the whole bar
if "%diff%"=="1" (for /f "tokens=1 delims=," %%A in ("%percent%") do (if not "%diff%"=="%%A" (set _titlepat=%_titlepat:: =::%&goto upb_endproc)))
:upb_
if not "%1"=="0" (goto upbproc)
set _titlepat=
goto upb_endproc
:upbproc
set _titlepat=[
for /l %%G in (1,1,%1) do (set _titlepat=!_titlepat!:)
set/a _significant=100-%1
if "%_significant%"=="0" (goto titlepat_enclose)
for /l %%G in (1,1,%_significant%) do (set _titlepat=!_titlepat! )
:titlepat_enclose
set _titlepat=%_titlepat%]
:upb_endproc
endlocal&set _titlepat=%_titlepat%&exit/b0

:ShowPercentageOfProgressInWindowTitle
setlocal
set frac=%1 %2
if defined trimtrailingzeros (
if defined updateprogressbar (
:spopiwt_invoke
call:spopiwt_proc %frac%||goto spopiwt_process_is_over
if defined hundred_reached (goto spopiwt_process_is_over)
::works semi-locally, assists itself with two variables that are set globally, to reduce progress bar drawing routine calls
endlocal&set _prev_percent=%percent%&set _titlepat=%_titlepat%&exit/b%spopiwt_returned%
)
)
shift
shift
:paramloop
if not defined trimtrailingzeros (if /i "%1"=="trimtrailingzeros_yes" (set trimtrailingzeros=yes) else (set trimtrailingzeros=no))
if not defined updateprogressbar (if /i "%1"=="updateprogressbar_yes" (set updateprogressbar=yes) else (set updateprogressbar=no))
shift
if not "%1"=="" goto paramloop
goto spopiwt_invoke
:spopiwt_process_is_over
endlocal&exit/b%spopiwt_returned%
:spopiwt_proc
::receives:
::
:: %1 - dividend (x%)
:: %2 - divisor (100%)
::
::exitcodes:
:: 0 = success calculating and setting everything
:: 1 = too few parameters
:: 2 = bad input (L>R )
:: 3 = division-by-zero error
::
set _left=%1
set _right=%2
if defined _left (if defined _right (goto spopiwt_continue))
set exitcode=1
goto spopiwt_endproc
:spopiwt_continue
if not [%_right:~7%]==[] (if not [%_right%]==[10000000] (set exitcode=2&goto spopiwt_endproc))
if %_right% EQU 0 (set exitcode=3&goto spopiwt_endproc)
if %_left% GEQ %_right% (set percent_tmp=100&set hundred_reached=true&goto spopiwt_setpercent)
if %_left% EQU 0 (set percent_tmp=0&goto spopiwt_setpercent)
:mulloop
set _left=%_left%0
set/a mulcnt+=1
set/a _left_tmp=%_left%+1>nul 2>&1||goto one_step_back
if %_left_tmp% GTR 0 (set/a _left_tmp-=1&goto mulloop)
::above piece of code "extends" the dividend until it exceeds 32-bit size, to get the longest decimal expansion of the fraction possible
:one_step_back
set _left=%_left:~0,-1%&set/a mulcnt-=3
set/a percent=(%_left%/%_right%)
set percent=0000000%percent%
if %mulcnt% EQU 0 goto point_shift_out
:point_shift
set percent_tmp=%percent:~-1%%percent_tmp%&set percent=%percent:~0,-1%&set/a mulcnt-=1
if %mulcnt% GTR 0 (goto point_shift)
:point_shift_out
if defined percent_tmp (set percent_tmp=%percent%,%percent_tmp%&set mantissa_present=true) else (set percent_tmp=%percent%)
:trimleadingzeros
if "%percent_tmp:~0,1%"=="0" (if not "%percent_tmp:~0,2%"=="0," (set percent_tmp=%percent_tmp:~1%&goto trimleadingzeros))
if "%mantissa_present%"=="true" (
if "%trimtrailingzeros%"=="yes" (
:trimtrailingzeros
if "%percent_tmp:~-1%"=="0" (set percent_tmp=%percent_tmp:~0,-1%&goto trimtrailingzeros)
)
)
if "%percent_tmp:~-1%"=="," (set percent_tmp=%percent_tmp:~0,-1%)
:spopiwt_setpercent
set percent=%percent_tmp%&set percent_tmp=
if "%updateprogressbar%"=="yes" (if not "%percent%"=="%_prev_percent%" (call:UpdateProgressBar %percent%))
if defined hundred_reached (call:spopiwt_ %2 %2&exit/b0)
:spopiwt_
::feel free to alter the line below
if defined _titlepat (title [%1/%2] [%percent%%%] %_titlepat%) else (title [%1/%2] [%percent%%%])
set exitcode=0
:spopiwt_endproc
exit/b%exitcode%


...and the variant that does not take mantissa into account at all:
@echo off
setlocal EnableDelayedExpansion
prompt $s
echo.
echo Usage:
echo.
echo ShowPercentageOfProgressInWindowTitle ^<stage^> ^<end^> updateprogressbar_[yes^|no]
echo.
echo Example: ShowPercentageOfProgressInWindowTitle 7 11 updateprogressbar_yes
echo Lack of the updateprogressbar_* parameter equals to updateprogressbar_no.
:loop
echo.
set ch_=%ch%
set ch=&set/p ch= give parameters :
if defined ch_ (for /f "tokens=1,2" %%A in ("%ch_%") do (for /f "tokens=1,2" %%C in ("%ch%") do (if "%%A"=="%%C" (if "%%B"=="%%D" (echo.&echo _PREV_PERCENT == PERCENT -^> to reduce performance expensiveness, progress bar&echo state stays halted until the routine is given with a different fraction)))))
::for /l %%G in (0,17034,9998958) do (call:ShowPercentageOfProgressInWindowTitle %%G 9998958 updateprogressbar_yes)
if /i "%ch%"=="demo" (call:demo) else (call:ShowPercentageOfProgressInWindowTitle %ch%)
if "%errorlevel%"=="1" (echo.&echo Too few parameters.)
if "%errorlevel%"=="2" (echo.&echo Integer overflow or divisor ^>1e+7.)
if "%errorlevel%"=="3" (echo.&echo Divide-by-zero attempt detected.)
goto loop
goto:eof
:demo
::counts size of all files in system32 root dir...
for %%A in (%windir%\system32\*.*) do (set/a exact_size+=%%~zA)
::floors bytes
set _exact_size=%exact_size:~0,-3%
:: ...and re-enumerates the directory to gradually add next files' sizes...
for %%A in (%windir%\system32\*.*) do (
set/a var+=%%~zA
:: ...to show the behaviour of the function
call:ShowPercentageOfProgressInWindowTitle !var:~0,-3! %_exact_size% updateprogressbar_yes
)
exit/b0
:UpdateProgressBar
setlocal EnableDelayedExpansion
if not defined _prev_percent (goto upb_)
set/a diff=%percent%-%_prev_percent%
::if there is a difference of 1 in whole percents between next calls of this function, the string-substitution feature is used just to replace ": " with "::", not to redraw the whole bar
if "%diff%"=="1" (if not "%diff%"=="%percent%" (set _titlepat=%_titlepat:: =::%&goto upb_endproc))
:upb_
if not "%1"=="0" (goto upbproc)
set _titlepat=
goto upb_endproc
:upbproc
set _titlepat=[
for /l %%G in (1,1,%1) do (set _titlepat=!_titlepat!:)
set/a _significant=100-%1
if "%_significant%"=="0" (goto titlepat_enclose)
for /l %%G in (1,1,%_significant%) do (set _titlepat=!_titlepat! )
:titlepat_enclose
set _titlepat=%_titlepat%]
:upb_endproc
endlocal&set _titlepat=%_titlepat%&exit/b0

:ShowPercentageOfProgressInWindowTitle
setlocal
set frac=%1 %2
if defined updateprogressbar (
:spopiwt_invoke
call:spopiwt_proc %frac%||goto spopiwt_process_is_over
if defined hundred_reached (goto spopiwt_process_is_over)
::works semi-locally, assists itself with two variables that are set globally, to reduce progress bar drawing routine calls
endlocal&set _prev_percent=%percent%&set _titlepat=%_titlepat%&exit/b%spopiwt_returned%
)
shift
shift
:paramloop
if not defined updateprogressbar (if /i "%1"=="updateprogressbar_yes" (set updateprogressbar=yes) else (set updateprogressbar=no))
shift
if not "%1"=="" goto paramloop
goto spopiwt_invoke
:spopiwt_process_is_over
endlocal&exit/b%spopiwt_returned%
:spopiwt_proc
::receives:
::
:: %1 - dividend (x%)
:: %2 - divisor (100%)
::
::exitcodes:
:: 0 = success calculating and setting everything
:: 1 = too few parameters
:: 2 = denominator out of the threshold
:: 3 = divide-by-zero error
::
set _left=%1
set _right=%2
if defined _left (if defined _right (goto spopiwt_continue))
set exitcode=1
goto spopiwt_endproc
:spopiwt_continue
if not [%_right:~7%]==[] (if not [%_right%]==[10000000] (set exitcode=2&goto spopiwt_endproc))
if %_right% EQU 0 (set exitcode=3&goto spopiwt_endproc)
if %_left% GEQ %_right% (set percent=100&set hundred_reached=true&goto spopiwt_setpercent)
if %_left% EQU 0 (set percent=0&goto spopiwt_setpercent)
:mulloop
set _left=%_left%0
set/a mulcnt+=1
set/a _left_tmp=%_left%+1>nul 2>&1||goto one_step_back
if %_left_tmp% GTR 0 (set/a _left_tmp-=1&goto mulloop)
::above piece of code "extends" the dividend until it exceeds 32-bit size, to get the longest decimal expansion of the fraction possible
:one_step_back
set _left=%_left:~0,-1%&set/a mulcnt-=3
set/a percent=(%_left%/%_right%)
if %mulcnt% EQU 0 goto spopiwt_setpercent
:truncate_expansion
set percent=%percent:~0,-1%&set/a mulcnt-=1
if not defined percent (set percent=0&goto spopiwt_setpercent)
if %mulcnt% GTR 0 (goto truncate_expansion)
:spopiwt_setpercent
if "%updateprogressbar%"=="yes" (if not "%percent%"=="%_prev_percent%" (call:UpdateProgressBar %percent%))
if defined hundred_reached (call:spopiwt_ %2 %2&exit/b0)
:spopiwt_
::feel free to alter the line below
if defined _titlepat (title [%1/%2] [%percent%%%] %_titlepat%) else (title [%1/%2] [%percent%%%])
set exitcode=0
:spopiwt_endproc
exit/b%exitcode%

I have deliberately split these two options for you to choose. The difference in performance is vast, and the effect of using the one that calculates mantissa is not great because the expansion shortens when a fraction has both big dividend and divisor.
As with all the other scripts, I look forward to hearing from you if it works or if it does not work, or if there is a circumstance I had not caught and made the script handle it.
Tested/working flawlessly in Windows 7 and Windows 8.

The former one, alpha version of ShowPercentageOfProgressInWindowTitle, is available for download from my Google Drive. I decided not to list it here anymore, since it is such a behemoth that I do not want anyone to get heart attack. :)

1 comment:

  1. Nice batch, and very well presented! The entire web site is very nicely done.

    ReplyDelete