動的にCSVファイルを取り扱うクラス

今まで「Rubyが動的な言語である」ということは、よく聞いていた。...が、その「動的」とはいったいどんなことで、それがいったい何の役に立つのか、いまいちピンときていなかった。An Exercise in Metaprogramming with Ruby 日本語訳では、その説明をサンプルコードとともに、分かり易く解説してくれている。コード丸写しだが、自分でも実際に試してみた。確かに、CSVファイルの内容が変わっても、コードの変更なしで、欲しいデータを自在に取り出すことが出来る!
DataRecordクラスは、

      • CSVファイルとやり取りするクラス(CSVファイル名のクラス)を、
      • CSVファイルの内容に合わせて、
      • プログラム実行中に作り出してくれる。

この「プログラム実行中に、与えられたデータに合わせて、処理コードを変化させたクラスを生成する」というところが、動的と表現される部分なのかな。コードを追っていると頭が混乱してくるけど...面白い!実際に、DataRecordクラスをRailsで利用してみた。

コードの準備

モデル
app/models/data_record.rb
  • 上記解説ページを参考に以下の部分だけ変更した。
  • 列タイトルを保存するクラス変数、@@columnsを追加した。
  • その値を返すクラスメソッド、columnsも追加した。
  • CSVファイルは、test/fixtures/people.txtに置いた。
class DataRecord
  def self.make(file_name)
    data = File.new(file_name)
    header = data.gets.chomp
    data.close
    class_name = File.basename(file_name,".txt").capitalize  
    # "foo.txt" >> "Foo"
    klass = Object.const_set(class_name,Class.new)
    names = header.split(",")

    klass.class_eval do
      # クラスメソッドで列タイトルを返すため、クラス変数に保存
      @@columns = names

      attr_accessor(*names)

      # newでインスタンス化されたあと呼び出される、初期化メソッドinitializeを定義
      define_method(:initialize) do |*values| 
        names.each_with_index do |name,i| 
          # 列タイトルをインスタンス変数として、対応する値をセットする。
          instance_variable_set("@"+name, values[i])
        end
      end

      # 自分自身の内容を文字列に変換する、to_sメソッドを定義
      define_method(:to_s) do
        str = "<#{self.class}:"
        names.each {|name| str << " #{name}=#{self.send(name)}" }
        return str << ">"
      end

      # 上記to_sメソッドの別名として、inspectメソッドを定義
      alias_method :inspect, :to_s      
    end

    # CSVファイルを読み込むクラスメソッド
    # 1行ずつインスタンス化した配列を返す
    def klass.read
      array = []
      data = File.new("test/fixtures/"+self.to_s.downcase+".txt")
      data.gets  # ヘッダ部分を読み飛ばす
      data.each do |line| 
        line.chomp!   
        values = eval("[#{line}]")
        array << self.new(*values)
      end
      data.close
      array
    end

    # 列タイトルを返すクラスメソッドを追加
    def klass.columns
      @@columns
    end

    # クラスメソッドDataRecord.make()は、CSVファイルを取り扱うクラスを返す。
    klass
  end
end
コントローラー
app/controllers/csvs_controller.rb
class CsvsController < ApplicationController
  def list
    @class = DataRecord.make("test/fixtures/people.txt")
    @csvs = @class.read
  end
end
ビュー
app/views/csvs/list.rhtml
<h1>Listing csvs</h1>

<table>
  <tr>
  <% for column in @class.columns %>
    <th><%= column %></th>
  <% end %>
  </tr>
  
<% for csv in @csvs %>
  <tr>
  <% for column in @class.columns %>
    <td><%=h csv.send(column) %></td>
  <% end %>
    <td><%= link_to 'Show', :action => 'show', :id => csv %></td>
    <td><%= link_to 'Edit', :action => 'edit', :id => csv %></td>
    <td><%= link_to 'Destroy', { :action => 'destroy', :id => csv }, :confirm => 'Are you sure?', :post => true %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New csv', :action => 'new' %>

テストcsvデータを準備

テストデータも同じものを使わせていただいた。

テストデータ
test/fixtures/people.txt
name,age,weight,height
"Smith, John", 35, 175, "5'10"
"Ford, Anne", 49, 142, "5'4"
"Taylor, Burt", 55, 173, "5'10"
"Zubrin, Candace", 23, 133, "5'6"

実行結果

URL
http://localhost:3001/csvs/list

CSVファイルの内容が、こんな感じで表示される。

もし、CSVファイルにちゃんとid列が存在していたら、showメソッドも簡単に実装できそう。

id列を追加したテストデータ
test/fixtures/people.txt
id,name,age,weight,height
1,"Smith, John", 35, 175, "5'10"
2,"Ford, Anne", 49, 142, "5'4"
3,"Taylor, Burt", 55, 173, "5'10"
4,"Zubrin, Candace", 23, 133, "5'6"
  • app/views/csvs/list.rhtmlの:id => csvの部分を、すべて:id => csv.idに変更しておく。
コントローラー
app/controllers/csvs_controller.rb
class CsvsController < ApplicationController
  def show
    @class = DataRecord.make("test/fixtures/people.txt")
    @csvs = @class.read
    @csv = @csvs[params[:id].to_i - 1]
  end
ビュー
app/views/csvs/show.rhtml
<% for column in @class.columns %>
<p>
  <b><%= column %>:</b> <%=h @csv.send(column) %>
</p>
<% end %>

<%= link_to 'Edit', :action => 'edit', :id => @csv.id %> |
<%= link_to 'Back', :action => 'list' %>

実行結果

URL
http://localhost:3001/csvs/show/3


なんだか、CSVファイルを簡単にwebページとして表示できることに、とても魅力を感じる!この仕組みを活かせば、社内でファイルを公開する仕組みが、簡単に実現できそうだ。