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