How to set the arithmetic precision in "set" batch files variables?

How do I set a precision to the SET arithmetic operation on divisions?

eg. SET /A t=3/4

But that gives result as '0' and not '0.75' ( I want at least 2 decimal places )

Or is there any other method for such arithmetic operations (using other than "set /A" ) to happen in Batch files?


Solution 1:

If you're using Microsoft's CMD, then what Nifle said applies. However, if you are using JP Software's TCC/LE to replace it, not only is there another way to do arithmetic in command scripts, but it is not limited to integer arithmetic and the precision of the calculations is controllable.

TCC/LE supports the /A option to the SET command, of course. But, as the documentation states, one can also perform arithmetic with the @EVAL[] variable function:

SET T=%@EVAL[3/4]

The output precision of the %@EVAL[] calculation is a controllable option, settable in three ways:

  • Using the configuration dialogue box invoked by the OPTION command
  • With the /F option to the SETDOS command
  • explicitly within the %@EVAL[] function itself

Solution 2:

SET /A only works on integers. So there is no way to get 0.75 using that.
And I know of no other way to do arithmetic in batch files.

Solution 3:

You might not be able to actually do what you want, but you may be able to approximate the desired results by multiplying and dividing by powers of 10, and doing string manipulation.  For example, consider this divide.bat:

@echo off
set /a q=%1 * 1000 / %2
echo q = %q:~0,-3%.%q:~-3%

%variable_name:~start_pos,length% is Command Prompt variable substring notation.  Negative numbers are relative to the length of the string (variable value); a missing length means the rest of the string.

Here are some sample results:

divide 8 4    ->    q = 2.000
divide 6 4    ->    q = 1.500
divide 3 4    ->    q = .750

In the last example (your example), q is actually 750.  If you want to compute 75% of something, either

  • multiply by %q% and divide by 1000, or
  • multiply by %q% and remember that the result is 1000× its true value, and you need to use the substring notation when you display it.

Solution 4:

The absence of leading zeroes in the result of a set /a assignment causes divide to fail when the result is less than .100; to wit,

divide 3 40 -> q = .75

Adding leading zeroes before extracting the last 3 digits fixes this problem, at least for positive numbers.

Here's the solution (for positive numbers only):

:divide

set /a q=%1 * 1000 / %2
set frac=00%q%
echo for x=%1, y=%2: q = %q:~0,-3%.%frac:~-3%

Another problem is negative numbers. The next-to-last character could be the '-' sign, and that shouldn't appear in the fraction part. The simplest fix for this is to remove the sign (if any) and then echo it ahead of the decimal string:

:divide

set /a q=%1 * 1000 / %2

set sgn=%q:~0,1%
if .%sgn%==.- (set q=%q:~1%) else (set sgn=)

set frac=00%q%
echo for x=%1, y=%2: q = %sgn%%q:~0,-3%.%frac:~-3%

Finally, there's the issue of rounding. If you're content with the value truncated to the last displayed decimal place, we're done. If you want the displayed value to be accurate within its precision, rounding is required. Since we're already going to be working with the absolute value (having removed the sign), rounding is simplified. We don't have to round positive and negative numbers oppositely:

:divide

set /a q=%1 * 10000 / %2

set sgn=%q:~0,1%
if .%sgn%==.- (set q=%q:~1%) else (set sgn=)

set /a q=(q+5)/10

set frac=00%q%

echo for x=%1, y=%2: q = %sgn%%q:~0,-3%.%frac:~-3%

That last division by 10 can be eliminated by extracting a different set of digits for display:

:divide

set /a q=%1 * 10000 / %2

set sgn=%q:~0,1%
if .%sgn%==.- (set q=%q:~1%) else (set sgn=)

set /a q+=5

set frac=00%q%
echo for x=%1, y=%2: q = %sgn%%q:~0,-4%.%frac:~-4,-1%

Multiplying by 10000 reduces the domain (i.e., the range of a parameter) of the function. cmd.exe's signed 32 bit arithmetic can represent values from -(2^31) to (2^31)-1. That's {-2147483648 to +2147483647}. Prescaling by 10000 reduces this to {-214748 to +214748}.

To handle larger numerator values, one can do the division without prescaling, then scale the result using the modulus to compute the fraction part.

The following code is mostly extracted from one of my projects. I've renamed the variables for generality and added code to permit negative denominator values. In my project, I wanted the displayed value to show a zero for the units digit rather than begin with the decimal point for values less than 1, so I generated the units part arithmetically rather than using a substring.
In case you prefer no zero unit for quotients less than unity, I've included an if defined no_zero_unit conditional to select which format to echo for small quotients.

This code is not immune to overflows. While it can handle numerator or denominator values up to +/- 2147483647, it can't handle both at the same time. As noted in the comments, Overflow can occur if min(abs(%1),abs(%2)-1) is greater than 214748 or if abs(quotient) is greater than 214748.3642.

If you try to divide -2147483648 by -1 (which is impossible in signed 32 bit arithmetic), it will crash cmd.exe with an integer overflow when it computes the modulus. Seems although cmd.exe handles the exception properly for a divide, the same operands croak it for a modulus.

I have not included tests for these overflow conditions, although it would be pretty simple to add them. We could actually double the range of the quotient by using an unsigned divide for the division by 10. Right-shifting the decimal string won't do because the overflow has already occurred. If it solves a problem for someone, I'll be happy to post the solution. It only adds one line of code.

NOTE: delayedexpansion must be enabled. If you remove the if defined no_zero_units ... else ... conditional and include only the code for the format you want (without the parentheses), you won't need delayedexpansion.

Here it is:

:grtdiv

rem delayedexpansion must be enabled before calling.

rem divides %1 by %2 and echos the quotient with 3 decimal places.

rem Constraints (violation of which yields invalid and possibly nonsensical output):
rem Max value of abs(quotient) is 214748.3642
rem Max value of min(abs(numerator), abs(denominator)-1) is 214748

rem By default, this subroutine echos a decimal string with the units
rem digit unconditionally present.  If you want no units digit for
rem values less than 1, then uncomment the following line:

rem set no_zero_units=defined

set /a qti = %1 / %2
set /a rem = %1 %% %2
set /a frc = rem*10000 / %2

rem Since abs(rem) can be any value up to min(abs(num), abs(den)-1),
rem frc calc can overflow if the lesser of these is greater than 214748.

rem Convert quotient integer and fraction parts to absolute value (either one may be zero,
rem so we can't just lop off the first character even if we know quotient is negative):

set qti=%qti:-=%
set frc=%frc:-=%

rem Get the correct sign:

set /a "sgn=(%1 ^ %2) & 0x80000000"
if %sgn%==0 (set sgn=+) else (set sgn=-)

rem frc is fraction in deci-milliseconds.

set /a qot3=(qti*10000+frc+5)/10

rem qot3 calc will overflow if abs(quotient) is greater than 214748.3642
rem qot3 is time in milliseconds rounded to nearest millisecond.
rem If value is zero, we clear sgn.

if %qot3%==0 (set sgn=)

rem Add 2 leading zeroes to value in milliseconds in case value is less than 100 ms;
rem Will use only the last 3 characters as the decimal fraction.

set frac=00%qot3%

if defined no_zero_units (

  @echo %1 / %2 = %sgn%%qot3:~0,-3%.%frac:~-3%

) else (

  set /a units=qot3/1000

  rem units is now the integer seconds part after rounding, and always has at least 1 digit.

  echo %1 / %2 = %sgn%!units!.%frac:~-3%
)
echo.

goto :eof

To eliminate the constraint on the Quotient, we can simply not put the integer and fraction parts of the quotient into the same variable. The only reason for doing this when the remainder is used to compute the fraction is so that the carry from rounding will automatically propagate from the fraction into the integer part.

To eliminate the constraints on the Numerator and Denominator requires a means of computing rem*10000/den with no possibility of overflow in the multiplication.

Scaling both rem and den down by the same factor takes care of this at the expense of some precision. The loss of precision has a calculable upper bound, however, and it's not significant when the intended final precision is 3 decimal places. Since the OP only asked for "at least 2", this should do nicely.

The approach used here is to check rem against the maximum allowable value, and right-shift both rem and den until rem does not exceed this limit. As explained in the comments, the maximum error resulting from this would appear in the 5th decimal place.

:grtdv2

rem Great Divide version 2 by Dick Neubert 10/17/2017.

rem delayedexpansion must be enabled before calling.

rem Divides %1 by %2 and returns a string in %3 containing the quotient to 3 decimal places.
rem The sign is suppressed if quotient is zero, and there is always at least one digit to
rem the left of the decimal point.

rem This routine is accurate for any valid input values (except zero denominator).
rem The integer and fraction parts of the output are computed and stored separately
rem so the fraction digits don't reduce the range of the output value.  A scaling
rem step prevents overflow in the fraction calculation regardless of the remainder value.

rem Arguments:

rem %1 is numerator.
rem %2 is denominator.
rem %3 is name of variable (no percent signs) to receive the output value string.
rem    (output variable need not already exist at call)


rem Convert numerator and denominator to absolute values:

set num=%1
set den=%2
set num=%num:-=%
set den=%den:-=%

set /a qti = %num% / %den%
set /a rem = %num% %% %den%

rem echo remainder was %rem%.

rem Scale rem & den if necessary to prevent overflow when multiplying rem by 10000:

if %rem% gtr 214748 (

 :sclfrc
  set /a "den >>=1"
  set /a "rem >>=1"
  if !rem! gtr 214748 goto :sclfrc
)

rem echo remainder after scaling =%rem%.

rem This scaling reduces the precision of qtf to no worse than 1 part in (final value of shifted rem).
rem Value of rem after last shift is not less than 214748/2, or 107374.  den gets shifted along with it,
rem but den is always greater than rem before shifting (they can be equal after shifting).
rem The maximum change in rem/den is therefore not more than 1/107374 of the displayed fraction part,
rem or 0.999/107374 = 0.0000093, or about 1 percent of an LSD.

set /a qtf = rem*10000 / den

set /a qtf=(qtf+5)/10

if %qtf% geq 1000 set /a qti += 1

rem qtf is now the fraction part rounded, and qti is the integer part,
rem adjusted if rounding produced a carry or if rem==den after scaling.
rem Note that we'll only be keeping at most the 3 LSD's of qtf, so the 1000's digit will be discarded.

rem Get the correct sign:

set /a "sgn=(%1 ^ %2) & 0x80000000"
if %sgn%==0 (set sgn=+) else (set sgn=-)

rem If quotient is zero, we clear sgn.

if %qti%==0 if %qtf%==0 (set sgn=)

rem Pad fraction part with 2 leading zeroes in case value is less than 100 (.100);
rem Will use only the last 3 characters as the decimal fraction.

set qtf=00%qtf%

rem Assemble and return output string:

set %3=%sgn%%qti%.%qtf:~-3%

goto :eof

Extending this algorithm to 4 displayed decimal digits with rounding would be unrealistic. The multiplier becomes 100000, so the max remainder value after scaling decreases by a factor of 10. The precision of the calculation thus gets worse by a factor of 10 while the displayed precision increases by the same factor, and the two now meet at the last displayed digit. Even using unsigned arithmetic for the divide by 10 in the rounding would only improve this by a factor of 2 - not enough to correct the result. If rounding were not applied, however, the multiplier would still be 10000 and the accuracy would be the same as in this version, just not rounded.