こんにちは、システム開発部のちょうです。今回は自分が最近の開発でもっとやりやすい方法で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を作成するとき、メンテナンスしやすいように、こういう書き方を試してみませんか。