根据值将数组拆分为子数组

22

我在寻找Ruby Core中与String#split等价的数组方法,但惊讶地发现这种方法并不存在。除了下面的方法,是否有更加优雅的方式将一个数组根据某个值拆分成子数组?

class Array
  def split( split_on=nil )
    inject([[]]) do |a,v|
      a.tap{
        if block_given? ? yield(v) : v==split_on
          a << []
        else
          a.last << v
        end
      }
    end.tap{ |a| a.pop if a.last.empty? }
  end
end

p (1..9 ).to_a.split{ |i| i%3==0 },
  (1..10).to_a.split{ |i| i%3==0 }
#=> [[1, 2], [4, 5], [7, 8]]
#=> [[1, 2], [4, 5], [7, 8], [10]]

编辑:对于那些感兴趣的人,触发此请求的“现实世界”问题可以在此答案中看到,其中我使用了下面@fd的答案进行实现。


1
@Rafe 也许我应该接受最绕的 hack 的答案。关于 YAML 的转换,有人可以提供帮助吗? :) - Phrogz
2
顺便提一下,我没有看到你的解决方案中需要 self 是一个 Array。你可以将该方法提升到 Enumerable 中,因为你只依赖于 self 响应 inject。 (顺便说一句,这也允许你在两个测试用例中摆脱 to_a。) - Jörg W Mittag
@akostadinov 这个问题将“相似”的值分组在一起。这个问题保留了原始数组的顺序,只是在某个边界处将值分开,并丢弃该值。 - Phrogz
@Phrogz,如果您查看Tapio Saarinen的答案,您将得到真正的好答案。您可以点赞,因为这是最好的答案。 - akostadinov
@akostadinov 不,你仍然没有理解这个问题和那个问题之间的区别。Tapio的答案并不能满足我问题的需求。请再次查看我的示例输入和输出。 - Phrogz
显示剩余5条评论
5个回答

18
有时候,partition是解决这类问题的好方法。
(1..6).partition { |v| v.even? } 
#=> [[2, 4, 6], [1, 3, 5]]

与问题无关:作者想要拆分分隔的运行序列。 - Lloeki

13

我试图把它压缩一下,但仍然没有一个方法:

(1..9).chunk{|i|i%3==0}.reject{|sep,ans| sep}.map{|sep,ans| ans}

或者更快:

(1..9).chunk{|i|i%3==0 || nil}.map{|sep,ans| sep&&ans}.compact
此外,Enumerable#chunk 似乎是 Ruby 1.9+ 版本才有的,但它非常接近你所需的功能。
例如,原始输出将是:
(1..9).chunk{ |i|i%3==0 }.to_a                                       
=> [[false, [1, 2]], [true, [3]], [false, [4, 5]], [true, [6]], [false, [7, 8]], [true, [9]]]

(to_a 是为了让 irb 输出更好的结果,因为 chunk 返回的是一个枚举器而不是数组)


编辑: 请注意上述优美的解决方案比最快的实现慢2-3倍:

module Enumerable
  def split_by
    result = [a=[]]
    each{ |o| yield(o) ? (result << a=[]) : (a << o) }
    result.pop if a.empty?
    result
  end
end

不错!我以前没见过chunk。记录一下,它是1.9.2+版本才有的,但对我来说完全可以接受。 - Phrogz
2
这是文档的链接:http://ruby-doc.org/core/classes/Enumerable.html#M001523 - Mike Tunnicliffe
由于reject/map需要额外的迭代,所以很自然地,chunk 要慢得多;我已经添加了一个基准测试答案来收集实现。 - Phrogz
(1..10).chunk{|n| n % 3 == 0 ? :_separator : :keep}.map{|_,v| v} - SwiftMango
(1..10).chuck{|n| n%3==0 || nil}.map{|_,v| v} - Mike Tunnicliffe
但是,“最快的实现”是不正确的:给它(3..9),你会得到一个前导[]。将[3, 6, 9]作为输入,它会给你返回[[], []]result.pop if a.empty?是错误/无用的:你必须result.reject!(&:empty?),需要第二次遍历,并实现与compact类似的效果,可能使你回到2x/3x因子范围。 - Lloeki

5
以下是聚合答案的基准测试结果(我不会接受这个答案):
require 'benchmark'
a = *(1..5000); N = 1000
Benchmark.bmbm do |x|
  %w[ split_with_inject split_with_inject_no_tap split_with_each
      split_with_chunk split_with_chunk2 split_with_chunk3 ].each do |method|
    x.report( method ){ N.times{ a.send(method){ |i| i%3==0 || i%5==0 } } }
  end
end
#=>                                user     system      total        real
#=> split_with_inject          1.857000   0.015000   1.872000 (  1.879188)
#=> split_with_inject_no_tap   1.357000   0.000000   1.357000 (  1.353135)
#=> split_with_each            1.123000   0.000000   1.123000 (  1.123113)
#=> split_with_chunk           3.962000   0.000000   3.962000 (  3.984398)
#=> split_with_chunk2          3.682000   0.000000   3.682000 (  3.687369)
#=> split_with_chunk3          2.278000   0.000000   2.278000 (  2.281228)

正在测试的实现(基于Ruby 1.9.2):

class Array
  def split_with_inject
    inject([[]]) do |a,v|
      a.tap{ yield(v) ? (a << []) : (a.last << v) }
    end.tap{ |a| a.pop if a.last.empty? }
  end

  def split_with_inject_no_tap
    result = inject([[]]) do |a,v|
      yield(v) ? (a << []) : (a.last << v)
      a
    end
    result.pop if result.last.empty?
    result
  end

  def split_with_each
    result = [a=[]]
    each{ |o| yield(o) ? (result << a=[]) : (a << o) }
    result.pop if a.empty?
    result
  end

  def split_with_chunk
    chunk{ |o| !!yield(o) }.reject{ |b,a| b }.map{ |b,a| a }
  end

  def split_with_chunk2
    chunk{ |o| !!yield(o) }.map{ |b,a| b ? nil : a }.compact
  end

  def split_with_chunk3
    chunk{ |o| yield(o) || nil }.map{ |b,a| b && a }.compact
  end
end

有点晚了,但是:这些方法并不完全可比,因为这些方法的结果并不完全相同。前三个返回类似于String#split的结果(包括在找到两个连续分隔符时返回空数组),而split_with_chunksplit_with_chunk2从不返回空数组,而split_with_chunk3仍然包含块的“分组”值。 - Confusion

1

其他你可能想考虑的Enumerable方法是each_sliceeach_cons

我不知道你想要多普遍的方式,这是一种方法

>> (1..9).each_slice(3) {|a| p a.size>1?a[0..-2]:a}
[1, 2]
[4, 5]
[7, 8]
=> nil
>> (1..10).each_slice(3) {|a| p a.size>1?a[0..-2]:a}
[1, 2]
[4, 5]
[7, 8]
[10]

仅适用于我的特定模3示例,而不是通用的。 - Phrogz

1

这里还有一个(与最快的split_with_each进行比较的基准测试,链接在这里https://dev59.com/Qm445IYBdhLWcg3wkLFG#4801483):

require 'benchmark'

class Array
  def split_with_each
    result = [a=[]]
    each{ |o| yield(o) ? (result << a=[]) : (a << o) }
    result.pop if a.empty?
    result
  end

  def split_with_each_2
    u, v = [], []
    each{ |x| (yield x) ? (u << x) : (v << x) }
    [u, v]
  end
end

a = *(1..5000); N = 1000
Benchmark.bmbm do |x|
  %w[ split_with_each split_with_each_2 ].each do |method|
    x.report( method ){ N.times{ a.send(method){ |i| i%3==0 || i%5==0 } } }
  end
end

                        user     system      total        real
split_with_each     2.730000   0.000000   2.730000 (  2.742135)
split_with_each_2   2.270000   0.040000   2.310000 (  2.309600)

这类似于Array#partition,而不是String#split。 - Lloeki

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接