1. Ping
Changes in tags
Changes in body
Source | HTML | Preview

Ruby でフリップフロップ

孫の手や歯科医院のカードなど、まれに必要になるけど探してもなかなか見つけられないものがある。
Ruby のフリップフロップの例などもそうかもしれない。なので、未来の自分のためにメモ。

フリップフロップとは

プログラム用語でいえば、(条件A)~(条件B)の間かどうかを検査するロジック、と言える。古くはAWKやsedなどが実装していたそうだが、PerlやRubyにも実装されている演算機能だ。

ワンライナーで入力テキストの行抽出に用いられたりするシーンが想像しやすいかもしれない。実際そういったテキスト抽出に活用できる。

各コマンドの行の条件範囲抽出の例
# Ruby プログラム中の =begin ~ =end のコメント部分を抽出する
$ cat yourfile.rb | perl -ne 'print if /^=begin/../^=end/'
$ cat yourfile.rb | ruby -ne 'print if /^=begin/../^=end/'
$ cat yourfile.rb | awk '/^=begin/,/^=end/'
$ cat yourfile.rb | sed -n '/^=begin/,/^=end/p'

# 特定の時間帯のログを抽出する
$ cat httpd_access.log | perl -ne 'print if /12:\d\d:\d\d/../15:\d\d:\d\d/'
$ cat httpd_access.log | ruby -ne 'print if /12:\d\d:\d\d/../15:\d\d:\d\d/'
# awkの正規表現は\dが無い…
$ cat httpd_access.log | awk '/12:[0-5][0-9]:[0-5][0-9]/,/15:[0-5][0-9]:[0-5][0-9]/'
# sedの正規表現はだいたいawkと同じ。-n と p を忘れずに
$ cat httpd_access.log | sed -n '/12:[0-5][0-9]:[0-5][0-9]/,/15:[0-5][0-9]:[0-5][0-9]/p'

実行系(プログラムを動かす側)の内部で、“(条件A)が真になったか” と “(条件B)が真になったか”、 を自動的に記憶している。それら2つの真偽値の判定条件と結果の組み合わせによって、返す値がパタパタというかギッコンバッタンというか変わるので、フリップフロップというとかなんとか。

プログラマ側からしてとっつきにくいのか、情報や活用事例があまりない。たかが式なのに、以前の状態を記憶しながら真偽判定するという、風変わりで分かりにくい演算式なためだろうか。別な言い方をすれば(Rubyのマニュアルが分かりやすいが式自身が状態を記憶している、といっても良いだろう。この「書いてもないのに誰か勝手に状態を記憶していて判定処理をしてくれる」というもやもやさが、今一つ活用されていない理由かもしれない。

実例コード

ワンライナーじゃない場合のサンプルコード。やってることはそんな難しくない。

Ruby
# (ex 1) html ドキュメント中の <body> タグの中身を行単位で抜き出す
response_body.split(/\r\n|\n/).each do |line|
  bodylines.push(line) if (line=~/<body/)..(line=~/<\/body>/)
end

# (ex 2) メールの生データ(SMTP)から本文部分を抜き出し
File.open('sample.txt') do |file|
  file.each_line do |line|
    print line if (line=~/^$/)..(line=~/^\.$/)
  end
end

ちなみに、Perlだとこう。終端判定にeof()を使ってもいいだろう。

Perl
# (ex 1)
foreach ( split /\r\n|\n/, $response_body ){
    push @bodyline, $_ if /<body/../<\/body>/;
}

# (ex 2) *utf8処理は割愛
open(FILE, '< sample.txt');
while (<FILE>){
  print if /^$/../^\.$/;
}
close(FILE);

.. と ... の違い

このピリオド2つと3つの違いは、具体的にRubyのマニュアルにも書いてある。
もう少し噛み砕くと、ややこしいが、(条件A)と(条件B)が異なった条件の場合は.....の振る舞いに違いはなく、同じ条件式であった場合に違いが生じる。この振る舞いは Ruby も Perl も同じだ。

  • (条件X)..(条件X)の場合は、(条件X)の該当値だけが真になる。
  • (条件X)...(条件X)の場合は、該当値〜次の該当値までが真になる。(該当箇所が1か所しかなければ、そこから最後まで)

前者は他の判断式で簡単に記述できるので使われることはないだろう。

後者では、フラグ値などを変数に持たずして特定の行以降を取得する、といったことができそうだ。
また、特定の区切り子に囲まれた部分を抽出する、ということができるので、Qiitaのようなマークダウン中の```に囲まれたコードブロックを抽出する、といったことが可能だ。
以下、sample.mdからコードブロックを抽出する例。

sample.md
    This is print sample.
    ```
    print "The quick brown fox jumps over the lazy dog"
   ```

    And this is Hear-Document sample.
    ```
    print(<<EOT) if no == 42
    the Answer to the Ultimate Question of Life, the Universe, and Everything
    EOT
    ```
抽出の実行と結果
    $ ruby -ne 'print if /^```/.../^```/' sample.md
    ```
    print "The quick brown fox jumps over the lazy dog"
    ```
    ```
    print(<<EOT) if no == 42
    the Answer to the Ultimate Question of Life, the Universe, and Everything
    EOT
    ```

※ Qiitaのブロックコード中の中に、``` を置くために各行インデントしました。

おわりに

と、説明してみたものの、やっぱり小難しいのと知名度が低いため、あまり使いどころは無いかもしれないなぁ。他の人が知らなければコードインスペクションでも問題になりそうだ。

追記

2016-3-14

@scivola さんが、本記事を「Ruby のココがダメ」で追記で引用してくれました (Thanks!)。
また、別稿「Ruby のフリップフロップの闇感と Enumerable#flipflop」を投稿しました。ご参照あれ。

2016-7-25

@HMMNRST さんが、本記事を「条件式では特殊な動作をするRubyリテラル」で引用してくれました(Thanks!)。ご参照あれ。