ようへいの日々精進XP

よかろうもん

最近, 祝日・休日について悩むことがあったので, 内閣府が提供する祝日・休日 csv データを Elixir から利用するライブラリを作ってみた && Travis CI で継続的にライブラリを運用する方法を検討したのでメモ #fukuokaex

tl;dr

最近, 祝日・休日について悩むことがあったので, 内閣府が提供する祝日・休日 csv データを Elixir から利用するライブラリを作ってみました. そして, 年が変わっても継続的にライブラリが運用出来るようにしてみたのでメモしたいと思います.

作ったもの

リポジトリ

github.com

hex.pm

簡単な使い方

Install

  • mix.exs で以下のように書いておきます
defp deps do
  [
    {:holidays_ex, "~> 0.0.4"},
  ]
end
  • mix deps.get でインストールしちゃいます
mix deps.get

to_json

JSON フォーマットで欲しい場合には, 以下のように to_json/0 を利用します.

iex> HolidaysEx.to_json
"{\"2019-11-03\":\"文化の日\",\"2018-09-23\":\"秋分の日\"..."

holiday?()

日付から祝日・休日名が欲しい場合には, 以下のように holiday?/1 を利用します.

iex> HolidaysEx.holiday?("2019-11-03")
"文化の日"

when?()

祝日・休日名から日付が欲しい場合には, 以下のように when?/1 を利用します.

iex> HolidaysEx.when?("海の日")
["2017-07-17", "2018-07-16", "2019-07-15"]

all()

単純に Map で欲しい場合には, 以下のように all/0 を利用します.

iex> HolidaysEx.all
[
   {"2017-01-01", "元日"},
   {"2017-01-02", "休日"},
   {"2017-01-09", "成人の日"}
   ...
]

どんなところで利用するかは貴方次第...きっと, 誰かの役に立つかもしれません.

メモ

パイプライン演算子使いまくり

正しい実装なのか解らないけど, File.stream! で読み込んだ CSV データ (事前に UTF-8 に変換している) をパイプライン演算子を駆使して Map に変更, さらにその Map をパイプライン演算子でいい感じで加工しています.

...
  def all() do
    generate_map_data() |> Enum.sort
  end

  defp generate_map_data() do
    CSV.decode!(File.stream!(Path.join(:code.priv_dir(:holidays_ex), "data.csv")), headers: false)
    |> Stream.drop(1)
    |> Stream.map( fn( [ date, name ] ) -> %{ date => name } end )
    |> Enum.flat_map( fn m -> Map.new(m) end )
    |> Map.new
  end
end
...

少ないコードで思ったような出力を得られることに感動しました...

CSV データをどこに配置するか

ライブラリが呼ばれる度に CSV をダウンロードしていては, 内閣府に怒られてしまうので, ライブラリ内に csv ファイルを保持する実装にしました. 以下のようなルールでファイルを置いておけば, ライブラリを利用するアプリケーションからもデータを参照出来ました.

  • priv というディレクトリを作成して, そのディレクトリ以下に csv ファイルを設置する
  • 以下のように :code.priv_dir(:holidays_ex), "data.csv" と書くことで data.csv を参照可能
Path.join(:code.priv_dir(:holidays_ex), "data.csv")

詳細は要確認ですが, :code.priv_dir() という書き方は Erlang で利用されている書き方のようです.

ドキュメント

以下のようにコード内にアノテーションを書いておくと, イイ感じでドキュメントも生成してくれます.

...
  @doc """
  Specifying a date returns a holiday's name.

  ## Examples

      iex> HolidaysEx.holiday?("2019-11-03")
      "文化の日"

  """
  def holiday?(date) do
    generate_map_data() |> Map.fetch!(date)
  end
...

以下のようにドキュメントが生成されます.

f:id:inokara:20180414140532p:plain

テスト

ライブラリを配布するからには, テストも書きたいので, 簡単に以下のようなテストを書きました.

defmodule HolidaysExTest do
  use ExUnit.Case
  doctest HolidaysEx, except: [:moduledoc, all: 0, to_json: 0]

  setup_all do
    {:ok, days: ["2017-07-17", "2018-07-16", "2019-07-15"], holiday: "元日"}
  end

  test "Get Umino-Hi's dates", state do
    assert HolidaysEx.when?("海の日") == state[:days]
  end

  test "Get holiday's name by date", state do
    assert HolidaysEx.holiday?("2018-01-01") == state[:holiday]
  end
end

doctest を付与していると, ドキュメントに記載している Example のコードが正しく動くかのテストもしてくれます. また, excpet にテストを除外する関数名を記載します. 尚, Elixir School のテスト によると...

Elixir でのモックに対する単純な解答は、使うな、です。本能のままにモックへと手を伸ばしているかもしれませんが、 Elixir のコミュニティや正当な理由からはとても推奨されていないものです。

とのことなので, setup_all マクロ内に各関数の期待する戻り値を書いておくことで対応しました.

やっぱり, モックを使うことにしたので, 別記事で簡単にモックライブラリの使い方をまとめる予定です.

Travis CI を利用したノーメンテナンス的継続的デリバリー

内閣府がデータを提供し続けている限り, ノーメンテナンスでこのライブラリを提供したいと考えました. ということで, Travis CI の cron を利用してライブラリをテストして, mix hex.publish するように .travis.yml を以下のように書きました.

language: elixir
sudo: false
elixir:
  - '1.6.4'
otp_release:
  - '20.0'
cache:
  directories:
    - _build
    - deps
before_install:
  - mkdir -p ~/.hex/
  - openssl aes-256-cbc -K $encrypted_xxxxxxxxx1_key -iv $encrypted_xxxxxxxxx1_iv -in hex.config.enc -out ~/.hex/hex.config -d
script:
  - wget http://www8.cao.go.jp/chosei/shukujitsu/syukujitsu_kyujitsu.csv
  - iconv -f Shift_JIS -t UTF8 syukujitsu_kyujitsu.csv > priv/data.csv
  - MIX_ENV=test mix test
deploy:
  provider: script
  script: >-
    mix deps.get &&
    wget http://www8.cao.go.jp/chosei/shukujitsu/syukujitsu_kyujitsu.csv &&
    iconv -f Shift_JIS -t UTF8 syukujitsu_kyujitsu.csv > priv/data.csv &&
    echo -n $HEX_LOCAL_PASSWORD | mix hex.publish --no-confirm &&
    mix clean &&
    mix deps.clean --all

こちらの .travis.yml をかなり参考にさせて頂きました. 有難うざいました.

事前に, hex.config を travis コマンドで暗号化した上で, hex local password を環境変数に登録しておく必要があります.

travis encrypt-file hex.config

travis コマンドを利用して Travis CI にログインした状態で travis encrypt-file を実行すると, hex.config.enc を生成した上で, 以下のようにデコードする為のコマンドも出力されます.

openssl aes-256-cbc -K $encrypted_xxxxxxxxx1_key -iv $encrypted_xxxxxxxxx1_iv -in hex.config.enc -out ~/.hex/hex.config -d

ちゃんとテストと mix hex.publish が実行されることを確認したら, Travis CI の cron を設定しました.

f:id:inokara:20180414141614p:plain

一つに一回くらい動かしておけば良いでしょう. もし, 国の気まぐれで csv データの変更 (祝日・休日が変わった場合) が入った場合, 気付いたら手動でビルドを実行すればいいかなーくらい感覚です.

ちゃんと動いてくれると嬉しいなあ.

以上

Elixir で初めてライブラリを作ってみましたが, @piacere_ex さんが Twitter で以下のように言及されているように, パイプライン演算子を駆使することで, 思ったよりも簡単にやりたいことが実現出来たので, 高い生産性を実感することが出来ました (実際に作ろうと思って, Hex にリリースするまでの所要時間は 6 時間くらい).

また, エコシステムについても, Ruby の gem や Rake に似たツールが利用出来る為, ある程度は雰囲気で扱うことが出来ました. 言語仕様等については, まだまだ勉強付属ですが, 今後も Elixir の良さを活かせるようなライブラリを書けるようになりたいと思います.

オッツデース.