Excelファイルの作成や編集などを行えるRuby GemのrubyXLを使って、予め用意したYAMLファイルを元にして、Excelに表を作ろうとしました。
rubyXL:RubyGems
rubyXL:Github
しかし、作成したExcelファイルを開くと「ファイル破損エラー」が発生しました。
調べた結果、セルを追加するコードに問題があると見当がつきました。
結論
以下のようにrubyXLでセルを追加するメソッドadd_cellで受け取ったセルオブジェクトにデータタイプ(datatype)を設定すると「ファイル破損エラー」は発生しなくなりました。
#値:2、数式:=1+1のセルをA1に追加して、データタイプ:数値(NUMBER)を指定する
new_cell = add_cell(0, 0, 2, "1+1")
new_cell.datatype = RubyXL::DataType::NUMBER
セルに入力する値は、数値や文字列が混在する場合(四則演算の他にIF関数を使う等)が多いと思うので、以下のようにcase文を使って設定するデータタイプを分けると良いかと思います。
new_cell = add_cell(row, col, value, formula)
case value
when Numeric then
new_cell.datatype = RubyXL::DataType::NUMBER
when String then
new_cell.datatype = RubyXL::DataType::RAW_STRING
end
以下、調べた内容と原因の予想を書きます。
ファイル破損エラーは発生するパターンと発生しないパターンがある
手元で調べた限りは以下のパターンがありました
- ファイル破損エラーが発生するNGパターン
- 値と数式を設定したとき(1)
- ファイル破損エラーが発生しないOKパターン
- 値だけを設定したとき(2)
- 空文字列("")と数式を設定したとき(3)
#NGパターン(1):値:2、数式:=1+1のセルをA1に追加する
new_cell = add_cell(0, 0, 2, "1+1")
#OKパターン(2):値:2のセルをA1に追加する
new_cell = add_cell(0, 0, 2)
#OKパターン(3):値:""、数式:=1+1のセルをA1に追加する
new_cell = add_cell(0, 0, "", "1+1")
ちなみに、rubyXLでセルを追加するメソッドadd_cellの引数の定義は以下の通りです
def add_cell(row_index = 0, column_index = 0, data = '', formula = nil, overwrite = true)
エラーが発生しないパターンで十分な場合もある
実は、rubyXLでExcelファイルを作成するだけで良いのであれば、値を空文字列にして数式を渡せば事足りるかもしれません。
というのも、数式さえ設定しておけば、ファイルを開いたときに数式の結果がセルに表示された状態になっていたからです。
先述のOKパターン(3)であれば、1+1の結果である2がセルに表示されます。
問題となるのは、rubyXLで作成したファイルをrubyXLでさらにパースしてYAMLファイルなどにデータを移すときです。
セルに値を設定しないとパースしたときに値が空文字列になる
rubyXLでセルの値を取得するときは「cell.value」のようにしますが、先述のOKパターン(3)のように「add_cell(0, 0, "", "1+1")」としてセルの値に空文字列を設定していると、シート上の見た目で「2」が表示されていたとしても、セルの値(valueプロパティ)としては空文字列になっていました。
しかし、rubyXLで作成したファイルを開いて、数式が入っているセルをアクティブにしたりファイルを保存したりするとシートの再計算処理が走り、セルのvalueプロパティに計算結果が入ります。なので、rubyXLでセルの値を取得しても空文字列ということはありません。
したがって、rubyXLで作成したファイルは必ず開くのであれば問題は表面化しませんが、一度もファイルを開かない場合があるときは問題が表面化します。
rubyXLソースコードではデータタイプの指定をわざと抜いているように見える
今回取り上げているrubyXLの add_cellメソッドのソースコードを見てみました。
下記リンク先は add_cellメソッドの定義になります。この中で、数式formulaの指定の有無をif文で分岐させて値だけの設定と値&数式の設定を行っています。
抜粋すると以下の通りで、値のみ指定(ifがfalseのとき)ではセルのデータタイプ(datatype)を各タイプに合わせて色々設定しているのですが、値と数式の指定(ifがtrueのとき)では、セルに値(data)を入れるだけでデータタイプの指定をしていません。rubyXLの設計思想として「数式のデータタイプまで面倒見きれないから各自で設定してね」ということなのかもしれません。
if formula then
c.formula = RubyXL::Formula.new(:expression => formula)
c.raw_value = data
else
case data
when Numeric then c.raw_value = data
when String then
c.raw_value = data
c.datatype = RubyXL::DataType::RAW_STRING(中略)
end