Rob van der Woude's Scripting Pages

Format numbers in NT batch files

Introduction

When working with numbers, we sometimes want to display them in an alternative format, e.g. scientific notation, hexadecimal, or GB with a single decimal.
Though NT batch files are capable of handling octal and hexadecimal numbers besides decimal, numbers are always displayed in plain decimal.
For alternative display formats, we will have to write our own batch code.

Hexadecimal

Displaying numbers in hexadecimal is not as hard as it may seem: integer divide the number by 16, and convert the remainder to a single hexadecimal digit; repeat until there is nothing left to integer divide.

In practical code:

:: Delayed variable expansion is required
SETLOCAL ENABLEDELAYEDEXPANSION
:: Variable Decimal holds the decimal number
SET Decimal=987654321
:: Variable Convert is a helper variable
SET Convert=0123456789ABCDEF
:: Variable Hexadecimal will hold the end result
SET Hexadecimal=
:: Variable Scratch holds the intermediate results
SET Scratch=%Decimal%

:Loop
:: Variable LSD holds the intermediate value's Least Significant Digit
SET /A "LSD = %Scratch% %% 16"
:: Convert LSD to hexadecimal (this is where delayed variable expansion is required)
SET LSD=!Convert:~%LSD%,1!
:: Prepend LSD to the hexadecimal string
SET Hexadecimal=%LSD%%Hexadecimal%
:: Integer divide by 16 by shifting 4 bits (1 hexadecimal digit) "over the edge"
SET /A "Scratch = %Scratch% >> 4"
:: Alternative: SET /A "Scratch = %Scratch% / 16"
:: Calculate the next digit if the intermediate value Scratch is still greater than 0
IF NOT %Scratch% EQU 0 GOTO Loop

:: Add 0x prefix to the hexadecimal string
SET Hexadecimal=0x%Hexadecimal%

:: Show decimal input and hexadecimal output
SET Decimal
SET Hexadecimal

ENDLOCAL

You can test the result using one of the following commands:

If you don't mind using PowerShell, displaying numbers in hexadecimal is a lot easier:

SET Decimal=987654321
FOR /F %%A IN ('powershell -C "\""0x{0:X8}\"" -f %Decimal%"') DO SET Hexadecimal=%%A
:: Or: FOR /F %%A IN ('powershell -C "[Convert]::ToString( %Decimal%, 16 )"') DO SET Hexadecimal=0x%%A
:: Show decimal input and hexadecimal output
SET Decimal
SET Hexadecimal

Octal

Displaying numbers in octal is even easier than hexadecimal, as there is no need for delayed variable expansion.

In practical code:

:: Variable Decimal holds the decimal number
SET Decimal=987654321
:: Variable Octal will hold the end result
SET Octal=
:: Variable Scratch holds the intermediate results
SET Scratch=%Decimal%

:Loop
:: Variable LSD holds the intermediate value's Least Significant Digit
SET /A "LSD = %Scratch% %% 8"
:: Prepend LSD to the Octal string
SET Octal=%LSD%%Octal%
:: Integer divide by 8 by shifting 3 bits (1 octal digit) "over the edge"
SET /A "Scratch = %Scratch% >> 3"
:: Alternative: SET /A "Scratch = %Scratch% / 8"
:: Calculate the next digit if the intermediate value Scratch is still greater than 0
IF NOT %Scratch% EQU 0 GOTO Loop

:: Add leading 0 to the octal string
SET Octal=0%Octal%

:: Show decimal input and octal output
SET Decimal
SET Octal

Testing the result of the octal "conversion" is similar to testing the result of the hexadecimal "conversion":

If you don't mind using PowerShell, displaying numbers in octal is a lot easier:

SET Decimal=987654321
FOR /F %%A IN ('powershell -C "[Convert]::ToString( %Decimal%, 8 )"') DO SET Octal=%%A
:: Show decimal input and octal output
SET Decimal
SET Octal

Right Aligned With Decimal

A list of files with their size in MBs with a single decimal digit calls for right alignment.

An example:

@ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
:: Calculate 1 MB
SET /A MB = 1024 * 1024
FOR %%A IN (*.mp3) DO (
	SET /A "FileSize = ( ( 20 * %%~zA / %MB% ) + 1 ) / 2"
	SET /A "Fraction = !FileSize! %% 10"
	SET /A "Whole    = !FileSize!  / 10"
	SET Align=        !Whole!.!Fraction!
	SET Align=!Align:~-8!
	ECHO !Align! MB    %%~nA
)
ENDLOCAL

The line SET /A "FileSize = ( ( 20 * %%~zA / %MB% ) + 1 ) / 2" takes the file size (%%~zA, multiplies it by 20, divides it by 1 MB, adds 1, and then divides it all by 2.
Why?
Without a decimal, it would have been like this: SET /A "FileSize = ( ( 2 * %%~zA / %MB% ) + 1 ) / 2".
What we really do here, is overcome some of the limitations of integer math.
Multiplying by 2, adding 1, and then integer dividing by 2 is the integer-only equivalent to taking the integer value of a ( floating number + 0.5 ), which is the proper way to correctly round floating numbers to integers.
By multiplying by 20 instead of 2, we get 10 times the result, allowing us to extract the whole and a 1-digit fraction in the lines following this line.
Variable Align prefixes the result with 8 spaces (SET Align=        !Whole!.!Fraction!), end is then chopped to 8 characters from the right (SET Align=!Align:~-8!).

Note: By multiplying the decimal number (file size) by 20, we limit the code to files of 107,374,182 bytes and less (0x7FFFFFFF / 20), or 102 MB.
For larger files, negative numbers will be displayed.
By not dividing by 1 MB but 0.1 MB, and then multiplying by 2 instead of 20, we may correctly handle files of about 1020 MB, at the cost of a slight accuracy decrease.

The result will look like this:

     9.2 MB    Al Stewart - Year of the Cat
     6.4 MB    Bolland en Bolland - You're in the army now
     8.5 MB    David Bowie - Heroes
     7.0 MB    David Bowie - Space Oddity
    22.5 MB    Earth and Fire - Atlantis
    24.7 MB    Earth and Fire - Song of the Marching Children
     4.1 MB    Elvis Presley - Can't Help Falling In Love
     5.1 MB    Enya - Orinoco Flow
     6.8 MB    Europe - The Final Countdown
     6.1 MB    Heart - Barracuda
     6.5 MB    Jon and Vangelis - I'll Find My Way Home
     6.9 MB    Jon and Vangelis - So Long Ago So Clear
    11.0 MB    Led Zeppelin - Stairway To Heaven
     3.1 MB    Louis Armstrong - What a Wonderful World
     5.5 MB    Procol Harum - A Whiter Shade of Pale
     4.3 MB    The Beatles - Girl
     4.4 MB    The Beatles - Hey Bulldog
     4.8 MB    The Beatles - Lucy In The Sky With Diamonds
     6.1 MB    The Moody Blues - Nights in White Satin
     8.3 MB    Vangelis - La petite fille de la mer
    12.4 MB    Vangelis - To the Unknown Man

See the Batch Files Math page for more details on handling floating numbers.

In this case it may be a good idea to drop batch and use "pure" PowerShell instead:

Get-ItemProperty -Path '*.mp3' | ForEach-Object {
	"{0,8:F1} MB    {1}" -f ( $_.Length / 1MB ), [System.IO.Path]::GetFileNameWithoutExtension( $_.Name )
}

Believe me, there comes a point where you no longer want to wrap these long PowerShell commands in batch code, and you're getting very close...

I cannot get the PowerShell "one-liner" shown above to work when wrapped in batch, but I can mix batch and PowerShell code and make that work:

FOR %%A IN (*.mp3) DO @powershell -C "\""{0,8:F1} MB`t{1}\"" -f ( %~zA / 1MB ), \""%~nA\"""

You see, batch is easier than PowerShell for getting file name and size.
Using PowerShell to format the number solves the file size limit too.
Note that when wrapping a "template string" with multiple spaces in batch, those multiple spaces will be replaced by a single space, hence the tab (`t) between MB and {1}.

Check out ToString.exe, a batch tool to format numbers and dates the .NET way.
With this tool, the MP3 list above can be created using the following command:

FOR %%A IN (*.mp3) DO @FOR /F %%B IN ('SET /A %%~zA / 1048576') DO @ToString.exe "{0,8:F1} MB\t{1}" %%B "%%~nA"

or for better readability:

@ECHO OFF
FOR %A IN (*.mp3) DO (
	FOR /F %B IN ('SET /A %~zA / 1048576') DO (
		ToString.exe "{0,8:F1} MB\t{1}" %B "%~nA"
	)
)

Besides simpler code compared to "pure batch", the file sizes will be more accurate and not limited to 102 MB, and the decimal separator will depend on your computer's culture.

Or how about this basic hexadecimal table:

FOR /L %%A IN (1,1,32) DO @ToString.exe "{0,2}\t0x{0:X2}" %%A

Or show today's date in ISO formats:

ToString.exe "{0:yyyy-MM-dd}" %Date%

or:

ToString.exe "{0:yyyyMMddTHHmmssfffzzz}" %Time%

page last modified: 2022-03-01