LoginSignup
0
1

More than 5 years have passed since last update.

国民の祝日.csv の解析プログラムのScala版を頑張って作ってみた。

Last updated at Posted at 2017-03-26

はじめに

国民の祝日.csv が一ヶ月ほど前に話題になりました。
ここ2年ほど趣味で学習しているScalaで変換にチャレンジしてみました。
『ScalaがXXXにより優れている』とかの意図はまったくありません。プログラミング言語による実装の違いが純粋に面白かったので、この記事を書いてみました。
2017-03-2616-56-09.png

Ruby版を見てて思いついた実装方法がScalaのパーサー・コンビネーター(parser combinators)のライブラリを利用する方法です。
コップ本にも紹介されています。
単語が記載される法則を定義して、簡単に変数に変換してくれます。
今回のようなCSVの中にも一定の規則があれば、解析はすごく簡単です。

参考URL)

http://qiita.com/suin/items/35bc4afe618cb77f80f6
http://seratch.hatenablog.jp/entry/20111010/1318254084

また、最終的に結果をJSONで返すようなアプリにしたかったので、Skinny Frameworkを利用しています。
が、結局はVue.jsを利用してのサイトになりました。(JSONも返しています)

それではコードの紹介をしたいと思います。

CSVの解析部分(1)

最初に、引数にCSVの文字列を受け取って、結果を返す部分を紹介します。以下のparseAll(syuku_csv_rule, target) で、CSVの記載規則(syuku_csv_rule)を第1引数で渡して、第2引数で変換対象の文字列も渡しています。

def parse(target: String): Either[String, List[SyukujitsuBody]] = parseAll(syuku_csv_rule, target) match {
  case Success(result, _) => Right(result.sortWith((a, b) => a.date.isBefore(b.date)))
  case Failure(msg, _) => Left(msg)
  case Error(msg, _) => Left(msg)
}

結果は、Either で返しています。Either は正常(Right)と異常(Left)と言った意味合いで結果を返す事ができます。Rightが英語の"正しい"と同じ意味なので、こういう名称なようです。
Rightには、変換結果(List[SyukujitsuBody])を日付でソートした結果を設定して返しています。(解析が失敗した)Leftの場合には、エラーメッセージのみです。

いきなり解析の関数のみを説明したのですが、これが"変換処理"の部分です。他にはCSVの記載ルールと変換結果を格納しているクラスのみを定義しているだけです。

変換結果のList[SyukujitsuBody]ですが、SyukujitsuBodyはcase classで定義しています。(ListはScalaのクラスです。)
祝日の名前(date_name)と日付(date)を保持します。

case class SyukujitsuBody(date_name: String, date: LocalDate)

CSVの解析部分(2)

あとは記載ルールです。記載ルールはBNF記法に似た手法で定義できます。
まずは、ヘッダー部分の2行を定義している箇所を紹介します。

private def head1 = """(平成|昭和|明治|大正).*,""".r
private def head2 = """名称,月日(,)?""".r
private def heads = rep(head1) ~ rep(head2)

本来はScalaの変数(val or var)で定義すべきかもしれませんが、関数(def)で宣言する必要があります。ひとつ目の"head1"は1行目のヘッダを表現しています。ダブルクォーテーション3つはraw stringと呼ばれるもので、\とか"とかのエスケープが必要な文字を直接記載できます。また、末尾の".r"は正規表現のインスタンスを返す表現です(そのため、"平成|昭和"とかの正規表現で記載)

そして"heads"が2行のヘッダを表現している部分です。rep(x)は繰り返しを表現しており、"x ~ y "(チルダ)は、xの後に yが来ることを表現しています。
これだけで、ヘッダの2行が表現できています。

次に祝日の行の解析部分です。 "syuku_name"と"syuku_date"とはheadと同様に正規表現を適用しています。この2つを結合しているところも"heads"と同様です(syuku_name ~ syuku_date)。
その後の ^^が、headと違うところです。

private def syuku_name = """.*?,""".r
private def syuku_date = """[\d]{4}/[\d]{1,2}/[\d]{1,2}""".r
private def syuku_name_and_date_split = syuku_name ~ syuku_date ^^
  {
    case name ~ date =>
      SyukujitsuBody(
        name.dropRight(1),
        LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy/M/d"))
      )
  }

{}で囲っている部分で、祝日名称(name)と日付(date)をcase classの"SyukujitsuBody"に変換しています。このように抽出した文字列を変数に直接代入できます。これを後の記載で、繰り返し抽出しListに格納した結果を返すようにしています。

private def syuku_csv_rule = heads ~> rep(syuku_name_and_date_split <~ opt(comma)) <~ rep(tail1 ~ opt(comma))

"~>" は"~"と似ていますが、右側(矢印の先)のみを抽出して、左側は解析するけど結果を捨てるようなイメージのルールです。
"<~" は逆バージョンで左側だけを残しています。opt(x)は存在したり、しなかったりです。前述済みですが、repは繰り返しを表現です。

以上で、List[SyukujitsuBody]として結果を返すことができます。

少しズルした気分になりますが、このような簡潔な表現で文字列を解析して、プログラミングで扱いやすい変数に格納することができるのが、Scalaの魅力です。

再帰処理(しかも末尾再帰)

一旦、List[SyukujitsuBody]に格納したのですが、私も"年"のハッシュに変換してみたいと思いました。(直接格納する方法が思い浮かばなかっただけです)

最初に思いつたのが、for文版で、色々考えてfoldLeft版、再帰(末尾再帰)版を実装してみました。

まずは、for文版です。一時変数を2つ宣言して、ループを回して存在した場合と、しない場合を分岐しての至って普通の実装方法です。

def convertYearMonthMap_for(lst: List[SyukujitsuBody]): SortedMap[Int, SortedMap[LocalDate, String]] = {
  var ret = SortedMap.empty[Int, SortedMap[LocalDate, String]]
  var tmp = SortedMap.empty[LocalDate, String]

  for (itm <- lst) {
    if (ret.isDefinedAt(itm.date.getYear)) {
      tmp = ret.get(itm.date.getYear).get
      if (tmp.isDefinedAt(itm.date)) {
        tmp.updated(itm.date, itm.date_name)
      } else {
        tmp = tmp + (itm.date -> itm.date_name)
        ret = ret.updated(itm.date.getYear, tmp)
      }
    } else {
      tmp = SortedMap.empty[LocalDate, String]
      tmp = tmp + (itm.date -> itm.date_name)
      ret = ret + (itm.date.getYear -> tmp)
    }
  }
  ret
}

forもJavaであれば普通なのかもしれませんが、Scalaらしくは無いと感じて色々考えました。

次に考えたfoldLeftですが、今まで一度も使ったことが無かったのですが、初めて実用的な使い方ができました。
初期値(空のSortedMap)を与えて、Listの要素を順番に適応していきます。都度、SortedMapに新規追加をしていき、そのSortedMapを返します。

def convertYearMonthMap_foldLeft(lst: List[SyukujitsuBody]): SortedMap[Int, SortedMap[LocalDate, String]] = {
  lst.foldLeft(SortedMap.empty[Int, SortedMap[LocalDate, String]]) { (r, itm) =>
    r.get(itm.date.getYear) match {
      case Some(smap_item) => {
        r.updated(
          itm.date.getYear,
          smap_item.get(itm.date) match {
            case Some(v) => smap_item.updated(itm.date, itm.date_name)
            case None => smap_item + (itm.date -> itm.date_name)
          }
        )
      }
      case None => {
        r.updated(itm.date.getYear, SortedMap(itm.date -> itm.date_name))
      }
    }
  }
}

foldLeft版でも、あまり短くならなかったので少しショックでした。更に色々思い出し、再帰で実装しました。
Listをhead(先頭要素)とtail(後続要素)に分割して、headをSortedMapに追加、tailとSortedMapを同じ処理に渡して、全ての要素に同様の変換を実施しています。

def convertYearMonthMap_reculsive(
    lst: List[SyukujitsuBody],
    mp: SortedMap[Int, SortedMap[LocalDate, String]] = SortedMap.empty[Int, SortedMap[LocalDate, String]]
  ): SortedMap[Int, SortedMap[LocalDate, String]] = lst match {
    case Nil => mp
    case head :: tail => {
      if (mp isDefinedAt (head.date.getYear)) {
        convertYearMonthMap_reculsive(tail, mp.updated(head.date.getYear, mp.apply(head.date.getYear) + (head.date -> head.date_name)))
      } else
        convertYearMonthMap_reculsive(tail, mp + (head.date.getYear -> SortedMap(head.date -> head.date_name)))
    }
  }

割と再帰処理で短く書けて満足です。ようやくScalaしの超初心者から、初心者と中級者の間に入れた気がしました。
更に、末尾再帰を実装するとIntellij IDEAの末尾再帰iconが左側に出るのを初めて知って小さい感動がありました。
もっと短いコードが書けるように頑張りたいと思います。

そしてVue.js

今回、SkinnyFrameworkにより実装したので、JavaScriptのライブラリを利用した何かを考えていました。当初、Angularを勉強していたのですが、手軽に組み込みにくい印象があり、採用はやめました。そんな中、Vue.jsのサイトを見てみると日本語の説明もあったので、簡単に試してみると数時間で実装することができました。

以下はhtml部分です。ほぼタグだけ。

<div id="app-simple">
    <h3>祝日リスト</h3>
    <a v-for="y in years" href="#" v-on:click="onFunc(y.year, $event)">
        <!-- <div v-bind:class="['btn_default', y.isActive ? 'btn_active': '']"> {{ y.year }} </div> -->
        <div v-if="y.year == select_year"><div class="btn_default btn_active"> {{ y.year }} </div></div>
        <div v-else><div class="btn_default"> {{ y.year }} </div></div>
    </a>
    <div class="clearfix"></div><br />
    <div v-for="m in err_msg"  class="alert alert-danger" role="alert">{{ m }}</div>
    <hr/>
    <ol>
        <li v-for="s in syukujitsu" v-if="s.date.slice(0,4) == select_year">
            <div > {{ s.name }}:{{ s.date }} </div>
        </li>
    </ol>
</div>

{{xxx}}はVue.js の変数を表示する置換定義です。あとv-for, v-ifのタグの属性で繰り返しや条件分岐が表現できています。すごく簡単です。

以下は、各要素へのデータバインディングの適用です。

/*
 * Vue.js
 */
var app = new Vue({
        el: '#app-simple',
        data: {
            years: years_list,
            syukujitsu: syukujitsu_default,
            select_year: "2016",
            err_msg: server_msg
            },
        methods: {
                onFunc: function(y, e) {
                    //console.log(app.default_year)
                    app.select_year = y; // data binding

                    // no good data bind
                    for(var i = 0; i < years_list.length; i++){
                        if(years_list[i].year == y){
                            years_list[i].isActice = true;
                            // console.log(years_list[i].year + ":" + years_list[i].isActice);
                        }
                        else
                        {
                            years_list[i].isActice = false;
                            // console.log(years_list[i].year + ":" + years_list[i].isActice);
                        }
                    };
                    app.years = years_list; // no good data bind
                }
            }
        });

app.select_year = y; を実行するだけで、祝日の一覧再表示が勝手に実施されました。めちゃくちゃ便利です。

私のように
+View層だけで簡単に試したい
+Html,CSS,JavaScriptの知識が多少ある
+npmとか詳しくない
場合はすぐに始められると感じました。

また、公式ページの日本語情報も充実しているように感じました。ここ数年のJavaScriptのデータバインディングの話題をニュースでは見ていたのですが、いまいち実感がわいていませんでした。Angular2のTUTORIAL: TOUR OF HEROESもやってみていたのですが、イマイチ感動が薄かったです。今回、こんなに簡単に表示が切り替わるのが体感できて嬉しかったです。

注記
以下だけで、選択状態を表現したかったのですが、私の知識が足りずに実現できずにいますのでご注意ください。

<div v-bind:class="['btn_default', y.isActive ? 'btn_active': '']"> {{ y.year }}

まとめ

Rubyでの解析と単純に比較はできませんが、感じた事を書いてみます。
・CSVを読み込むライブラリがあるのがすごい。
・foreach, map はScalaでも出てくる(動作も少し似ている?)
・"|xxx, yyy|"で展開ができている?(すごい簡単!)
・日付変換できない時の"Date.parse(text) rescue nil"って表現も簡潔
・transpose が若干難しい
・Hashキーの有無確認無しでも吸収してくれている?(hash[parsed_date] = name)

書いていると興味がつきないけど、今はもっとScalaを身につけます。

ちなにソースはgithubで公開しています。
https://github.com/matsutomu/parse-syukujitsu-scala

2年間勉強していますが、まだまだ初心者です。ご指摘お待ちしています。

おまけ

当初、『JSONで・・・』も考えていたのですが、とりあえず以下のURL叩いて結果が返ってくるまで実装できています。
http://localhost:8080/syukujitsu.json/2017

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1