RubyのGsubチェーンはイケてない? ~ GsubFilterの紹介

任意のテキストに対して複数の置換を実行したい
ってときあるよね
そんなときRubyでは普通
String#subあるいは#gsubメソッドをチェーンするよ

def replace(text)
  text.gsub(/\w+/) { |m| m.capitalize }
      .sub(/ruby/i) { |m| "*#{m}*" } 
      .gsub(/a(.)/) { "a-#{$1}" }
end

text =<<EOS
ruby is a fantastic language!
I love ruby.
EOS

puts replace(text)

# >> *Ruby* Is A Fa-nta-stic La-ngua-ge!
# >> I Love Ruby.

でもこのやり方には次のような問題があるよ

  1. 置換の数が多くなると、コードが読みづらくなる。
  2. 置換の条件がメソッドにハードコードされているので、後から条件を変えたり追加したりできない。

ちょうど今書いてるコードで
単一のテキストに対して14もの置換が必要になったから
上の問題が気になったんだよ


そんなわけで...

上記問題を解消するGsubFilterというクラスを書いてみたよ!


GsubFilterは次のように
複数のfilterを登録してからrunで置換を実行するよ

require "gsub_filter"

gs = GsubFilter.new("ruby is a fantastic language!\nI love ruby.")

# 各単語をキャピタライズする。
gs.filter(/\w+/) {|md| md.to_s.capitalize }

# 最初の'ruby'だけにアスタリスクを付ける。
gs.filter(/ruby/i, global:false) { |md| "*#{md.to_s}*" }

# MatchDataオブジェクトがブロックの第1引数として渡される。
gs.filter(/a(.)/) { |md| "a-#{md[1]}" }

# runメソッドでこれらのフィルタを実行する。
gs.run # => "*Ruby* Is A Fa-nta-stic La-ngua-ge!\nI Love Ruby."


GsubFilter#runは
他のテキストを先のフィルタのために取ることができるよ

gs.run("hello, world of ruby!") # => "Hello, World Of *Ruby*!"


GsubFilter#replaceを使えば
各フィルタをあとから交換できるよ

gs.replace(1, /ruby/i) { |md| "###{md.to_s}##" }

gs.run # => "##Ruby## Is A Fa-nta-stic La-ngua-ge!\nI Love ##Ruby##."

またMatchDataオブジェクトは
フィルタブロックの第2引数を通してストックできて
これはGsubFilter#stocksで後からアクセスできるんだ

gs.filter(/#(\w+)#/) { |md, stocks| stocks[:lang] << md[1]; "+#{md[1]}+" }

gs.run # => "#+Ruby+# Is A Fa-nta-stic La-ngua-ge!\nI Love #+Ruby+#."
gs.stocks # => {:lang=>["Ruby", "Ruby"]}


まあ需要があるとは思えないけど
いままでgemを作ったことがなかったから
勉強を兼ねてこのクラスをgem化してみたよ!
gem i gsub_filterでインストールできるから
暇つぶしに遊んでくれたらうれしいよ


https://rubygems.org/gems/gsub_filter


Rubyの四則演算をもっと便利にしたいよ!

この間RubyのEnumerable#mapを便利にした
Enumerable#mappを紹介したよ


RubyのEnumerable#mapをもっと便利にしたいよ - hp12c

module Enumerable
  def mapp(op=nil, *args, &blk)
    op ? map { |e| op.intern.to_proc[e, *args]} : map(&blk)
  end
end

langs = ["Ruby", "Python", "Lisp", "Haskell"]
langs.mapp(:+, 'ist') # => ["Rubyist", "Pythonist", "Lispist", "Haskellist"]

[1, 2, 3].mapp(:+, 10) # => [11, 12, 13]

(1..5).mapp(:**, 2) # => [1, 4, 9, 16, 25]

[[1,2,3,4], [5,6,7,8], [9,10,11,12]].mapp(:last, 2) # => [[3, 4], [7, 8], [11, 12]]

["ruby", "python", "lisp", "haskell"].mapp(:[], -2, 2) # => ["by", "on", "sp", "ll"]


で今日またこれに関連して
別のアイディアを思いついたよ
それはここまで来たらもうmappは
要らないんじゃないかってことなんだ


つまり四則演算に限定されちゃうけど
以下の構文でmapp相当のことができたら
面白いと思ったんだ

%w(Ruby Python Lisp Haskel) + 'ist' # => ["Rubyist", "Pythonist", "Lispist", "Haskelist"]

[1, 2, 3, 4] + 10 # => [11, 12, 13, 14]
[1, 2, 3, 4] + 5.5 # => [6.5, 7.5, 8.5, 9.5]
[1, 2, 3, 4] - 10 # => [-9, -8, -7, -6]
[1, 2, 3, 4] * 2 # => [2, 4, 6, 8]
[1, 2, 3, 4] / 2.0 # => [0.5, 1.0, 1.5, 2.0]
[1, 2, 3, 4] ** 2 # => [1, 4, 9, 16]

割と違和感がないと思うんだけどどうかな?


実装はこんな感じだよ

class Array
  {:+ => :plus, :- => :minus, :* => :multi}.each do |op, rep|
    alias :"__#{rep}__" :"#{op}"
    define_method(op) do |other|
      case other
      when String, Numeric
        map { |elm| elm.send(op, other) }
      else
        self.send("__#{rep}__", other)
      end
    end
  end

  %w(/ **).each do |op|
    define_method(op) do |other|
      map { |elm| elm.send(op, other) }
    end
  end
end

まあ元のArray#*(Integer)が死んじゃうんだけど..


ちなみにネストした配列でも有効だよ

[[1,2],[3,4]] ** 2 # => [[1, 4], [9, 16]]
[%w(child neighbor), %w(brother false)] + 'hood' # => [["childhood", "neighborhood"], ["brotherhood", "falsehood"]]


ついでに逆も定義してみるよ

class String
  alias :__plus__ :+
  def +(other)
    case other
    when Array
      other.map { |o| self + o }
    else
      self.__plus__(other)
    end
  end
end

'ruby' + 'ist' # => "rubyist"
'ruby' + ['ist', 'er', 'mate'] # => ["rubyist", "rubyer", "rubymate"]


まあネタ止まりかな..

RubyでLevenshtein Distanceを解く-CodeEval

できません..
アルゴリズム的にはできてるんだけど*1
答えを得るのに1時間とかorz..
5秒で答えなきゃいけないのに
あとグローバル変数を使ってしまった


どうも高速化は苦手です
そこに注力する気がなかなか起きない..


レーベンシュタイン距離が1の語同士をfriendとして
与えられた辞書におけるhelloの語から始まる
friendの輪に含まれるすべての語の数を答える

*1:'causes'の解答が一致した