What is it, naokirin?

F#でTDDBC福岡の課題をやってみた(1)

TDDBC終わって既に3日目なんですね、早いものです。

TDDBCでは課題として、「ScreenName\tTweet」のような形になっているツイートのデータを受け取って、与えられた仕様に基づいて処理する、という感じでした。

実際には10個の仕様があったのですが、今回はそのうち5個までを終わらせました。
TDDBCでは時間制限もあるので、やはり言語にある程度慣れていないとリファクタリングまで手が回りにくいというのがあったので、今回は少しゆとりを持って作業しました。ちなみに今回やった仕様の5個というのが

1. 普通のツイートを判定
  普通のツイートの場合に「Normal\tTweet」にして返す


2. ハッシュタグを含むツイートを判定
  ツイートにハッシュタグが含まれる場合、「HashTag\tTweet」にして返す


3. リプライのツイートを判定
  ツイートがリプライの場合に「Reply\tTweet」にして返す


4. メンションを含むツイートを判定
  ツイートにメンションが含まれる場合に「Mention\tTweet」にして返す


5. 複数の種類を含むツイートを判定
  ツイートにリプライ、ハッシュタグ、メンションといった種類のうち、2つ以上を含むものをたとえば「HashTag,Reply,Mention\tTweet」のようにして返す

でした。

できたコードがこちら。

open NUnit.Framework
open FsUnit
open System
open System.Text.RegularExpressions

// ハッシュかどうかを判定
let is_hash str = 
  Regex.IsMatch(str, ".+ +#[a-zA-Z0-9]+") || Regex.IsMatch(str, "^#[a-zA-Z0-9]+")

// リプライかどうかを判定
let is_reply str =
  Regex.IsMatch(str, "^@[a-zA-Z0-9_]+")

// メンションかどうかを判定
let is_mention str =
  Regex.IsMatch(str, "@[a-zA-Z0-9_]+")

// ノーマルかどうかを判定
let is_normal str =
  Operators.not((is_hash str) || (is_reply str) || (is_mention str))

// 判定関数とその場合のタグ名の組を持つリストを返す
let set_list _ = 
  [(is_hash, "HashTag"); (is_reply, "Reply"); (is_mention, "Mention"); (is_normal, "Normal")]

// 引数で渡されたツイートにつけるタグをリストにして返す
let tag str = 
  List.filter (fun x -> Operators.not(x.Equals "")) 
              (List.map (fun (func,name) -> if (func str) then name else "") (set_list()))

// 適切なタグを付けて、ツイートの内容を返す
let categorize tweet =
  match Array.toList (Regex.Split(tweet, "\t")) with
  | _::rest -> 
    let str = String.concat "" rest in
    (String.concat "," (tag str)) + "\t" + str
  | _ -> failwith "tweet data parsing is fail!"


[<TestFixture>]
type categorizeのテスト ()=
  [<TestCase("bleis\tあいうえお", "Normal\tあいうえお")>]
  [<TestCase("naoki_rin\tかきくけこ", "Normal\tかきくけこ")>]
  [<TestCase("naoki_rin\tあいうえお#tddbc", "Normal\tあいうえお#tddbc")>]
  [<TestCase("bleis\tあいうえお #tddbc", "HashTag\tあいうえお #tddbc")>]
  [<TestCase("bleis\t#tddbc", "HashTag\t#tddbc")>]
  [<TestCase("bleis\t@naoki_rin あいうえお", "Reply,Mention\t@naoki_rin あいうえお")>]
  [<TestCase("bleis\tあいうえお @naoki_rin", "Mention\tあいうえお @naoki_rin")>]
  [<TestCase("bleis\t@naoki_rin あいうえお @pocketberserker かきくけこ #tddbc", "HashTag,Reply,Mention\t@naoki_rin あいうえお @pocketberserker かきくけこ #tddbc")>]
  member this.投稿者名を消して適切なタグをを追加する (tweet: string, expected: string)=
    categorize tweet |> should equal expected

ちなみにF#を今回書きましたが、ほとんどが勘で書いてます。OCamlと構文的に近いので基本構文はOCamlのときと同じ気分で書いて、あとは「こんな関数、多分あるはず」と言った感じでやってました。実際にTDDBCの一日目の夜がはじめてまともにF#のコードを書いたときで、今日が二回目と言った感じです。おそらく、コードとしてはよいコードとは言えないと思います。

ちなみにTDDBCでid:bleis-tiftさんに「VsVim入ってないの?」と言われてしまったので、入れてみました。さらになんとなくItaBackgroundImageというのを入れてみました。
結果↓

なーんか見覚えのあるマークが(色違いで)ついてますが、気にしない気にしない。

追記:
ちなみに"Normal"のタグをつける場合のみ、ちょっと特殊なのでどのようにするか悩んだのですが、やはりできれば変更点を小さくしたいので、関数内に"Normal"のタグをつける場合は組み込んでしまうことにしました。

// 判定関数とその場合のタグ名の組を持つリストを返す
let set_list _ = 
    [(is_hash, "HashTag"); (is_reply, "Reply"); (is_mention, "Mention")]

// 引数で渡されたツイートにつけるタグをリストにして返す
let tag str = 
  let lst = (List.filter (fun x -> Operators.not(x.Equals "")) 
                         (List.map (fun (func,name) -> if (func str) then name else "") (set_list()))) in 
  if (List.forall (fun x -> x = "") lst) then
    ["Normal"]
  else
    lst

ちょっと関数内がごちゃごちゃしてしまいますが、こうすると次に付けるタグが増えても判定用の関数とリストへの追加のみで済むようになります。んー、まだ満足できるコードからは程遠い…