大前端金融领域的浮点数处理

金融领域对数字的处理尤其敏感,如股票终端上通常会高频计算涨跌幅、涨跌额等行情指标,稍微复杂的场景可能还涉及汇率等多个计算因子。我们希望在端上保持计算的高性能和准确性。

浮点字符串 VS 乘以系数的整数

大家都知道,使用二进制表示浮点数无法精确表示(就像1/3无法用十进制精确表示一样)。服务端如果直接使用 JSON 格式的浮点字段返回给前端,传输、解析和计算的任意环节的精度丢失都将是业务不可接受的。

一种解决方法是使用浮点字符串,然后通过字符串解析为特定平台的 Decimal 高精对象(例如 Objc 的 NSDecimalNumber 或 Java 的 Decimal 类型)。

另一种方法是将浮点数乘以一个倍数然后返回整数类型。每次计算直接使用整数运算,最后显示的时候再格式化为对应的小数格式。

如果你的业务存在高频(如股票行情推送实时刷新)计算的场景,用户终端解析浮点字符串可能会有不小的性能压力,使用带乘数因子的整数性能表现会好的多(见下表)。如果在业务早期就规定好浮点数传输的乘数因子协议,将会规避掉很多后续的规范和性能问题。

特征 带乘数因子的整数价格浮点字符串价格
解析 客户端解析一个简单的整数。这是一个非常快且开销较低的操作。 客户端必须解析字符串构造数值结构体,这涉及复杂且计算量巨大的逻辑来处理各种格式(例如,科学计数法、小数点)。这比解析整数慢得多。
算术 整数运算直接由 CPU 的算术逻辑单元 (ALU) 执行,速度极快。乘法尤其高效。 浮点运算更为复杂,通常比整数运算花费的时间更长,但随着时间的推移,性能差距已显著缩小。
精确 财务价值的存储非常精确。例如,1.23 美元的价格可以存储为123因子为 100 的整数,从而消除任何潜在的舍入误差。 浮点数(如 Float 或 Double)是实数的近似值,在计算过程中可能会引入小的、不可恢复的错误,这对于财务数据来说是不可接受的。
内存和缓存 整数比浮点数更紧凑。这提高了缓存效率,允许更多价格数据存储在 CPU 的缓存中,并减少内存访问时间。 浮点类型 Double 比标准整数占用更多的内存,在处理大型数据集时缓存效率较低。
代码简单性 数学很简单:对整数进行计算,然后仅将最终结果转换回格式化的字符串以供显示。 处理浮点数需要仔细考虑精度问题,与整数数学相比,这增加了复杂性和开销。

 

小数舍入-银行家舍入法

保留特定位数的小数点然后展示在界面是大前端最最基本的需求之一,例如在 iOS 中,如果你使用 c 语言浮点数格式化,你将得到一个标准的四舍五入:

float num = 3.125;
NSString *result = [NSString stringWithFormat:@"%2f", num];
NSLog(@"使用 c 默认舍入%@", result);
// result 输出 3.13

如果你使用 NSNumberFormatter 格式化一下 NSNumber 对象,输出结果却不太一样:

NSNumberFormatter *fmt = [[NSNumberFormatter alloc] init];
fmt.numberStyle = NSNumberFormatterDecimalStyle;
fmt.minimumFractionDigits = 2;
fmt.maximumFractionDigits = 2;
NSLog(@"使用 NSNumberFormatter 舍入%@", [fmt stringFromNumber:@(3.125)]);
// 输出 3.12

 

为什么同样是保留 2 位小数点,值却相差了 0.01?原来 iOS 的 NSNumberFormatter 是默认使用银行家舍入法的(IEEE 754),当然可以设置 roundingMode 指定其他舍入方式,推荐在前端展示时统一使用银行家舍入法,其可以有效减少累计误差并维持数据统计特性。

对应 Android(Java) 中的 BigDecimal HAIL_EVEN 类型。

对应 JavaScript 中的 toFixed() 方法,不过我测试 toFixed 方法发现因为引擎实现有差异,在 Chrome 上并不会精确实现银行家舍入,需要借助第三方库 decimal-format 辅助实现。

银行家舍入法规则可以总结为:

  • 当要舍弃的数字小于 5 时,直接舍去。
  • 当要舍弃的数字大于 5 时,进位。
  • 当要舍弃的数字正好是 5 时,会考虑两种情况:
    • 如果 5 后面有非零数字,则无论前面的数字为何,都应进位。
    • 如果 5 后面没有数字(或都是零),则需要查看 5 前面的数字:
      • 如果 5 前面的数字是偶数,则舍去 5。
      • 如果 5 前面的数字是奇数,则进位。

🌰 举个例子:

原始数字保留 2 位小数点后结果说明
3.785272 3.79 最后一位等于 5,更接近 3.79 而不是 3.78,因此它不是等距的,虽然前面一位为偶数8也不应该向偶数舍入,而是想接近的数舍入,即进位变成 3.79。
3.785 3.78 最后一位等于 5, 和 3.79 、 3.78 是等距的,前面一位为偶数8,直接舍去变成 3.78。
3.125 3.12 最后一位等于 5, 和 3.12、3.13 是等距的,前面一位为偶数 2,直接舍去变成 3.12。
3.135 3.14 最后一位等于 5,和 3.13、3.14 是等距的,前一位数字是奇数 3,即进位变成 3.14。
3.134 3.13 最后一位小于 5,直接舍弃变成 3.13。
3.136 3.14 最后一位大于 5,直接进位变成 3.14。

 

posted on 2025-10-07 15:01  刘继新  阅读(55)  评论(0)    收藏  举报

导航