From: Tanaka Akira Date: 2009-05-09T15:30:20+09:00 Subject: [ruby-dev:38392] Enumerable#gather_each ときに、複数行をまとめて扱いたいことがあります。 たとえば、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]