再帰を用いた括弧の処理


化学式の計算
括弧付きの化学式の式量を計算する良いスクリプトのアイディアが浮かばなかったが、再帰を用いると、比較的シンプルに書けることに気がついた。単純な化学式は、以前考えたが、

def fmlwt(fml)
  awt=Hash[*IO.read("formula.dat").strip.split(/\s+/m)]
  r=0
  fml.scan(/([A-Z][a-z]?)([\d\.]*)/){|a,n|
    n=1 if n==""
    r+=awt[a].to_f*n.to_f
    }
  r
end

のような感じで書ける。この原子量の代わりに、括弧の中の化学式に関しては自分を再帰的に呼び出すことによって、

def fmlwt(fml)
  awt=Hash[*IO.read("formula.dat").strip.split(/\s+/m)]
  r=0
  fml.scan(/(\(.*?\)|[A-Z][a-z]?)([\d\.]*)/){|a,n|
    n=1 if n==""
    a=a.sub(/^\(/){""}.sub(/\)$/){""}
    r+=((a=~/^[A-Z][a-z]?$/)?awt[a].to_f : fmlwt(a))*n.to_f
    }
  r
end

と書くと、特定の括弧の付け方に対応できるようになる。Cu(NO3)2(H2O)2.5などはうまく判定できる。しかし、(Cu(NH3)4)Cl2はうまくいかない。scanの部分の正規表現の.*?の?を取ればこれに対応するが、今度は前者に対応できなくなる。この両者をシンプルに判定するにはどうしたら良いのだろう。

2011/2/1追記 汚い方法だが、括弧の中を内側から、式量を計算して、それを水素に置き換えていくという方法で、いろいろな場合の括弧に対応できた。

def fmlwt(fml)
  awt=Hash[*IO.read("formula.dat").strip.split(/\s+/m)]
  fml=fml.sub(/[\(\[]([^\(\)\[\]]*)[\)\]]([\d\.]*)/){
    n=($2=="")?1:$2.to_f;"H%f"%(fmlwt($1)*n/awt["H"].to_f)} while fml=~/[\(\[]/
  r=0
  fml.scan(/([A-Z][a-z]?)([\d\.]*)/){|a,n|
    n=1 if n==""
    r+=awt[a].to_f*n.to_f
    }
  (r*1e3).round/1e3
end

しかし、やり方が汚すぎる。置換の部分を少し工夫したら、もう少し短くなった。

def fmlwt(fml)
  awt=Hash[*IO.read("formula.dat").strip.split(/\s+/m)]
  re=/[\(\[]([^\(\)\[\]]*)[\)\]]([\d\.]*)/
  fml=$`+"H%f"%(fmlwt($1)*($2==""?1:$2.to_f)/awt["H"].to_f)+$' while fml=~re
  r=0
  fml.scan(/([A-Z][a-z]?)([\d\.]*)/){|a,n| r+=awt[a].to_f*(n==""?1:n.to_f)}
  (r*1e3).round/1e3
end