動的にCSVファイルを取り扱うクラス
今まで「Rubyが動的な言語である」ということは、よく聞いていた。...が、その「動的」とはいったいどんなことで、それがいったい何の役に立つのか、いまいちピンときていなかった。An Exercise in Metaprogramming with Ruby 日本語訳では、その説明をサンプルコードとともに、分かり易く解説してくれている。コード丸写しだが、自分でも実際に試してみた。確かに、CSVファイルの内容が変わっても、コードの変更なしで、欲しいデータを自在に取り出すことが出来る!
DataRecordクラスは、
この「プログラム実行中に、与えられたデータに合わせて、処理コードを変化させたクラスを生成する」というところが、動的と表現される部分なのかな。コードを追っていると頭が混乱してくるけど...面白い!実際に、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"
もし、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/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' %>