在前一篇中,我们详细介绍了字符串和布尔数据类型。在本文中,我将重点讨论数值数据类型,特别是整数和浮点数据类型,详细研究它们。批处理可以轻松地处理整数,无论它们是十进制、十六进制还是八进制。

然而,浮点数与布尔数类似,因为批处理实际上并不显式地支持它们作为数据类型。但是,再一次,这种限制为富有想象力的批处理编码器提供了一个创造性的机会,这正是我们在本文完成之前要做的。

八进制案例研究

记得在某一年的8月1号,我对批处理还比较陌生之前,但我知道的比很多人都多,所以一位同事带着一个他一直在努力完成的任务来找我。在批处理代码中,他需要在给定当前日期的情况下确定前一天的日期。对于一年中的大多数日子来说,这是非常简单的,但是当今天是一个月的第一天时,它就变得复杂了。月有不同的长度;元旦带来了独特的挑战;闰年每四年发生一次,除非不是闰年。

这个最初的事件发生在2月,也许是3月,这是一个有趣的小练习,我编写并测试了它。像任何优秀的程序员一样,我在新年的第一天和最后一天都进行了测试。我还测试了几个月的第一天,尤其是极端的月份,比如1月和12月。我对3月1日进行了几个不同年份的测试,这不是因为我是在2月份左右编写这个代码的,而是因为闰年的特殊性。很快,我就交出了代码,并转向了其他项目。

代码运行了大约6个月。但在8月1日,情况突然变了。我不记得之后的后果,但我的同事花了大量的时间来追查根本原因。他最终锁定了我的bat文件,但不明白为什么它在那天停止工作。他的老板什么也没听到——代码半年都不能工作,然后就大发雷霆。我的同事一定做了某种改变,破坏了这个过程,他很难找到它。

那次搜索最终浪费了他半个工作日的时间,但经过一番尽职调查后,他最终把失败的结果告诉了我。我打开了执行日志,找到了试图查找08/01之前日期的逻辑的结果,并且……

我抬头看了看天空,举起双手,用夸张的口气尖叫道:“八进制!”我稍微修饰一下——但至少对我来说,这是相当难忘的。

执行日志里有什么让我如此不安?让我们来找出答案,但在深入研究八进制之前,我先从整数开始。

整数

我们已经对字母数字值使用了set命令,但是它也可以通过/A选项用于算术。回想一下这样的语句会发生什么:

set x=4+5

用x表示的变量设置为文本4+5。

使用/A选项将其转换为算术命令,因此以下结果将x变量设置为数字9:

set /A x=4+5

/A选项将set命令转换为执行加法和其他算术运算的方法。这些先前的值显然是硬编码为数字的。

一个稍微有趣一点的例子涉及到将变量设置为数值,然后通过set /A命令添加它们,如下所示。

set nbr1=4
set nbr2=5
set /A sum = nbr1 + nbr2
> con echo The sum is %sum%.

通过set /A命令添加两个数值变量

控制台输出为“The sum is 9”。上述演示了/A选项执行了三次set命令。首先也是最明显的一点是,算术是不加锁的。其次,等号周围有空格,在前文中我强调了这样做的危险性。为了演示,这个命令缺少/A选项

set myVar = X

不会将myVar设置为x。它将具有六个字符名称的变量myVar设置为后跟空格的两个字符值x。通过比较,/A选项使set命令的行为更像现代语言的赋值操作符,因为命令中的空格不被视为变量名称或值的一部分;令人耳目一新的是,它们只是空格。

这三个命令在功能上都是相同的;each设置myVar为7:

set myVar=7
set /A myVar=7
set /A myVar = 7

要在没有/A选项的情况下获得所需的结果,等号周围不能存在空格。然而,对于/A选项,它们可以存在,但它们也不是必需的,这是/A选项的第二个重要区别。

第三个区别是变量nbr1和nbr2没有被百分号包围。因此,/A选项允许您在不使用普遍存在的分隔符的情况下解析变量。为了灵活起见,您仍然可以使用百分号和嵌入的空格,也可以不使用,因此这四个语句在逻辑上是等效的:

set /A result = nbr1 + nbr2
set /A result = %nbr1% + %nbr2%
set /A result=nbr1+nbr2
set /A result=%nbr1%+%nbr2%

空格使代码更具可读性,所以我建议不要使用前面代码中的最后两个选项。第一个选项是最干净的,但是有些人太习惯在变量周围加上百分号了,所以第二个选项可能会提供舒适的一致性。

让我们再看一次代码中的set /A命令,但这次是在bat文件的最开头执行的:

set /A sum = nbr1 + nbr2
> con echo The sum is %sum%.

写入控制台的sum的结果值将为0。因为nbr1和nbr2还没有定义,所以在数字上下文中使用的未设置变量被认为是零,而在字母数字上下文中使用的未设置变量默认为null。由于两者都未设置,因此算术运算0 + 0的结果为0。

警告:

允许的整数范围包括-2,147,483,648到2,147,483,647(包含)。批处理将数字存储为32位带符号字段,因此任何整数都将采用这232个值中的一个。这很少会造成问题,但是因为代码没有被编译,所以要注意确保正在处理的数据符合限制。代码不会中止,也不会挂起;它只是无法计算出正确的值。批处理不是宏观经济学的首选语言。

批处理算法

批处理算术不仅仅是简单的加法。下面的清单显示了五种主要的算术运算(加、减、乘、除和模除)及其语法:

set /A sum = nbr1 + nbr2
set /A difference = nbr1 - nbr2
set /A product = nbr1 * nbr2
set /A quotient = nbr1 / nbr2
set /A modulo = nbr1 %% nbr2

这些运算符类似于其他编程语言中的运算符,但请注意模除法的双百分号。help命令显示一个%符号,但是正确的批处理语法需要两个。(实际上,模字符只是一个单独的百分号,但第一个百分号实际上是转义第二个。如果现在还不太明白,可以把这个想法留到后面的文章中介绍,但现在使用两个字符。)

现在让我们执行这些算术命令,但首先我们要定义两个操作数,nbr1和nbr2。结果以注释的形式显示在每个语句的右侧(如前所述,与符号分隔了两个命令,第二个命令可以是rem命令):

setlocal EnableDelayedExpansion

set nbr1=7
set nbr2=2

set /A sum = nbr1 + nbr2           &rem sum=9
set /A difference = nbr1 - nbr2    &rem difference=5
set /A product = nbr1 * nbr2       &rem product=14
set /A quotient = nbr1 / nbr2      &rem quotient=3
set /A modulo = nbr1 %% nbr2       &rem modulo=1

加法、减法和乘法运算不会产生意外结果,但7除以2返回3而不是3.5,因为批处理算法只处理整数,并截断结果的小数部分。19除以10不会得到1.9,它甚至不会返回2的四舍五入值。1.9的中间结果被截断为1。取模是一个返回余数的有用运算符。对n取模返回0到n - 1的值,所以对偶数取模运算返回0,因为2/2、4/2、6/2等等都是整数,不会产生余数。奇数返回1,因为3/2、5/2、7/2等等余数都是1。

奇怪的是,批处理不支持指数函数或幂函数,这对一些人来说是沮丧的,但对另一些人来说是创造力的动力。你可以创建一个案例,它接受一个基数和一个指数,并返回指数结果。

扩展的赋值运算符

当您希望向变量添加数字并将结果存储在同一变量中时,扩展的赋值操作符可以简化代码。最明显的例子是一个简单的计数器,您可能希望在每次执行set命令时将变量增加1,例如:

set /A veryVerboseTallyVariable = veryVerboseTallyVariable + 1

我故意选择了一个冗长而麻烦的变量名,因为尽管我们编码人员可能会尝试,但它们有时几乎是不可避免的。

下面的语法在逻辑上是相同的,浓缩的,并且更容易理解:

set /A veryVerboseTallyVariable += 1

下一个命令将17添加到一个命名更简洁的变量中:

set /A nbr += 17

同样,下面的一组命令分别是减2、乘2、除2和取模2:

set /A nbr -= 2
set /A nbr *= 2
set /A nbr /= 2
set /A nbr %%= 2

同样,请注意模数除法的双百分号。许多有经验的批处理程序员不知道批处理中有扩展的赋值操作符,错误地认为它们只存在于更现代的语言中,但它们确实存在,您应该在适当的时候使用它们。

运算顺序

你可以用数学中的运算顺序规则做更复杂的算术。您可能在代数课上学过“括号、指数、乘法和除法以及加法和减法”的首字母缩略词PEMDAS。对于批处理,我们有PMDAS,它更难发音,但正如前面提到的,指数不支持。让我们举个例子:

set /A nbr = 3 * (1 + 2) / 4 - 5

首先,1和2加起来等于3,因为它们在括号里,尽管加法和减法在操作顺序上是最后的。乘法和除法具有相同的层次结构,因此解释器从左到右执行它们。表达式前面的3乘以加法中的3,得到9,然后9除以4,得到2.25。实际上,它被截断了,所以它就是2。最后,减去5,结果是-3。

这个例子仅用于教学,因为将nbr设置为-3要简单得多。在实践中,将使用硬编码数字和变量的混合。例如:

set /A nbr = ((nbr1 + nbr2) * -10) / 4

根据PMDAS的规则,这里的外括号是不必要的,但它们使语句更具可读性。扩展的赋值运算符还可以处理更复杂的表达式。这两个语句在逻辑上是相同的:

set /A nbr = nbr + (2 * (4 + nbr) - -5)
set /A nbr += 2 * (4 + nbr) - -5

在这两个命令中,变量nbr都是由一个也包含nbr的数学表达式来递增的,唯一的区别是第二个命令使用扩展赋值运算符。根据操作顺序,两者都对变量加4,将其加倍,然后减去-5。(减-5等于加5。)最终,这个表达式的结果是nbr增加的量。

八进制和十六进制算术

批处理支持八进制和十六进制算术。这两种数字系统都更类似于计算机的思维方式,而不是以10为基数,所以对于程序员来说,理解并使用它们是很有用的。

十进制数字系统以10为基数,使用数字0到9。10没有数字;取而代之的是两个数字:新的位值从1开始,而个位从0重新开始,因此是10。相比之下,八进制系统是以8为基数,使用数字0到7。八进制数7加上1不能得到8,因为8(和9)在八进制数系统中是没有意义的字符。相反,八进制数10(发音为“1 - 0”,因为它不是“10”)相当于十进制数8。同样,八进制11等于十进制的9,以此类推。

十六进制的数字系统以16为基数,所以它有与八进制相反的问题:它需要16个唯一的数字,比大多数人类数字系统中使用的10个数字要多,因为我们已经进化到两只手上各有5个数字。从0数到9,我们得到了A、B、C、D、E和F。十六进制数B等于十进制数11,十六进制数F等于十进制数15,十六进制数10等于十进制数16。

批处理可以使用八进制、十六进制和/或十进制输入执行算术,同时始终以十进制形式返回答案。十六进制数前加0x,八进制数前只加0。因此,这两个变量分别被赋八进制和十六进制值:

set octalNbr=012
set hexadecimalNbr=0xB

不管操作数的基数是十进制、八进制还是十六进制,批处理总是将结果存储为十进制。为了演示,首先看这个例子:

set decimal7=7
set decimal1=1
set octal7=07
set octal1=01
set /A decimal = decimal7 + decimal1
set /A octal = octal7 + octal1

数字7和1以十进制和八进制的形式相加。十进制的结果显然是8。这两个八进制数的和是八进制10(“1 - 0”,而不是十进制10),但是解释器立即将该值存储为十进制8。在这个例子中,十进制和八进制表现相同,但这并不总是正确的。

现在来看看这个例子:

set decimal11=11
set decimal2=2
set octal11=011
set octal2=02

set /A decimal = decimal11 + decimal2
set /A octal = octal11 + octal2
> con echo The decimal sum is %decimal%.
> con echo The octal sum is %octal%.

十进制加法得到十进制13,而八进制加法得到八进制13(“1 - 3”,而不是十进制13)。记住,八进制没有8和9。八进制10是十进制8,在这个例子中八进制13是十进制11。因此,在批处理中,11 + 2 = 13,而011 + 02 = 013 = 11,因此显示结果如下:

The decimal sum is 13.
The octal sum is 11.

解释器甚至可以处理十进制和八进制混合的算术运算。10 + 10的十进制加法是20,010 + 010的八进制加法是16。当将一个十进制数和一个八进制数相加时,比如10 + 010,批处理会给出正确的结果18。通常,这种类型的算术是偶然完成的,但有时精明的编码人员会利用这一点,很高兴知道这是可能的。

以类似的方式,这些值被视为十六进制:

set /A hexadecimalNbr = 0xA * 0x14

通过这种乘法运算,0xA等于十进制数10,而0x14在转换为十进制数时比16大4。该语句执行后,变量等于200,即10和20的乘积。

八进制和十六进制可以是强大的工具;但是,如果您打算进行十进制算术,请小心确保没有前导零。由于十六进制从0x开始,意外地执行十六进制算术要困难得多,但由于一个看似无害的前导零而不知不觉地执行八进制算术则非常容易。

重要:

因为数学无处不在,你会在后续文章中找到包含各种bat文件算术示例的例子。批处理还具有用于位操作的算术运算符:位和、位或、位异或、逻辑左移和逻辑右移。我将等到后续再讨论它们,因为这些操作符使用了一些特殊的字符,这些字符有其他用途,而且许多有经验的程序员从来没有在编译过的代码中操作过一点,更不用说批处理了。

浮点数

批处理不显式地处理浮点数——即非整数的有理数。事实上,如果要对这些数字进行广泛的处理,可以使用比批处理更好的工具。这类似于用铁锹挖房子的地基。这是可以做到的,但只有最严厉的苦行僧才能做到。如果任务足够大,可以编写一些编译后的代码并从bat文件调用它,但是当需要执行一些轻量级浮点运算时,批处理可以处理它,就像您可以使用铲子在前院种几个郁金香球茎一样。

请记住,所有批处理变量实际上都只是经过美化的字符串。我们可以很容易地为两个变量赋值为浮点值——也就是说,为小数点赋一个点的一些数字。这里有两种面额的美元和美分:

set amt1=1.99
set amt2=2.50

如果这些是整数,我们可以简单地使用set /A命令将它们相加。让我们试一下,看看会发生什么:

set /A sum = amt1 + amt2

结果是存储在总和中的值3,而不是期望的4.49。每个数字的小数部分被完全忽略,结果是整数1和2的总和。

我们需要去掉小数点,做算术,然后恢复小数点。将每个数字乘以100就可以了,但是批处理不允许这样做。然而,由于浮点值只是一个经过伪装的字符串,我们可以使用前文中描述的语法删除小数点:

set amt1=%amt1:.=%
set amt2=%amt2:.=%

现在的数量是199和250。这个set /A命令的结果是449

set /A sum = amt1 + amt2

要还原小数,我们不能简单地除以100——再次强调,这只适用于整数——但我们可以使用前面文章中更多的字符串解析逻辑。使用子字符串,下面的set命令将变量重置为三个项目的连接:除了数字的最后两个字节之外的所有内容,硬编码的小数点(或点),以及数字的最后两个字节:

set sum=%sum:~0,-2%.%sum:~-2%
> con echo The sum is %sum%.

最后,写入控制台的变量被设置为4.49

乘法也是这样。如果你花499美元买了一台新电脑,第一年不用付款,利率为19%,一年后你会欠多少钱?利率换算成1.19,但我们必须去掉小数点。找到两个整数的乘积后,我们通过将小数点插入最后两个字节之前来恢复小数点,如下所示

set amt=499
set factor=1.19
set factor=%factor:.=%
set /A product = amt * factor
set product=%product:~0,-2%.%product:~-2%
> con echo The product is %product%.

上述就表示整数和浮点数的乘法

593.81的产品可能会让你重新考虑投资计划。

每个程序员的目标都想编写“银弹”代码。不幸的是,之前的产品更多的是棉网而不是凯夫拉,并且有许多batveat需要讨论。我们做了几个假设,如果违反了其中任何一个,代码就会失效。加法假设两个数都有两位小数;1.9而不是1.90会使结果偏差10倍。除小数点以外的非数字字符会导致问题,值的前导零会触发八进制运算。乘法就更复杂了。前面代码包含一个整数数量,但如果amt以美元和美分表示,则乘积将得到四位小数,而不是两位。为了将结果表示为美元和美分,最后两个字节应该被截断——或者更好的是,四舍五入。

我不会在这里讨论这些细微差别,原因很简单,如果输入不一致并且需要数据验证,批处理浮点算法可能不是最佳解决方案。为所有可能的情况编写代码将是无趣的。重要的是,程序员需要选择最佳的选项。如果所有值的小数位数一致,则只需几行代码就可以完成算术。在极少数情况下,当我在批处理中使用浮点数据类型时,它是用于涉及一致数据的非常特定的任务。

八进制案例研究,续

那么,我在那份执行日志里到底找到了什么在千禧年年初的那一年8月的第一天?在bat文件中,今天的日期格式为CCYYMMDD,例如20050801,它被分解为三个独立的字段:

todaysYear = 2005
todaysMonth = 08
todaysDay = 01

如果todaysDay不是01,我们只需从8位数中减去1,然后继续。但是当它是01时,我们需要做一些额外的算术。考虑到月份的逻辑(并且理解一月份会有一些特殊的逻辑),我们必须减去1来确定前一个月:

set /A month = todaysMonth - 1

当todaysMonth为03时,月份为2;当todaysMonth是07时,月份是6。但是,当todaysMonth为08时,即8月1日,前一个算法中的月份解析为-1的值。

解释器看到前导0并将算术处理为八进制算术。Octal只理解数字0到7,所以当解释器看到8时,它认为这个字符和“ohkuh”(在Vulcan语言中对应8的数字)一样陌生,并直接忽略它。最后,set /A命令将表达式剩余部分的数学结果(-1)分配给month变量。该值最终打破了日期逻辑,我们无法获得所需的日期7月31日。

该死的八进制

使用子字符串和if命令,我插入了这一行修复,以去除todaysMonth变量值的前导零(如果存在):

if %todaysMonth:~0,1% equ 0  set todaysMonth=%todaysMonth:~1%

这段代码在接下来的几年里运行良好,即使是在8月和9月的第一天。如果原始代码没有在8月1日运行,那么如果在9月1日运行,它就会失败,因为9月用09表示。但是如果代码没有在这两天运行呢?下一次失败是什么时候?在10月1日,这个月将被记为10。解释器会把它当作小数来处理,代码也会按照预期执行。所以,8月1日和9月1日是唯一能够解决密码的日期。

注意八进制。

总结

在本文中,我讨论了数字数据类型以及在批处理中如何处理它们。与大多数其他语言不同,批处理变量没有定义为特定的数据类型。本质上,所有变量都是简单的字符串,但是当字符串包含数字时,它可以被视为数字。

加法、减法、乘法、除法甚至模除法都可以相对轻松地处理十进制整数,使用的是您可能在学校里学过的运算顺序规则。也支持八进制和十六进制整数,尽管八进制算术很容易被错误地调用。从我的个人经验来看,确保你的十进制整数没有任何零前缀。扩展的赋值运算符提供了一种方便的、未充分利用的整数递增工具。

批处理中不支持浮点数字数据类型,但是您已经了解到,只需做一点工作,就可以对带有小数点的数字执行一些轻量级算术。

换句话说,我将在下一篇文章中讨论文件移动。批处理的一个非常有用的特性是创建、复制、移动、重命名和删除文件和目录。