ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

CsvBuilderとExcelBuilderを作ってみました

こんにちは、システム開発部のちょうです。今回は自分が最近の開発でもっとやりやすい方法でCSVやExcelを作成することについて話したいと思います。

まずCSVから。Ruby標準のライブラリにすでにcsvというCSVを取り扱うライブラリがあります。でもCSVを作成する際に、ヘッダーと内容は別々で出力します。

CSV.generate do |csv|
  csv << ["column1", "column2", "column3"]

  items.each do |it|
    line = []
    line << it.foo
    line << it.bar
    line << it.baz
    csv << line
  end
end

ここに、サービスの開発をしていくにつれ、列が増える可能性があります。すると、修正する箇所が2つあります。一つはヘッダーのところ、もう一つはループのところです。列の数がすくないならまだいいですが、列が10個以上、20個に増えたらデータの漏れや順番の間違いは発生しやすいです。 そもそも列と内容の出力は一緒にすればいいのではと自分が考えました。実際、こんなコードを書きました。

csv_builder = UniFa::CsvBuilder.new(items)
csv_builder.bind('column1', &:foo)
csv_builder.bind('column2') { |it| it.bar }
csv_builder.bind('column3') do |it|
  do_something_with(it)
end
csv_builder.build

列の名前と内容をくっつけることによってわかりやすくようになったんではないでしょうか。列の数が増えても、修正する箇所は一箇所だけなので、データの漏れや順番の間違いがありません。

CsvBuilder

class CsvBuilder
  # @param [Array] data
  def initialize(data)
    @data = data
    @bindings = []
  end

  # @param [String] header
  # @param [Proc] block
  def bind(header, &block)
    @bindings << [header, block]
  end

  # @return [String]
  def build
    ::CSV.generate(row_sep: "\r\n") do |csv|
      csv << @bindings.map {|b| b[0]} # headers
      @data.each do |item|
        csv << @bindings.map {|b| b[1].call(item)}
      end
    end
  end
end

次はExcelの作成です。ここでは、spreadsheetというgemを使ってます。spreadsheet元々の使い方はこうなんです。

work_book = Spreadsheet::Workbook.new

work_sheet = work_book.create_worksheet(name: 'sheet1')
work_sheet.row(0).concat(["column1", "column2", "column3"])
items.each_with_index do |it, i|
  line = []
  line << it.foo
  line << it.bar
  line << it.baz
  work_sheet.row(i + 1).concat(line)
end

work_sheet = work_book.create_worksheet(name: 'sheet2')
work_sheet.row(0).concat(["column1", "column2", "column3"])
items2.each_with_index do |it, i|
  line = []
  line << it.foo
  line << it.bar
  line << it.baz
  work_sheet.row(i + 1).concat(line)
end

@work_book.write(output)

CSVと同じ列が増えれば二箇所を修正しないといけない、加えて複数のシートを取り扱うので、コードがCSVより複雑です。試しに、CsvBuilderのように、ExcelBuilderを作ってみました。

excel_builder = UniFa::SpreadsheetExcelBuilder.new
excel_builder.add_sheet('sheet1', items) do |sheet|
  sheet.bind('column1', &:foo)
  sheet.bind('column1', &:bar)
  sheet.bind('column1', &:baz)
end
excel_builder.add_sheet('sheet2', items2) do |sheet|
  sheet.bind('column1', &:foo)
  sheet.bind('column1', &:bar)
  sheet.bind('column1', &:baz)
end
excel_builder.build

シートが何枚があっても、分かりやすいではないでしょうか。

SpreadsheetExcelBuilder

class SpreadsheetExcelBuilder
  class Sheet
    def initialize(work_sheet, data)
      @work_sheet = work_sheet
      @data = data
      @bindings = []
    end

    def bind(header, &block)
      @bindings << [header, block]
    end

    def apply
      @work_sheet.row(0).concat(@bindings.map {|b| b[0]}) # headers
      @data.each_with_index do |item, i|
        @work_sheet.row(i + 1).concat(@bindings.map {|b| b[1].call(item)})
      end
    end
  end

  def initialize
    @work_book = Spreadsheet::Workbook.new
  end

  # @param [String] title
  # @param [Array] data
  def add_sheet(title, data)
    work_sheet = @work_book.create_worksheet(name: title)
    sheet = Sheet.new(work_sheet, data)
    yield sheet
    sheet.apply
  end

  def build(path = nil)
    if path
      @work_book.write(path)
    else
      io = ::StringIO.new
      @work_book.write(io)
      io.string
    end
  end
end

いかがでしょうか。CSVとExcelを作成するとき、メンテナンスしやすいように、こういう書き方を試してみませんか。