- 開発
READ MORE
こんにちは @kazz です。長い梅雨が終わって暑い夏がやってきました。夏のTechRachoフェアということで、今日はみなさん1度は実装したことのあるCSV出力に関して語って見たいと思います。
今回は以下のテーブルをCSVに出力したいとおもいます。
class CreateArticles < ActiveRecord::Migration[5.2]
def change
create_table :articles do |t|
t.integer :year # 発行年
t.integer :no # 号数
t.string :title # タイトル
t.string :description # 内容
t.integer :price # 価格
t.timestamps
end
end
end
※ NOT NULLとかINDEXとかはご容赦ください
適当にデータを突っ込んだ画面です。
※新発売!はけんちゃんラーメン方式です
Articleのクラスメソッドで実装するならこんな実装になるかと思います。
class Article < ApplicationRecord
def self.csv_dump
header = %w[タイトル 内容 価格]
CSV.generate do |csv|
csv << header
all.each do |article|
row = []
row << article.title
row << article.description
row << article.price
csv << row
end
end
end
end
新発売!
じゃないか!いらないこんな感じで改修すると思います。
class Article < ApplicationRecord
def self.csv_dump
header = %w[年度 号数 タイトル 価格]
CSV.generate do |csv|
csv << header
all.each do |article|
row = [
article.year,
article.no,
article.title,
article.price
]
csv << row
end
end
end
end
…😢
このように仕様の変更が起こる度に、CSVの列に変更が入ります。
この実装例の場合、 header
と row
の両方を変更する必要があります。
列数が少なければ目視で確認できますが、列数が増えてくると取り違えする可能性が高くなります。
例えば私ならこういうCSVを作成してしまう可能性は120%自信があります(ドヤ
年度 | 号数 | タイトル | 販売数 | 価格 |
---|---|---|---|---|
2018 | 1 | 2018年01号 | 540 | 500 |
2018 | 2 | 2018年02号 | 540 | 482 |
2018 | 3 | 2018年03号 | 540 | 38 |
… |
※「販売数」と「価格」を取り違えた例。データ次第では気が付かない
ヘッダと、ヘッダに対応する各値の実装が分離しているから取り違えてしまいます。
ならば同時に書ければ取り違えは起こりません。
こんなコードにしてみました。
def self.dump_csv
rule = {
'年度' => lambda(&:year),
'号数' => lambda(&:no),
'タイトル' => lambda(&:title),
'価格' => lambda(&:price),
}
CSV.generate do |csv|
csv << rule.keys
all.each do |article|
csv << rule.values.map { |predicate| predicate.call(article) }
end
end
end
csv << rule.keys
でヘッダ部の出力になります。
csv << ['年度', '号数', 'タイトル', '価格']
と全く同じです
all.each do |article|
row << rule.values.map { |predicate| predicate.call(article) }
csv << row
end
row
にはあるarticle
をCSVの1行に変換した配列が入っていそうですが、具体的に何をしているんでしょう?詳しく見てみましょう
rule.values.map { |predicate| predicate.call(article) }
の部分ですが、まず rule.values.map
を書き下すと以下のようになります。
[
lambda(&:year).call(article),
lambda(&:no).call(article),
lambda(&:title).call(article),
lambda(&:price).call(article)
]
例えば先頭の lambda(&:year).call(article)
を書き下してみましょう。
lambda(&:year).call(article)
lambda
を書き下すと以下の用になるので
->(arg) { arg.year }.call(article)
call(article)
が実行されると
article.year
となります。
つまり全体では、結局以下のようになります。
[
article.year,
article.no,
article.title,
article.price
]
ヘッダ名とヘッダに対応する値の取得方法(述語)とを、予め対応づけしておくことで、列の変更に強い実装にすることができました。
列数が大量になるCSVを出力する羽目になった場合は是非お試しください
rule = [
[ '価格', lambda(&:price) ],
[ '価格', lambda(&:price_with_tax) ]
]
lambda(:&method)
の構文に拘ることはありませんよrule = {
'価格(税込)' => -> (article) { article.price * 1.08 }
}
-> {}
でlambda定義するのは宗教上の理由でできません!rule = {
'価格(税込)' => lambda(&:price) >> lambda(&:*).curry(2).call(1.08)
}
すごい良さそう / “Ruby: CSVでヘッダとボディを同時に定義するやり方” https://t.co/sMMuXq4rN7
— ゆーじ / Yuji Imagawa (@ug23_) August 6, 2019
知見 good. けんちゃんラーメンって世代によっては分からないかも。。 "Ruby: CSVでヘッダとボディを同時に定義するやり方" https://t.co/DmsAxjPN6l
— Masao S (@masawo) July 31, 2019
週刊Railsウォッチ(20190617-1/2前編)マルチプルデータベースガイドが追加、mmcと「Ruby 3の型解析に向けた計画」、Ruby 2.6のCSVライブラリはいいほか