【问题标题】:ruby alphanumeric sort not working as expected红宝石字母数字排序未按预期工作
【发布时间】:2023-03-25 04:33:01
【问题描述】:

给定以下数组:

y = %w[A1 A2 B5 B12 A6 A8 B10 B3 B4 B8]
=> ["A1", "A2", "B5", "B12", "A6", "A8", "B10", "B3", "B4", "B8"]

预期的排序数组为:

=> ["A1", "A2", "A6", "A8", "B3", "B4", "B5", "B8", "B10", "B12"]

使用以下(香草)排序,我得到:

irb(main):2557:0> y.sort{|a,b| puts "%s <=> %s = %s\n" % [a, b, a <=> b]; a <=> b}
A1 <=> A8 = -1
A8 <=> B8 = -1
A2 <=> A8 = -1
B5 <=> A8 = 1
B4 <=> A8 = 1
B3 <=> A8 = 1
B10 <=> A8 = 1
B12 <=> A8 = 1
A6 <=> A8 = -1
A1 <=> A2 = -1
A2 <=> A6 = -1
B12 <=> B3 = -1
B3 <=> B8 = -1
B5 <=> B3 = 1
B4 <=> B3 = 1
B10 <=> B3 = -1  # this appears to be wrong, looks like 1 is being compared, not 10.
B12 <=> B10 = 1
B5 <=> B4 = 1
B4 <=> B8 = -1
B5 <=> B8 = -1
=> ["A1", "A2", "A6", "A8", "B10", "B12", "B3", "B4", "B5", "B8"]

...这显然不是我想要的。我知道我可以尝试先拆分 alpha,然后对数字进行排序,但似乎我不应该这样做。

可能的大警告:我们现在被困在使用 Ruby 1.8.7 :( 但即使是 Ruby 2.0.0 也在做同样的事情。我在这里错过了什么?

建议?

【问题讨论】:

  • 你的第一个预感是正确的;由于这些是字符串,它们将按字典顺序排列。如果您想将数字作为排序的元素考虑在内,则需要将字母与数字分开,并在排序时根据自己的意愿使用。
  • 我很好奇你为什么认为 string "B12" 会排在 string "B2" 之前。这不是 Ruby 对字符串进行排序的方式,而是 everything 对字符串进行排序的方式。
  • 你想要y.sort_by { |s| [s[0], s[1..-1].to_i] } #=&gt; ["A1", "A2", "A6", "A8", "B3", "B4", "B5", "B8", "B10", "B12"]。请参阅Array#<=> 了解 Ruby 如何对数组进行排序。

标签: arrays ruby sorting alphanumeric


【解决方案1】:

您正在对字符串进行排序。字符串的排序方式类似于字符串,而不是数字。如果要对数字进行排序,则应该对数字进行排序,而不是对字符串进行排序。字符串'B10'在字典上小于字符串'B3',这不是Ruby独有的东西,甚至不是编程独有的东西,这就是按字典顺序对一段文本进行排序的方式几乎无处不在,在编程,数据库,词典,字典、电话簿等。

您应该将字符串拆分为数字和非数字分量,并将数字分量转换为数字。数组排序是按字典顺序排序的,所以最终排序会完全正确:

y.sort_by {|s| # use `sort_by` for a keyed sort, not `sort`
  s.
    split(/(\d+)/). # split numeric parts from non-numeric
    map {|s| # the below parses numeric parts as decimals, ignores the rest
      begin Integer(s, 10); rescue ArgumentError; s end }}
#=> ["A1", "A2", "A6", "A8", "B3", "B4", "B5", "B8", "B10", "B12"]

【讨论】:

  • 您可以有条件地转换它,而不是为每个非数字子字符串处理异常,例如通过检查它是否以数字开头s =~ /^\d/ ? s.to_i : s
  • Kernel::Integer 在 v2.6 中进行了更改,以添加可选参数。如果包含exception: true 作为参数,则在转换为整数失败时返回nil。这将允许你写y.sort_by {|s| s.split(/(\d+)/).map {|e| Integer(e, exception: false) || e}} #=&gt; ["A1", "A2", "A6", "A8", "B3", "B4", "B5", "B8", "B10", "B12"]
  • 我看到:exception 选项也在 v2.6 中添加到 Kernel::ComplexKernel::FloatKernel::Rational
  • @CarySwoveland 感谢您指出这一点。我知道 exception 关键字,但 OP 特别要求提供符合 1.8.7 的解决方案。
  • @Stefan,没错,但我们的工作不仅仅是帮助提问者,而且提问者有一天可能会从蒸汽驱动的 v1.8.7 升级。一定要给出一个适用于 v1.8.7 的解决方案,但也可以展示在使用更高版本时如何简化该解决方案。
【解决方案2】:

这里有几种方法可以做到这一点。

arr = ["A1", "A2", "B5", "B12", "A6", "AB12", "A8", "B10", "B3", "B4",
       "B8", "AB2"]

按 2 元素数组排序

arr.sort_by { |s| [s[/\D+/], s[/\d+/].to_i] }
  #=> ["A1", "A2", "A6", "A8", "AB2", "AB12", "B3", "B4", "B5", "B8",
  #    "B10", "B12"] 

这类似于@Jorg 的解决方案,只是我分别计算了比较数组的两个元素,而不是将字符串分成两部分并将后者转换为整数。

Enumerable#sort_byarr 的每一对元素与spaceship 方法&lt;=&gt; 进行比较。由于被比较的元素是数组,所以使用Array#<=> 方法。请特别参阅该文档的第三段。

sort_by 比较以下 2 元素数组:

arr.each { |s| puts "%s-> [%s, %d]" %
  ["\"#{s}\"".ljust(7), "\"#{s[/\D+/]}\"".ljust(4), s[/\d+/].to_i] }

"A1"   -> ["A" , 1]
"A2"   -> ["A" , 2]
"B5"   -> ["B" , 5]
"B12"  -> ["B" , 12]
"A6"   -> ["A" , 6]
"AB12" -> ["AB", 12]
"A8"   -> ["A" , 8]
"B10"  -> ["B" , 10]
"B3"   -> ["B" , 3]
"B4"   -> ["B" , 4]
"B8"   -> ["B" , 8]
"AB2"  -> ["AB", 2]

在字符串的字母和数字部分之间插入空格

max_len = arr.max_by(&:size).size
  #=> 4
arr.sort_by { |s| "%s%s%d" % [s[/\D+/], " "*(max_len-s.size), s[/\d+/].to_i] }
  #=> ["A1", "A2", "A6", "A8", "AB2", "AB12", "B3", "B4", "B5", "B8",
  #    "B10", "B12"]

这里sort_by 比较以下字符串:

arr.each { |s| puts "%s-> \"%s\"" %
  ["\"#{s}\"".ljust(7), s[/\D+/] + " "*(max_len-s.size) + s[/\d+/]] }

"A1"   -> "A  1"
"A2"   -> "A  2"
"B5"   -> "B  5"
"B12"  -> "B 12"
"A6"   -> "A  6"
"AB12" -> "AB12"
"A8"   -> "A  8"
"B10"  -> "B 10"
"B3"   -> "B  3"
"B4"   -> "B  4"
"B8,"  -> "B 8"
"AB2"  -> "AB 2"

【讨论】:

    【解决方案3】:

    如果您知道号码中的最大位数是多少,您还可以在比较时为您的号码添加前缀0

    y.sort_by { |string| string.gsub(/\d+/) { |digits| format('%02d', digits.to_i) } }
    #=> ["A1", "A2", "A6", "A8", "B3", "B4", "B5", "B8", "B10", "B12"]
    

    这里'%02d'指定如下,%表示值的格式,0则指定在数字前加上0s,2指定数字的总长度, d 指定您希望以十进制(以 10 为基数)输出。您可以找到更多信息here

    这意味着'A1' 将转换为'A01''B8' 将变为'B08''B12' 将保持'B12',因为它已经有 2 位数字。这仅在比较时使用。

    【讨论】:

      【解决方案4】:

      将需要自然或字典排序,而不是标准的基于字符值的排序。这些宝石之类的东西将是一个起点:https://github.com/dogweather/naturallyhttps://github.com/johnnyshields/naturalsort

      人类将像“A2”这样的字符串视为“A”后跟数字2,对字符串部分使用字符串排序,对数字部分使用数字排序。标准sort() 使用字符值排序将字符串视为字符序列,而不管字符是什么。所以对于sort() "A10" 和 "A2" 看起来像 [ 'A', '1', '0' ] 和 [ 'A', '2' ],因为 '1' 排序在 '2' 之前字符不能更改“A10”的顺序,因此排在“A2”之前。对于人类来说,相同的字符串看起来像 ["A", 10 ] 和 ["A", 2 ], 10 在 2 之后排序,所以我们得到相反的结果。可以操作字符串以使基于字符值的sort() 产生预期的结果,方法是使数字部分固定宽度并在左侧填充零以避免嵌入空格,使“A2”变成“A02”它使用标准sort() 在“A10”之前排序。

      【讨论】:

      • 这实际上解决了问题,但并没有解释为什么排序不能开箱即用。不过非常有趣。
      • 数值排序与其字符串表示的逐个字符排序不同。字符值排序不适用于例如。 “A10”和“A2”,因为“1”的字符值小于“2”的字符值,这使得“A10”排在“A2”之前。自然排序 OTOH 会将 "A2" 解释为等效于 "A02" 或 [ "A", 2 ](我们对待它的方式,即 "A" 后跟数字 2)。
      • 注意:以上就是为什么您在数据文件中经常看到数字在固定宽度固定小数位置零填充字段中右对齐的原因,因此字符排序与数字的结果相匹配排序。
      • “将需要自然或字典排序,而不是标准的基于字符值的排序。” - Ruby 正在进行字典排序,这正是 OP 想要的。
      猜你喜欢
      • 1970-01-01
      • 2013-01-13
      • 2023-02-03
      • 2015-01-21
      • 1970-01-01
      • 2021-10-04
      • 1970-01-01
      • 2014-12-15
      • 2019-05-16
      相关资源
      最近更新 更多