Technical Article

Delphi 中的工程函数:进制转换与复数运算

Excel 中的工程函数系列读起来像是函数参考中最简单的部分。DEC2BIN 将数字转换为二进制字符串。HEX2DEC 将其转换回。IMSUM 将两个复数相加。每一个看起来都像是格式化练习。其实不然。在这些名称背后,坐落着自计算机体系结构课程以来大多数开发人员从未接触过的十位二进制补码编码、完全存在于字符串内部的复数格式,以及如果您在检查之前进行移位就会默默溢出 64 位整数的按位运算符。精确再现 Excel 的电子表格引擎无法对其中任何内容进行四舍五入。

这些函数分为三组,每组都隐藏着不同的陷阱。进制转换涉及负数和每个进制的阈值。复数运算涉及解析和格式化字符串。按位运算涉及保持在 Int64 的边界内。本文将逐个介绍 HotXLS 实现的每组函数,并附带您实际编写的电子表格调用。

进制转换与十位补码

正向转换是每个人都期望的部分。DEC2BIN(9) 给出 "1001",可选的第二个参数将结果左填充到固定宽度。陷阱是负输入。Excel 不写减号。它将值编码为目标进制中的十位二进制补码字符串,这就是为什么 DEC2BIN(-5,10) 返回 "1111111011" 而不是任何带符号的内容。一旦值是负数,位数参数就会被忽略,因为编码已经固定为十位数字。

十位数字是固定的预算,而该预算设置了每个进制的可表示范围。在二进制中,翻转到负半部分的大小是 512,环绕模数是 1024,因此二进制字符串仅在长度正好为十个字符且其值至少为 512 时才带符号。同样的概念随着进制而扩展。八进制使用 2^29 的半阈值和 2^30 的满模数。十六进制使用 2^39 和 2^40。HotXLS 读取器精确应用此规则:它累加数字,并且只有当字符串为十个字符宽且累加值等于或高于半阈值时,它才减去满模数以恢复有符号值。九个字符的字符串无论多大,始终是非负的。

编码器是镜像。非负值逐位转换,并可选地用零填充到请求的宽度,如果它溢出进制的正上限或者如果请求的宽度太窄而无法容纳它,它就会被拒绝。负值首先通过加上满模数带入范围内,这将其转换为其进制表示始终为十位数字的值,然后输出带有前导零的数字以填充宽度。单一共享的范围检查,即每个进制对称的下限和上限,是保持 DEC2BINDEC2OCTDEC2HEX 在其边缘处相互一致的关键。

这留下了跨进制转换,例如 HEX2BINOCT2HEX,它们在更改进制时函数名中不经过十进制。实现并没有为每个有序对携带一个单独的例程。它使用源进制将输入字符串解析为有符号十进制值,然后将该十进制值格式化为目标进制。十进制是枢纽。一个解析例程和一个格式化例程组合在一起,覆盖了所有组合,并且由于两半部分共享相同的十位有符号约定,负值在转换过程中能保留其符号完好无损。

复数是字符串,因此工作是解析

Excel 没有复数数据类型。复数值是字符串 "a+bi",且 IM 系列中的每个函数都接受这些字符串作为输入并返回一个。COMPLEX 从实部和虚部构建字符串。IMSUMIMSUBIMPRODUCTIMDIV 解析它们的参数,对数字部分进行算术运算,并将结果格式化回字符串。数字工作是大学代数。困难完全在于可靠地将文本转换为两个浮点数,这正是内部解析器发挥作用的地方。

该解析器中有两个细节很容易出错。第一个是裸虚数单位。字符串 "i" 表示 1 乘以 i,不是零也不是错误,因此当后缀前的系数为空或为单独的加号时,解析器必须将其读取为值 1,而单独的减号为 -1。跳过这一点,IMSUM("i","i") 就不再是 2i。第二个是科学计数法与分隔实部和虚部的符号冲突。解析器通过扫描加号或减号来查找该分隔符,但是写为 "1.5E-3" 的数字包含属于指数的减号。因此,当紧接在前面的字符是 eE 时,扫描拒绝将加号或减号视为分隔符。如果没有该保护,实部将在指数符号处被撕成两半,解析将在完全合法的输入上失败。

后缀本身被保留而不是被规范化。Excel 接受 ij,并且 HotXLS 会记住输入使用了哪一个,以便格式化后的结果携带相同的字母。格式化随后应用常规缩写:值为 1 的虚部仅打印为后缀,负 1 打印为 -i,为 0 的虚部折叠为普通实数,为 0 的实部删除前导 0+

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Engineering');
    // Negative input: a ten-bit two's complement, places argument ignored.
    Sheet.Cells[1, 1].Value := Sheet.Calculate('=DEC2BIN(-5,10)'); // 1111111011
    // Complex multiply on two "a+bi" strings.
    Sheet.Cells[2, 1].Value := Sheet.Calculate('=IMPRODUCT("3+4i","1+2i")'); // -5+10i
  finally
    Book.Free;
  end;
end;

复数超越函数,其中包括 IMSQRTIMEXPIMLNIMPOWER,在直角坐标系中不起作用。它们将解析后的值转换为极坐标形式,对模和辐角应用运算,然后转换回。平方根将辐角减半并对模取根。乘幂对辐角进行乘法运算并对模取幂。以任何其他方式进行将意味着在直角坐标系中重新推导每个恒等式,这既会增加代码,又在分支割线附近缺乏数值稳定性。

按位运算符与您必须首先检查的溢出

Excel 2013 添加了 BITANDBITORBITXORBITLSHIFTBITRSHIFT。操作数受到限制:每个必须是且不大于 2^48 减 1 的非负整数,任何分数或负数参数都是数字错误。这个上限足够大,可以覆盖任何实际的标志集,同时保持在双精度浮点数可精确表示的范围内,这很重要,因为 Excel 将每个数字参数作为浮点值传递。

移位函数带有真正棘手的唯一顺序规则。左移可以产生比其输入大得多的值,如果您先执行 shl 然后检查结果,您就已经溢出了 Int64,测试就毫无意义了。检查必须在移位之前进行。HotXLS 将操作数与右移了移位量的上限进行比较,只有在操作数适合时,它才执行实际的左移。超出 53 位的移位幅度会被直接拒绝,负移位只需反转方向,因此带有负计数的 BITLSHIFT 表现为右移。这一原则不仅限于这一个函数:当存在防止溢出的保护时,它必须运行在输入上,而不能运行在它旨在保护的结果上。

// Bitwise calls evaluate the same way through Calculate.
Sheet.Cells[3, 1].Value := Sheet.Calculate('=BITAND(13,11)');    // 9
Sheet.Cells[4, 1].Value := Sheet.Calculate('=BITLSHIFT(5,2)');   // 20
Sheet.Cells[5, 1].Value := Sheet.Calculate('=BITRSHIFT(40,3)');  // 5

未来函数与 _xlfn 名称前缀

按位运算符和一长串其他 2007 年后添加的内容与命名方案相互作用,该方案与它们计算的内容无关,而与 Excel 如何存储它们息息相关。最初的二进制电子表格格式在固定表中为每个内置函数分配了一个数字槽。在该表冻结后发明的函数没有槽。为了将此类函数保存到文件中并使现代 Excel 识别它,名称在写入时带有 _xlfn. 前缀,因此即使用户只输入 BITANDBITAND 在磁盘上也存储为 _xlfn.BITAND

问题在于规则并不统一。一些较新的函数被赋予了表槽并以裸名称写入,而一些遗留的隐藏函数尽管年代久远,也在没有前缀的情况下写入。HotXLS 保留了哪些名称需要前缀的显式白名单,在写入时添加它并在读取时剥离它,因此您设置并读回的公式文本始终是面向 Excel 的干净名称。您设置 =BITLSHIFT(5,2),文件保存 _xlfn.BITLSHIFT,无论如何值都会以 20 返回。前缀是一个存储细节,永远不应该泄露到您在代码中使用的公式中。

在电子表格中组合使用

所有这些的公共接口都很小。创建一个 TXLSXWorkbook,添加一个电子表格,然后通过 Cells[Row, Col].Formula 将公式写入单元格并重新计算,或者直接使用电子表格的 Calculate 方法评估表达式,该方法针对该表格编译公式并返回 Variant。上面的示例使用 Calculate,因为它显示了单个工程调用的结果,而没有周围的表格状态,但在工作簿重新计算时,相同的函数在真实的单元格公式中评估是完全相同的。

需要记住的是编码,而不是调用位置。二进制字符串仅在十位数字且仅超过其进制的半阈值时才带符号。复数是文本,空的虚数系数为 1,解析器跨过指数的 e。左移在移位前进行检查。弄清这四个事实,工程函数系列就再也不会成为由于符号错误而导致意外的源头了。

如果您将自己的领域数学连接到相同的引擎中,注册处理程序和返回值的方法在我们关于使用自定义函数扩展公式引擎的文章中进行介绍,当这些公式必须按名称而不是按单元格地址跨表格访问时,关于定义名称和跨表格公式的演练展示了引用是如何解析的。这里介绍的工程函数作为适用于 Delphi 和 C++Builder 的 HotXLS 电子表格组件的一部分提供,与本博客其他地方介绍的读取、写入和计算 API 相配套。