「topcoderの道1」をGolfで

これも実は2009-06-06 - uokumuraの日記する前にやっていたのだけれど、こっちはやる前にコードを見ちゃったのと、野球見ながらやっていたので時間が計れてないのです。
で、縮めれそうなのでGolfの方向でネタにする事にして縮めてみたら、(指定されてる範囲で)仕様を守りつつ70文字(本体63文字)までは縮める事が出来ました。

topcoderの道1|プログラミングに自信があるやつこい!!

与えられた英語の大文字で構成された文字列の中の文字を、与えられた数字の分だけ左にシフトさせなさい。たとえば、’C’を2つ左にシフトさせると’A’、’Z’を2つ左にシフトさせると’X’。
与えられる英語の文字列はAからZで、Aの次はZにシフトさせるものとする。

という問題で、見た答えはゲンゾウ用ポストイット: 続・topcoderの道1をといてみる

class CCipher{  
    final def ALPHA_LIST = 'A'..'Z'  
    String decode(String ciphertest, int shift){  
        ciphertest.collect{  
            ALPHA_LIST[ALPHA_LIST.indexOf(it) - shift]  
        }.join()  
    }  
}  

groovyだとインデックスがマイナスなら後ろから見るっていう特徴をうまく使った、エレガントなお答え。
「対象文字列の各文字について、アルファベット上でshift字前の文字を集める」なんてまさしく仕様を素直にコードに落とせています。
これ以上する事も無いのですが、
Groovyで文字処理を書くときの心得 - uehaj's blog

Cだと例えば

char ch;
char converted = (ch-'A'-shift) % 26 + 'A';

みたいに良くやるやりかたでGroovyを書こうとすると死ぬる。えーとCharacter.valueOf(ch.charAt(0) as char)・・・とか、全然簡潔じゃないし!!

なんて読んじゃえばそのいばらの道に踏み込みたくなるのが人情というもの。で、やってみると結構簡潔になったのです。Golfできるくらいに。
で、出来たのが以下のコード。

decode={s,n,Z=(int)'Z'->s.collect{(char)(Z+((int)it-Z-n)%26)}.join()}

Aを起点にすると符号が変わってしまって剰余だけではうまく動かないので、Zの何文字前かを調べて、そのさらにn文字前、と必ず負の値になるように仕掛けています。ちなみに、'Z'やitをchar型でなくint型にキャストしているのはGolfしたくなったからです。せこく2文字稼ぎました^^;
ファイル名をCCipher.groovyにすれば指定通りのメソッドが70文字のファイルで定義できる事になります(正確にはdecodeフィールドに置かれたクロージャですが、それはメソッドと見なすのがgroovy流なので…)。引数名は指定と違いますが、まぁそこはIFに現れないので実装上の詳細という事で(Golfしたかったのです)。

ひとまず例をそのままassert文にすれば

assert decode("VQREQFGT", 2) == "TOPCODER":decode("VQREQFGT", 2)
assert decode("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 10) == "QRSTUVWXYZABCDEFGHIJKLMNOP":decode("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 10)
assert decode("TOPCODER", 0) == "TOPCODER"
assert decode("LIPPSASVPH", 4) == "HELLOWORLD"

で、これはばっちり通ります。境界例をテストしてみても…

assert decode("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 26) == "ABCDEFGHIJKLMNOPQRSTUVWXYZ":decode("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 26)
assert decode("BCDEFGHIJKLMNOPQRSTUVWXYZA", 27) == "ABCDEFGHIJKLMNOPQRSTUVWXYZ":decode("BCDEFGHIJKLMNOPQRSTUVWXYZA", 27)

ばっちり。また、仕様では指定されていない以下のような機能まで付加する事が出来ました。

assert decode("vqreqfgt",2,(int)'z') == "topcoder":decode("vqreqfgt", 2,(int)'z')
assert decode("VQREQFGT",2,(int)'Z') == "TOPCODER":decode("VQREQFGT",2,(int)'Z')

が、さすがにshiftに負の数値を指定すると動きません。

assert decode("QRSTUVWXYZABCDEFGHIJKLMNOP", -10) == "ABCDEFGHIJKLMNOPQRSTUVWXYZ":decode("QRSTUVWXYZABCDEFGHIJKLMNOP", -10)

これはエラーになってしまいます。まぁ負の場合後ろへシフトするように作っても

decode={s,n,O=(int)(n<0?'A':'Z')->s.collect{(char)(O+((int)it-O-n)%26)}.join()}

と起点を表すデフォルト引数(Zとは限らなくなったのでORIGINでOとしました)を3項演算で取れば良いだけなんですけど、Golfにこだわっちゃいました;p
まぁ、Golfしないなら

String decode(String ciphertest ,int shift) {
    def origin = (char) (shift<0 ? 'A' : 'Z');
    ciphertest.collect {
        (char) (origin + ((char) it - origin - shift) % 26)
    }.join()
}

て感じでしょうか。なんともトリッキーですねぇ…
たしかに、「Groovyで文字の絡む処理はC言語的にやったら負け」なようです。