Railsで既存のエクセルファイルをテンプレートにできる魔法のヘルパー

### はじめに

今回のコードは、以下の環境で動作確認しています。

 - Rails 5.2.0
 - rubyXL 3.3.29

### 1.  rubyXL Gem をプロジェクトに追加

Ruby で エクセルファイルを扱うのに [rubyXL](https://github.com/weshatheleopard/rubyXL) を利用します。

```ruby:Gemfile
gem 'rubyXL'
```
### 2. 次に魔法のヘルパーを追加します

中身はとりあえずこれだけ❤

```ruby:app/helpers/excel_template_helper.rb
module ExcelTemplateHelper
  def excel_render(template_file)
    RubyXL::Parser.parse(template_file).tap do |workbook|
      workbook.worksheets.each do |worksheet|
        @worksheet = worksheet
        @worksheet.each_with_index do |row, row_num|
          row&.cells&.each do |cell|
            next if cell.nil?
            cell_render(cell)
          end
          row_height_auto(row_num)
        end
      end
    end
  end

  private

  def content_eval(content)
    view_context.instance_eval(%("#{content}")).gsub(/\R/, "\n") # エクセルの改行は LF
  end

  def cell_render(cell)
    cell.change_contents(content_eval(cell.value))
    cell.change_text_wrap(true) if (cell&.value&.lines("\n")&.count) > 1
  rescue
    cell.change_contents('error!')
    cell.change_font_color('FF0000')
    cell.change_fill('FFFF00')
  end

  def row_height_auto(row_num)
    max_lines = @worksheet[row_num]&.cells&.map { |cell| cell&.value&.lines("\n")&.count || 0 }&.max
    origin_height = [@worksheet.get_row_height(row_num), 20].max # 最小値が RubyXL::Row::DEFAULT_HEIGHT (= 13) では合わなかったので手動調整
    @worksheet.change_row_height(row_num, origin_height * max_lines) if max_lines&.positive?
  end
end
```

ポイントは `content_eval` メソッド内の `view_context.instance_eval()` です。これのおかげで、通常の view と同じように、エクセルファイル内でインスタンス変数を扱えるようになります。

### 3. 呼び出す controller 側のサンプルを書きます

こんな感じ。

```ruby:app/controllers/excel_controller.rb
class ExcelController < ApplicationController
  include ExcelTemplateHelper

  def sample1
    create_data
    send_data(
      excel_render('lib/excel_templates/sample1.xlsx').stream.string,
      type: 'application/vnd.ms-excel',
      filename: "#{@datetime.strftime("%Y%m%d_%H%M%S")}_sample1（#{@name}）.xlsx"
    )
  end

  private

  def create_data
    @datetime = Time.now
    @name = 'm-kubo'
    @multi_lines_text = "Remember kids,\nthe magic is with in you.\nI'm princess m-kubo."
  end
end
```

### 4. テンプレートになるエクセルを用意しよう
例えば、 `lib/excel_templates/sample1.xlsx` にこんな感じのエクセルファイルを配置します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/84749/eb8f0760-651a-f1de-d155-329161e14937.png)

### 5. 出力してみよう

routing を追加して、

```ruby:config/routes.rb
get 'sample1', to: 'excel#sample1'
```

`/sample1` にアクセスすると、レンダリングされたエクセルファイルがダウンロードされます 🧙‍♀️

![image.png](https://qiita-image-store.s3.amazonaws.com/0/84749/5dd1f07c-97b8-e55c-2596-6e5fd7eb0522.png)

帳票の種類が多かったり、帳票レイアウトの変更が多い案件の場合、エクセルファイルで直接レイアウトなどを修正できるため、開発も運用もすごく楽になると思います :smile: 

### 6. おまけ

`excel_template_helper` の `cell_render` メソッドを以下のように変更して、

```ruby:app/helpers/excel_template_helper.rb
  def spread_render(cell, array_name)
    original_style_index = cell.style_index
    original_row_height = @worksheet.get_row_height(cell.row)
    view_context.instance_eval(array_name).each_with_index do |_array, index|
      target_row = cell.row + index
      target_column = cell.column
      target_value = cell.value.gsub(array_name) { "#{array_name}[#{index}]" } # 配列変数部に添え字を付与する
      @worksheet.add_cell(target_row, target_column, content_eval(target_value))
      @worksheet[target_row][target_column].style_index = original_style_index
      @worksheet.change_row_height(target_row, original_row_height)
    end
  end

  def normal_render(cell)
    cell.change_contents(content_eval(cell.value))
    cell.change_text_wrap(true) if (cell&.value&.lines("\n")&.count) > 1
  end

  def cell_render(cell)
    if /(?<array_name>@[\w-]+)\[:[\w-]*\]/ =~ cell.value
      spread_render(cell, array_name) # 配列変数を展開しながらのレンダリング
    else
      normal_render(cell) # 通常のレンダリング
    end
  rescue StandardError
    cell.change_contents('error!')
    cell.change_font_color('FF0000')
    cell.change_fill('FFFF00')
  end
```

こんなテンプレートを用意すると、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/84749/c10c037b-c627-827f-5fce-6f609bf0e003.png)

配列変数を自動展開させて、任意の一覧表を出力させることもできます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/84749/a3a13b41-816c-d919-ae42-587b24d3b1a2.png)



工夫次第ですね❤

### 終わりに

以上、どこかの案件で書いたコードの紹介でした。

誰かの役に立ちますように！