[#38392] Enumerable#gather_each — Tanaka Akira <akr@...>

ときに、複数行をまとめて扱いたいことがあります。

47 messages 2009/05/09
[#38394] Re: Enumerable#gather_each — ujihisa <ujihisa@...> 2009/05/09

ujihisaと申します。

[#38400] Re: Enumerable#gather_each — Yukihiro Matsumoto <matz@...> 2009/05/09

まつもと ゆきひろです

[#38399] Re: Enumerable#gather_each — "Akinori MUSHA" <knu@...> 2009/05/09

At Sat, 9 May 2009 15:30:20 +0900,

[#38405] Re: Enumerable#gather_each — Tanaka Akira <akr@...> 2009/05/10

In article <86r5yy2nrg.knu@iDaemons.org>,

[#38417] Re: Enumerable#gather_each — "Akinori MUSHA" <knu@...> 2009/05/10

At Sun, 10 May 2009 10:08:47 +0900,

[#38524] [Bug #1503] -Kuをつけた時、/[#{s}]/n と Regexp.new("[#{s}]",nil,"n") で実行結果が異なる — sinnichi eguchi <redmine@...>

Bug #1503: -Kuをつけた時、/[#{s}]/n と Regexp.new("[#{s}]",nil,"n") で実行結果が異なる

8 messages 2009/05/22

[ruby-dev:38392] Enumerable#gather_each

From: Tanaka Akira <akr@...>
Date: 2009-05-09 06:30:20 UTC
List: ruby-dev #38392
ときに、複数行をまとめて扱いたいことがあります。

たとえば、RD でインデントされた部分を取り出すとか、IO のパラ
グラフモードっぽいこととか、個人的には最近 chkbuild でログの
一部をソートするという必要がありました。

しかし、Enumerable でそういうまとまりを扱うメソッドは
each_slice しかなくて、柔軟なことは出来ません。

ファイルを全部読み込んで正規表現でやるというのはひとつの案で
すが、ファイルがとても大きいかもしれないとなるとやりたくない
ことがあります。
(実際 chkbuild のログはとても大きくなることがあります)

では自分で書くか、というと、これがなかなかきれいにかけません。
例えば、インデントされた部分を取り出すにはその直後のインデン
トされていない部分まで読まないとまとまりが判断できなくて、
IO#each_line で書こうとするとけっこう面倒です。ひとつの行が
インデントされているかどうかを調べるのは行頭が空白かどうかを
調べるだけで簡単にできるのですが、その結果に従って連続した行
をまとめるのが厄介です。

ここで、インデントされているかどうかとか、パラグラフだったら
空行かどうかとか、個々の要素を分類するところは問題によって異
なるのですが、その後の分類結果にしたがって連続した要素をまとめ
るのは共通しているので、Enumerable にメソッドとしてあっても
いいんじゃないかと思います。

というわけで、Enumerable#gather_each(arg) {|ary| ... } の提
案です。引数には Proc を与えて、これは各要素について呼ばれ、
結果として分類結果を返します。gather_each はその分類結果が等
しい連続した要素を配列としてまとめて yield します。

なお、分類結果が nil というのは特別扱いで、その要素は単独で
まとまりとなることを示します。

Ruby での実装を以下に示します。

module Enumerable
  def gather_each(arg)
    prev_value = prev_elts = nil
    self.each {|e|
      v = arg.call(e)
      if prev_value == nil
        if v == nil
          yield [e]
        else
          prev_value = v
          prev_elts = [e]
        end
      else
        if v == nil
          yield prev_elts
          yield [e]
          prev_value = prev_elts = nil
        elsif prev_value == v
          prev_elts << e
        else
          yield prev_elts
          prev_value = v
          prev_elts = [e]
        end
      end
    }
    if prev_value != nil
      yield prev_elts
    end
  end
end

たとえば、lib/scanf.rb には RD なドキュメントが入っていて、
以下のようにするとインデントされたコード例の部分をひとつにま
とめることができます。

arg = lambda {|l| /\A\=~ l ? true : nil }
open("lib/scanf.rb") {|f|                                                        
  f.gather_each(arg) {|lines| pp lines }                   
}
=>
["# scanf for Ruby\n"]
["#\n"]
["# $Release Version: 1.1.2 $\n"]
...
["the return array (or yielded to the block, if a block was
given).\n"]
["\n", "\n"]
["==Basic usage\n"]
["\n",
 "   require 'scanf.rb'\n",
 "\n",
 "   # String#scanf and IO#scanf take a single argument (a
 format string)\n",
 "   array = aString.scanf(\"%d%s\")\n",
 "   array = anIO.scanf(\"%d%s\")\n",
 "\n",
 "   # Kernel#scanf reads from STDIN\n",
 "   array = scanf(\"%d%s\")\n",
 "\n"]
["==Block usage\n"]
["\n"]
...

パラグラフモードっぽく、空行で区切られた部分をまとめたいなら
以下のようにできます。

arg = lambda {|l| l == "\n" }       
open("lib/scanf.rb") {|f|
  f.gather_each(arg) {|lines| pp lines }
}
=>
["# scanf for Ruby\n",
 "#\n",
 "# $Release Version: 1.1.2 $\n",
 "# $Revision: 22784 $\n",
 "# $Id: scanf.rb 22784 2009-03-06 03:56:38Z nobu $\n",
 "# $Author: nobu $\n",
 "#\n",
 "# A product of the Austin Ruby Codefest (Austin, Texas,
 August 2002)\n"]
["\n"]
["=begin\n"]
["\n"]
["=scanf for Ruby\n"]
["\n"]
["==Description\n"]
["\n"]
["scanf for Ruby is an implementation of the C function
scanf(3),\n",
 "modified as necessary for Ruby compatibility.\n"]
["\n"]
...

どうでしょう?

なお、複数行をまとめる方法として使いそうなものは、まとめる先
頭要素を検出する方法を指定するとか、他にもいくつかあるように
思います。たとえば ChangeLog や mbox を扱うのには、先頭要素
を指定するのがいいでしょう。そういうものはまた別のメソッドと
して作るのがいいのではないかと思います。
-- 
[田中 哲][たなか あきら][Tanaka Akira]

In This Thread

Prev Next