区切り文字を選択可能にする。

表計算ソフトで数値を表示する時は、表示形式で桁数を3桁ごとにカンマで区切ることがよくある。その状態でCSVファイルをアップロードすると、表示形式の桁区切りであっても、CSVサーバーは項目の区切りと認識してしまう...。
アップロードした結果が思い通りにならない原因は、ほとんどがこのケースだ。区切り文字をカンマに限定している所が問題なのだ。区切り文字は自由に選択できた方がいい。(CSVとはcomma-separated valuesの略なので、厳密にはCSVファイルでなくなってしまうが...。)
項目を"123,456,789"のように、ダブルクォーテーションで囲むという方法も考えてみた。しかし、CSVサーバーは"ダブルクォーテーション"で囲まれた部分は文字列と認識するように設定してあるので、それでは表示する時に左寄せになってしまう...。(数値か、文字列か、判定して、ページを表示する時に右寄せ、左寄せを調整しているのだ。)
ということで、区切り文字フリー入力制にしてみた。

モデルにインスタンス変数@separatorを定義

CSVファイルをアップロードすると、モデルcsv.rbでデータベースへのインポート処理をしている。まずは区切り文字を保存するための、Csvモデル内で参照可能な変数が必要だ。そこで現在、区切り文字としてカンマ「,」を指定している箇所を全て@separatorに変更した。深く考えずに、カンマの部分はとにかく@separatorに変更。正規表現文字列であっても、式展開#{@separator}が利用できるのでとても助かる。

app/models/csv.rb
モデル
  • 以下、ちょっと長くなるがCSVファイルのインポート処理をするコード全てを書き出してみた。(変更した部分だけでは処理の流れが分からなくなるため)@separatorに変更した部分は、オレンジ色になっている。
class Csv < ActiveRecord::Base
...(途中省略)...  
  # テーブルを追加する。
  def create_csv_table
    #execute_sql("CREATE TABLE #{csv_table} (id INTEGER PRIMARY KEY NOT NULL#{csv_columns});")
    ActiveRecord::Migration.create_table csv_table do |t|
      csv_columns.each do |f|
        t.column f, :string
      end
    end
  end
  
  # テーブル名を返す。
  # 先頭に_を付加する。(CSVファイルとアプリケーションのテーブルを区別するため。)
  def csv_table
    #File.basename(self.file_name, ".*").gsub(/^(.)|\W/, '_\1')
    self.file_name
  end
  
  # 列名の配列を返す。
  # 同時に不完全なファイル内容を最適化する。
  # インポート用の一時ファイルを保存する。
  def csv_columns
    # ファイルを読み込む。"'は描画する時に調整する。
    # 文字コードUTF-8、改行コードをLFに変換して、先頭の余分な改行は削除する。
    # 改行で区切った配列を返す。
    str = @file.read
    str = NKF.nkf('-wLu', str).sub(/^\n+/, "\n")
    lines = str.split("\n")#.map{|s| s.gsub(/"([\d\+\-\\\$\.,+−ー¥▲]+)"/, '\1')}
    
    # 配列の先頭を取り出して、小文字に変換、カンマで区切った配列にして、余分な空白を削除する。
    # gsubの内容は以下の通り。
    #   数字で始まる時は、先頭に_を付ける。
    #   英数字または_でない文字は、_に置き換える。
    # split(",")は、行末の余分なカンマを無視する。
    # 例:"1,2,,3,,,".split(",") >> ["1","2","","3"]
    # gsub(/['"]/, '')は、'と "を全て削除する。
    line = lines.shift
    columns = line.gsub(/['"]/, '').downcase.split(@separator).map!{|n| n.strip.gsub(/^([\d])|\W/, '_\1')}
    
    # 列タイトルが重複している場合は、データ配列に戻して、列タイトルは空行にする。
    unless columns == columns.uniq
      lines.unshift(line)
      columns = []
    end
    
    # 列タイトルが空行なら、ABCの列名を追加する。
    if columns.empty?
      name = 'A'
      lines[0].split(@separator).size.times do
        columns << name
        name = name.succ
      end
    end
    
    # 列タイトルの一部がブランクなら、ブランクをABCの列名に置き換える。
    name = 'A'
    columns.map! do |column|
      column = name if column.empty?
      name = name.succ
      column
    end
    
    # 行末の余分なカンマを削除する。lineは51行目で定義。
    n = lines[0].gsub(/[^#{@separator}]/, '').size - columns.size + 1
    lines.map! {|line| line.sub(/#{@separator}{#{n}}$/, '')} if n > 0
    
    # id列が存在しなければ追加する。
    if columns.index('id').nil?
      id = 0
      lines.map! {|line| id +=1; "#{id}#{@separator}#{line}"}
    end
    
    # path列が存在しなければ追加する。
    if columns.index('path').nil?
      columns << "path"
      lines.map! {|line| "#{line}#{@separator}"}
    end
    
    # インポート用の一時ファイルを保存する。
    File.delete("public/temp.csv") rescue nil
    File.open("public/temp.csv", "wb") do |f|
      f.write(lines.join("\n"))
    end
    
    # 最初のidフィールドは、読み飛ばす。
    columns.shift if columns.first == 'id'
    columns
  end
  
  # csvデータをインポートする。
  # インポートを高速化するため、sqlite3のインポート命令を、直接コマンド実行する。
  def import_csv
    env = ENV['RAILS_ENV'] || 'development'
    system("sqlite3", "-separator", "#{@separator}", "db/#{env}.sqlite3", ".import public/temp.csv #{csv_table}")
  end
  
  # テーブルを削除する。
  def drop_csv_table
    ActiveRecord::Migration.drop_table(csv_table)
  end
end

コントローラーとモデルの情報の受け渡し

app/models/csv.rb
モデル
...(途中省略)...  
  # 区切り文字をインスタンス変数に保存する。
  # new()や、update_attributes()の時、呼び出される。
#  def separator=(s)
#    @separator = s
#  end

 # csvモデルの区切り文字を読み取る。
#  def
#    @separator
#  end

# つまり、以下の一行でOKなのです。
  attr_accessor :separator

...(途中省略)...  
  • 上記のような「=」記号を含んだインスタンス変数へのアクセスメソッドをモデルに定義しておけば、コントローラーからは以下のようなアクセスが可能になる。
@csv = Csv.new(:separator=>',') Csvモデルの@separatorに「,」がセットされる。
@csv.separator = '\' Csvモデルの@separatorに「\」がセットされる。
a = @csv.separator 変数aに「\」が代入される。

これで、csvモデルのcreateメソッドの中で、@csv.separator = '|'のように書いておけば、アップロードする時の区切り文字は「|」になる。

      • まだ、区切り文字を入力するインターフェースがない...。

ビューに区切り文字の入力項目を追加

  • アクセッサメソッドを定義したので、text_fieldを使って、以下のように簡潔に書くことが出来る。
app/views/csvs/_form.rhtml
ビュー
...(途中省略)...
<tr>
  <th align="right"><label for="separator">区切り文字:</label></th>
  <td>
  <%= text_field 'csv', 'separator', :size=>2 %>
  </td>
</tr>
...(途中省略)...


とりあえずは、アップロードする時に区切り文字を指定できるようになった。

      • でも、どんな値でも、さらには値がなくてもOKな状態だ。自由すぎる...。

区切り文字のチェック機能(バリデーション)

  • attr_accessor :separatorを定義してあるので、いつも通りの検証が可能。以下のように簡潔に表現できる。簡単だ!
app/models/csvs.rb
モデル
class Csv < ActiveRecord::Base
  validates_length_of :separator , :in=>1..1, 
                      :too_short=>"...区切り文字の指定をお願いします。", 
                      :too_long=>"...%d文字でお願いします。"
...(途中省略)...  

初期値を「,」で表示する。

  • アップロードする時、区切り文字が空欄で、毎回入力するのは面倒だ。アプリケーションのデフォルト値を設定して、それを表示することにした。(オレンジ色の部分を追記)
app/controllers/application.rb
コントローラー
  # デフォルトデータベースからユーザー設定を読み取り、返す。
  # 値は全て文字列としてデータベースに保存されているので、ここで最適な形式に変換する。
  def default_user(force_reload = false)
    default_app.merge(YAML.load(current_user.default(force_reload).yaml)) rescue default_app
  end
  
  # アプリケーションのデフォルト値を返す。
  # ハッシュのキーは、シンボルでなく、文字列で指定する必要あり。:per_page...NG/'per_page'...OK
  # ハッシュの値も、全て文字列で指定する必要あり。'per_page'=>10...NG/'per_page'=>'10'...OK
  def default_app
    {'per_page'=>'10', 'sort_field'=>'created_at', 'sort_direction'=>'desc', 'separator'=>','}
  end
app/controllers/csvs_controller.rb
コントローラー
class CsvsController < ApplicationController
...(途中省略)...
  def new
    # ログインユーザーをアップロード担当者にする。new(:user_id => session[:user].id)
    @csv = Csv.new(:user_id=>session[:user].id, :editable=>false, :separator=>default_user['separator'])
    @sections = session[:user].management_sections
    render :layout=>'modal'
  end
...(途中省略)...

ユーザー設定として保存する。

  • 区切り文字もユーザーごとに保存できた方が、使い勝手がいい。ユーザー設定として保存できるようにしてみた。
    • アップロードページの区切り文字の入力フォームの横に「区切り文字を保持する」リンクを設定して、クリックするとユーザー設定として保存できるようにした。
    • アップロードページで直接変更できた方が無駄がない気がしたので、「設定」メニューは使わなかった。
app/views/csvs/_form.rhtml
ビュー
...(途中省略)...
<tr>
  <th align="right"><label for="separator">区切り文字:</label></th>
  <td>
  <%= text_field 'csv', 'separator', :size=>2 %>
  <small id='separator_save'>
  <%= link_to_remote '区切り文字を保持する', 
                     :update=>'form_update', 
                     :submit=>'form_update', 
                     :url=>{:action=>'save_separator'},
                     :complete=>visual_effect('highlight', 'separator_save') %>
  <%= @msg %>
  </small>
  </td>
</tr>
...(途中省略)...
app/controllers/csvs_controller.rb
コントローラー
...(途中省略)...
  def save_separator
    @csv = Csv.new(params[:csv].merge(:file_name=>'test'))
    @sections = session[:user].management_sections
    if @csv.valid?
      current_user.create_default(:yaml => {}.to_yaml) if current_user.default.nil?
      @default = current_user.default
      @default_hash = default_user.merge('separator'=>@csv.separator)
      current_user.default.update_attributes(:yaml => @default_hash.to_yaml)
      @msg = "» 完了!"
    else
      @msg = "» エラー"
    end
    render :partial=>'form'
  end
...(途中省略)...

これで区切り文字を、気の向くままに変更可能になった。

バリデーションの表示調整

  • 現状では「:separator」がエラーのキーになり、<%= text_field 'csv', 'separator' %>は、エラーが発生した時、以下のように展開される。
<div class="fieldWithErrors"><input id="csv_separator" name="csv[separator]" type="text" value="" /></div>
  • これでは、エラーが発生するとdivタグで囲まれてしまうので、後に続くメッセージが改行されてしまう...。


  • 今回は、エラーが発生しても1行で表示したいので、以下のように変更した。

app/models/csvs.rb
モデル
  • text_fieldがdivタグで囲まれないように、エラーのキーを"区切り文字"に変更した。
class Csv < ActiveRecord::Base
#  validates_length_of :separator , :in=>1..1, 
#                      :too_short=>"...区切り文字の指定をお願いします。", 
#                      :too_long=>"...%d文字でお願いします。"
  def validate_on_create
    errors.add("区切り文字", "...区切り文字の指定をお願いします。") if separator.blank?
    errors.add("区切り文字", "...1文字でお願いします。") if separator.length > 1
  end
...(途中省略)...
app/views/csvs/_form.rhtml
ビュー
  • tdタグが全体がエラー表現されるように変更した。(field_with_errors_class(@csv, '区切り文字')は自分で定義したヘルパメソッドで、エラー発生時にclass="field_with_errors"が返る。)
  • error_message_on "csv", "区切り文字"で、csvモデルのエラーのキーが"区切り文字"のエラー内容を表示してくれる。エラーが発生した場合、右側に表示することにした。
<div id="form_update">
<%= error_messages_for 'csv' %>
...(途中省略)...
<tr>
  <th align="right"><label for="separator">区切り文字:</label></th>
  <td <%= field_with_errors_class(@csv, '区切り文字') %> id='separator'>
  <%= text_field 'csv', 'separator', :size=>2 %>
  <small id='separator_save'>
  <%= link_to_remote '区切り文字を保持する', 
                     :update=>'form_update', 
                     :submit=>'form_update', 
                     :url=>{:action=>'save_separator'},
                     :complete=>visual_effect('highlight', 'separator_save') %>
  <%= @msg %><%= error_message_on "csv", "区切り文字" %>
  </small>
  </td>
</tr>
...(途中省略)...
  • エラーメッセージも改行されないように、以下のスタイルシートを追加した。
td div.formError{
  display: inline
}


変更が広い範囲に渡ったが、区切り文字を自由に入力指定できるようになった。