Elixir CSVデータを取り込んで表示する方法

# 簡単Elixirシリーズ 
## ~ Elixir CSVデータを取り込んで表示する方法 ~

この記事は「[Elixir Advent Calendar 2022](https://qiita.com/advent-calendar/2022/elixir)」14日目の記事です
東京にいるけどfukuokaexのYOSUKEです。

2019年にかいた記事をUPデートしました。

## 本題 : Elixir CSVデータを取り込んで表示する方法

fukuoka.exのpiacereさんが書かれたElixirのチュートリアル[Excelから関数型言語マスター1回目：データ行の”並べ替え”と”絞り込み”
](https://qiita.com/piacere_ex/items/6714e1440e3f25fb46a1)は非常に入りやすい教材なのでお勧めです。第７回まであるので、まだやっていない方は是非トライしてみてください。

このElixirのチュートリアルで、JsonAPIからデータ表示や、DBからデータ表示を体験した方向けに、だったらCSVも表示してみたい！というご要望に応えるものです。（あるのか知りませんがw）

## おさらい　DBアクセスモジュールを作る
[Excelから関数型言語マスター3回目：WebにDBデータ表示【PostgreSQL or MySQL編】
](https://qiita.com/piacere_ex/items/a7558adc6856e3577dc6)より、DBアクセスモジュールを作るという項目があります。
CSVデータの読み込みも、最終的にはdbの読み込みと似たような考え方で実装できそうなので、これを参考にして行きましょう。

ただ、せっかくなので、今回は別の書き方で実装してみます。

```elixir
defmodule Db do

  def query(sql) do
    case Ecto.Adapters.SQL.query( YourProjectName.Repo  ,sql, []) do
      {:ok, result } -> result
      {:error, _result } -> "sql error"
    end
  end

  def columns_rows(result) do
     Enum.map(result.rows, fn row -> to_map(result.columns, row) end )
  end

  defp to_map(column, row) do
    List.zip([ column, row ])
    |> Enum.into(%{})
  end

end
```

### 解説
Db.query関数では、単純に Ecto.Adapters.SQL.queryを利用すると結果が
{:ok, result }　というタプル形式で返ってきます。

case 文を使って:okが返って来た時にresultだけを取得できるようにしています。
:errorが返って来た時には、"sql error"を返しています。

試しに、取得されるData構造を見てみましょう。

```elixir
%Postgrex.Result{
   ["id", "name", "age", "team", "position", "inserted_at",
   "updated_at"],
  command: :select,
  connection_id: 8899,
  messages: [],
  num_rows: 2,
  rows: [
    [1, "YOSUKENAKAO.me", 40, "The Waggle", "CEO",
     ~N[2019-02-04 13:22:01.000000], ~N[2019-02-04 13:22:01.000000]],
    [2, "YOSUKENAKAO.me", 40, "fukuoka.ex", "シュミリクサー",
     ~N[2019-02-04 14:30:53.000000], ~N[2019-02-04 14:30:53.000000]]
  ]
}
```

構造体のDataで、columns:とrows:にそれぞれリスト形式でデータが入っている事がわかります。

この構造体のデータから、扱いやすいようにマップ型のデータに変換しているのがDb.to_map関数になります。

### 解説

```elixir
defp to_map(column, row) do
    List.zip([ column, row ])
    |> Enum.into(%{})
end
```

この関数の結果作りたいdataはこちらになります。

```elixir
%{
    "id" => 1,
    "inserted_at" => ~N[2019-02-04 13:22:01.000000],
    "name" => "YOSUKENAKAO.me",
    "age" => 40,
    "team" => "The Waggle",
    "position" => "CEO"
    "updated_at" => ~N[2019-02-04 13:22:01.000000]
  }
```

``` List.zip([[1,2,3],["a","b","c"]]) ```関数はこのようなリストのデータを
``` [{1, "a"}, {2, "b"}, {3, "c"}] ```というようにタプルのデータを格納したリストデータに変換してくれます。

タプルを格納したリストデータにできると、Enum.intoを使って、マップ型のデータに変換する事ができます。
``` Enum.into([{1, "a"}, {2, "b"}, {3, "c"}], %{}) ```
とすると、``` %{1 => "a", 2 => "b", 3 => "c"} ```と返ってきます。

ただ、このままだと rows:のデータは[[],[],...[]]このようなリスト構造になるので、繰り返しto_map関数に引き渡したい所です。
その願いを叶えてくれる簡単な関数がEnum.mapになります。

```elixir

def columns_rows(result) do
     Enum.map(result.rows, fn row -> to_map(result.columns, row) end )
end
```

## CSVを読み込むモジュールを作る

DBの読み込みモジュールを踏まえた上で、CSVの読み込みモジュールを作って行きましょう。と、その前に簡単にCSVデータをdecodeしてくれるモジュールが既にあるので、そちらを利用したいと思います。

mix.exs ファイルのdepsに ``` {:csv, "~> 2.0.0"} ```を追記します。

```mix.exs

defp deps do
    [
　　　：
     省略
     # {:csv, "~> 2.0.0"} 2019年時点のバージョン
     {:csv, "~> 3.0"}
    ]
end
```

コンソールから、``` mix deps.get ```でモジュールを追加してください。

```console
mix deps.get

```

今回は、CSVモジュールを使うので、[CSV](https://hexdocs.pm/csv/CSV.html)のドキュメント読んで使い方を参考にしてデータを取得してみます。

>参考 
>
```elixir
iex> "../test/fixtures/docs/valid.csv"
iex> |> Path.expand(__DIR__)
iex> |> File.stream!
iex> |> CSV.decode!
iex> |> Enum.take(2)
[["a","b","c"], ["d","e","f"]]
```

ファイルからCSV.decode!経由して、Enum.take(2)でCSVのRowデータをリスト型で２つ取得する事ができるようです。

Enum.takeの引数に数値を入れる事でその数値分を取得できるのですが、CSVデータ全部を読み込みたいので、ここを少し変えれば簡単にできそうですね。

作成した、``` util ``` フォルダ（[Excelから関数型言語マスター3回目：WebにDBデータ表示【PostgreSQL or MySQL編】で作成済み）の中に、``` csvread.ex ``` と``` data_csv.ex ``` ファイルを作成します。

モジュール名が若干イケテナイのですが、そこはご愛嬌でw

```csvread.ex

defmodule CsvRead do

 def stream(file_path) do
    file_path
    |> Path.expand(__DIR__)
    |> File.stream!
    |> CSV.decode
    |> Enum.to_list
    |> Enum.map(fn row -> tuple_to_list(row) end)
  end

end
```
### 解説
stream関数では、CSVモジュールの参考例を元に書いています。変えた部分は、``` Enum.take ```を``` Enum.to_list ``` に変更しています。これで、全てのデータを取得してくれます。

ですが、データ形式は``` [ {:ok, row_data},...{} ]　``` 一行毎に ``` { status, data } ``` という形のタプルで返って来ます。このタプル形式をリストに変更したいので、tuple_to_list関数を作って、リストデータに変換します。
``` :error ``` の場合は今回は簡単にする為に、空のリストを返しています。

```csvread.ex
defmodule CsvRead do

  省略

  defp tuple_to_list(tuple_data) do
    case tuple_data do
      {:ok, list_data} -> list_data
      {:error, _list_data } -> []
    end
  end

end
```

あとは、これをDbモジュールの時のようにマップ型にすれば、既に知っている方法で使えそうです。
``` %Postgrex.Result ```構造体を思い出して見ましょう。（最初の方に記載してます）
構造体の中で利用したのは、``` columns: ``` と ``` rows: ``` でした。

そこで、この２つのデータを保持する構造体を定義して、それぞれにデータを入れてあげれば良さそうです。
先ほど作成した。```data_csv.ex``` に構造体の定義を書いて行きましょう。

構造体の定義には、``` defstruct ``` を使います。

```data_csv.ex

defmodule Data_csv do
  defstruct [:columns, :rows]
end

```

それでは、構造体にデータを追加する関数を追記しましょう。

```csvread.ex

defmodule CsvRead do

  def stream(file_path) do
    file_path
    |> Path.expand(__DIR__)
    |> File.stream!
    |> CSV.decode
    |> Enum.to_list
    |> Enum.map(fn row -> tuple_to_list(row) end)
  end

  def csv_struct(head, tail) do # <- この関数を追記 
    %Data_csv{columns: head, rows: tail }
  end

  defp tuple_to_list(tuple_data) do
    case tuple_data do
      {:ok, list_data} -> list_data
      {:error, _list_data } -> []
    end
  end

end
```

それでは、CSVデータを読み込んで表示して行きましょう。
utf-8のCSVを用意しました。場所はプロジェクトフォルダの直下に置いています。

今回は、こちらのオープンデータを利用しました。[AED情報(東京都品川区)](http://ckan.odp.jig.jp/dataset/jp-tokyo-shinagawa-771-odp)

```templates/page/index.html.eex
<%

path = "../../sample_utf8.csv"
[head | tail] = CsvRead.stream(path)
res = CsvRead.csv_struct(head, tail)
columns = res.columns
recode = Db.columns_rows(res)

%>

<table border="1">
<tr>
<%= for column <- columns do %>
<th><%= column %></th>
<% end %>
</tr>
<%= for recode_row <- recode do %>
<tr>
<%= for column <- columns do %>
<td><%= Map.get(recode_row,column) %></td>
<% end %>
</tr>
<% end %>
</table>
```

これで、無事にデータを表示できました。

![スクリーンショット 2019-02-10 23.26.01.png](https://qiita-image-store.s3.amazonaws.com/0/72962/1da80e7a-b132-b487-eec1-345efadfb7c7.png)

これを応用すれば、地図にCSVデータから取得した位置情報をマッピングする事も簡単です。

![スクリーンショット 2019-02-10 23.26.16.png](https://qiita-image-store.s3.amazonaws.com/0/72962/86609367-8d76-0f25-44c6-f050c1ce59bb.png)

## 終わり

今回は、CSVデータを簡単に読み込んで取得できるようにしてみました。
Elixirって楽しそう！と思って頂ければ幸いです！

## p.s

良かったら、是非　いいねをよろしくお願いします。
また、プログラミング未経験者から学べる教材開発もして行きますので、こんな内容学びたい！という要望があれば、どしどしご意見ください。

東京近辺でElixirを仕事として行きたいエンジニアの方いたら、是非是非、交流しましょう！