跳至内容
GitHub 仓库 论坛 RSS-新闻提要

为人类格式化漂亮数字

Johannes Müller

在 Crystal 0.28.0 中,我们为人类读者格式化数字提供了一个新特性。

以前,可用的选项是在各种 Number 类型上使用 #to_s,或者最好是使用 sprintf。两者都只提供有限的输出格式,而且它们关注的是如何为计算机表示数字。它们没有考虑人类的易读性。

在用户界面中显示数字时,它们需要让人类读者能够理解。

格式化数字

认识新方法 Number#format

它允许以可自定义的格式打印数字,可以表示数字通常为人类书写的形式。

数字样式

数字可以使用可配置的小数点分隔符和千位分隔符进行格式化。

123_456.789.format('.', ',')   # => "123,456.789"
123_456.789.format(',', '.')   # => "123.456,789"
123_456.789.format(',', ' ')   # => "123 456,789"
123_456.789.format(',', '\'')  # => "123'456,789"

千位分组中的数字位数也是可配置的。这对于例如以万为单位分组的中文数字来说很有用。

123_456.789.format('.', ',', group: 4) # => "12,3456.789"

在不同的文化环境中使用着许多不同的样式,而这种方法足够灵活,可以表示大多数常见的格式。

世界如何分隔数字 提供了国际样式的概述,而 维基百科关于小数点分隔符的文章 提供了一些关于此主题的更多见解。

小数位数

浮点数在转换为人类可读字符串时可能会产生很多小数位数。对于用户输出,这种细节通常会分散注意力,显示几个小数位数就足够了。

小数位数可以在 #format 方法中直接配置。

123_456.789.format(decimal_places: 2) # => "123,456.79"
123_456.789.format(decimal_places: 0) # => "123,457"
123_456.789.format(decimal_places: 4) # => "123,456.7890"

与在格式化之前手动对值进行舍入相比,这种方法更容易,并且提供更多选项。

小数位数默认是固定的。只有当 only_significanttrue 时,尾随零才会被省略。

123_456.789.format(decimal_places: 6)                         # => "123,456.789000"
123_456.789.format(decimal_places: 6, only_significant: true) # => "123,456.789"

使数字人性化

当不同数量级的数字被关联起来时,很难用有意义的方式表示一个很大的值范围。

在这种情况下,通常使用量词来表达值的量级。

为此,我们有 Number#humanize:它将数字舍入到最接近的千位数量级,并保留一定数量的有效数字。

1_200_000_000.humanize # => "1.2G"
0.000_000_012.humanize # => "12.0n"

它与 Number#format 具有相同的用于小数点 separator 和千位 delimiter 的参数,因此样式的配置方式完全相同。

可以使用 precision 调整有效数字的位数。但是默认值 3 可能已经很适合大多数应用程序了。当 siginficanttrue 时,precision 的值是固定的小数位数,与数字的值无关。

量词默认情况下是 SI 前缀(kMG 等),但它们是完全可配置的,可以通过提供一个列表或一个 proc 来配置。

可自定义的量词

Number#humanize 可以接收一个 proc 参数,它计算特定数量级的数字位数和量词。

以下示例展示了如何格式化以公制单位表示的长度,包括单位符号。它通过使用 0.010.99 之间的值(通用映射会将其表示为毫米)的通用厘米单位来从默认实现中派生。所有其他值使用通用 SI 前缀(由 Number.si_prefix 提供)。

def humanize_length(number)
  number.humanize do |magnitude, number|
    case magnitude
    when -2, -1 then {-2, " cm"}
    else
      magnitude = Number.prefix_index(magnitude)
      {magnitude, " #{Number.si_prefix(magnitude)}m"}
    end
  end
end

humanize_length(1_420) # => "1.42 km"
humanize_length(0.23)  # => "23.0 cm"
humanize_length(0.05)  # => "5.0 cm"
humanize_length(0.001) # => "1.0 mm"

使字节人性化

第三种方法是 Int#humanize_bytes,它允许以典型格式格式化字节数(例如内存大小)。它支持 IEC (KiMiGiTiPiEiZiYi) 和 JEDEC (KMGTPEZY) 前缀。

1.humanize_bytes                          # => "1B"
1024.humanize_bytes                       # => "1.0kiB"
1536.humanize_bytes                       # => "1.5kiB"
524288.humanize_bytes(format: :JEDEC)     # => "512kB"
1073741824.humanize_bytes(format: :JEDEC) # => "1.0GB"

此方法的实现 是基于 Numer#humanize 的自定义格式的另一个示例。

总结

这些新方法为使数字对读者来说更漂亮提供了很棒的功能。

它们没有为特定区域设置提供样式映射。这是一个非平凡的任务,应该留给专门的 I18N 库。但是它们是有用的构建块,这些库可以基于它们进行构建。当不需要支持不同的区域设置时,它们可以直接使用。

但是,该实现并不完美。本地化很复杂,很难做对。通常来说,细节决定成败。例如,千位分隔符和组大小是可配置的,但具有固定值。无法用这种方式表示 印度数字系统。然后只支持阿拉伯数字。而且可能还有许多其他情况需要更专业的行为。

但是,它可能适用于超过 90% 的典型用例,并且在许多地方已经很有用。并且始终有改进的空间。

可以在 引入了这些特性的 PR 中找到更多背景信息。

从更一般的角度来说,格式化数字也是一本好书:为机器和人类格式化数字,作者 Hjalmar Gislason。