Rubyで読書記録を集計してみよう(総合演習)
ここまでの Ruby レッスンで学んだことを全部つないで、動くものを1つ完成させます。新しい構文は出てきません。配列・ハッシュ・each・select・メソッド・if文——すでに練習したものだけで組み立てます。
作るのは読書記録の集計です。読んだページ数を足し、読み終わった本を数え、進捗を棒グラフにして、レポートの形に整えます。
データを集めて、絞り込んで、整えて見せる。この流れは、Rubyで書く仕事のほとんどに共通します。題材が読書記録か売上か在庫かの違いだけです。
このページではコードを実行していません。Rubyはブラウザの中では走らせられないためです。かわりにお手本と同じコードを書けたかを、その場で判定します(判定はあなたのブラウザの中だけで行われ、どこにも送信されません)。
表示している実行結果は本物です。このページを作るときに、実際のRubyで走らせて得た出力をそのまま載せています。
完成イメージ
最後まで進むと、このコードが完成します。下の実行結果は、このページを作るときに開発機で実際にコードを走らせて得たものです(このページ上では実行していません)。
===== 今年の読書記録 ===== はじめての統計学 #################### 100% 小さな家の設計 #################### 100% デザインの教科書 ########------------ 41% 土を育てる暮らし #################### 100% 夜明けの航海記 ######-------------- 33% ---------------------------------------- 記録した本 : 5冊 読み終えた本: 3冊(達成率 60.0%) 読んだページ: 1026ページ いちばん厚い本: デザインの教科書(432ページ) 読みかけが2冊あります。
STEP 1記録をハッシュの配列で表す
🎯 このステップのゴール: 本1冊をハッシュにし、それを並べた配列を作る
まずデータの形を決めます。ここを適当に決めると、あとの処理が全部やりにくくなります。
本1冊が持つのは「タイトル・総ページ数・読んだページ数・読んだ月」の4つ。Rubyではハッシュ——{ title: "...", pages: 184 }のように名前と値の組で書ける入れ物——を使います。
title:という書き方はシンボルという種類の名前です。文字列と似ていますが、同じ名前なら同じものが使い回されるので軽く、ハッシュのキーにはこちらを使うのが Ruby の習慣です。取り出すときはbook[:title]と書きます。
そして1年ぶんには何冊も入るので、ハッシュを並べた配列にします。この「ハッシュの配列」という形は、Rubyで表形式のデータを扱うときの基本形です。CSVを読み込んでも、データベースから引いてきても、たいていこの形になります。
books.lengthで件数を数えて、正しく作れたか確かめます。books[0]が1冊目——添字は0から始まるので、1冊目が[0]です。
記録した本: 5冊 1冊目: はじめての統計学
STEP 2読んだページ数を合計してメソッドにする
🎯 このステップのゴール: eachで足し上げ、メソッドとして切り出す
読んだページ数を全部足します。eachで1冊ずつ取り出して、sumに足していきます。
足し始める前にsum = 0と入れ物を用意するのを忘れないでください。ここを書き忘れるとundefined local variableで止まります。初心者がいちばん多く踏む場所です。
そしてこれをメソッドに切り出します。def total_read(books)——「読んだページ数の合計」という名前が付いた瞬間、中身を読まなくても何をするか分かるようになります。これが関数に切り出すいちばんの効果です。
Rubyのメソッドで特徴的なのが戻り値にreturnを書かないことです。最後に評価した式が、そのまま戻り値になります。だから末尾のsumという1行が「これを返す」という意味になります。
return sumと書いても同じように動きますが、Rubyでは書かないのが普通です。人のコードを読むときに戸惑わないよう、この形に慣れておいてください。
なお Ruby にはbooks.sum { |b| b[:read] }という短い書き方もあります。まずはeachで仕組みを掴んでから、そちらに移るとよいでしょう。
読んだページ数: 1026ページ
STEP 3読み終わった本だけを選び出す
🎯 このステップのゴール: selectで絞り込み、達成率を小数1桁で出す
「読み終わった本」だけを取り出します。条件は読んだページ数が総ページ数と同じ——book[:read] == book[:pages]です。
ここでselectが効きます。books.select { |book| 条件 }と書くと、条件が真になった要素だけを集めた新しい配列が返ります。
eachとの違いをはっきりさせておきます。eachは「1つずつ何かをする」だけで、結果を集めません。selectは集めた配列を返すので、そのまま次の処理に渡せます。元のbooksは変わりません——新しい配列が作られるだけです。
次が達成率です。ここにRubyで最も引っかかりやすい罠があります。3 / 5と書くと0になります。整数どうしの割り算は、小数点以下が切り捨てられるからです。
だから.to_fで片方を小数にしてから割ります。3.to_f / 5なら0.6です。割り算で答えが0になったら、まずこれを疑ってください。
表示はformat('%.1f', rate)で小数第1位までに丸めます。そのまま出すと60.00000000000001のような値が見えることがあり、読みにくくなります。
読了: 3冊 / 5冊 達成率: 60.0%
STEP 4いちばん厚い本を探す
🎯 このステップのゴール: eachで比べながら、最大のものを1つ選び出す
次はいちばんページ数の多い本を探します。「合計する」の次によく出てくる、「いちばん〇〇なものを選ぶ」という処理です。
やり方は決まっています。まず1冊目を暫定の1位にしておき、残りを順に比べて、勝ったら入れ替える。これだけです。
best = books[0]で暫定の1位を決め、eachで回しながらbook[:pages] > best[:pages]ならbestを差し替えます。
best = books[0]から始めるのが要点です。best = nilやbest = 0から始めると、比較のたびに「まだ入っていない場合」を考えなければならず、面倒になります。最初の1件を答えにしてしまえば、その面倒が消えます。
best = book if 条件という書き方は後置ifと呼ばれるRubyらしい書き方です。「〇〇する、もし△△なら」と英語の語順で読めます。if〜endで3行に書いても同じですが、1行で収まるならこちらが読みやすくなります。
なおbooks.max_by { |b| b[:pages] }という短い書き方もあります。仕組みが分かってから使うと、意味が透けて見えて安心です。
いちばん厚い本: デザインの教科書(432ページ)
STEP 5進捗を棒グラフにして桁を揃える
🎯 このステップのゴール: 文字の繰り返しでバーを作り、formatで表を整える
数字が並んでいるだけでは、進み具合が一目で分かりません。文字で棒グラフを描きます。
Rubyでは"#" * 5と書くと"#####"になります。掛け算が文字列にも使えるのです。これを利用して、読んだぶんを#、残りを-で埋めれば棒グラフになります。
長さの計算がread * BAR_WIDTH / pagesです。掛け算を先に書くのが大事です。read / pages * BAR_WIDTHと書くと、整数の割り算で先に0になってしまい、バーが常に空になります。ステップ3で見た罠が、ここでも顔を出します。
バーの幅はBAR_WIDTH = 20と定数にしました。20を直接書くと、幅を変えたいときに2か所も3か所も直すことになります。大文字で始まる名前は定数——Rubyでは書き換えようとすると警告が出ます。
表の桁揃えはformatです。%-10sは「左揃えで10文字ぶん」、%3dは「右揃えで3桁の整数」、%%はパーセント記号そのものを出すための書き方です。%1つだと書式指定の始まりと解釈されてしまうので、2つ重ねます。
はじめての統計学 #################### 100% 小さな家の設計 #################### 100% デザインの教科書 ########------------ 41% 土を育てる暮らし #################### 100% 夜明けの航海記 ######-------------- 33%
STEP 6ひとつのメソッドにまとめて仕上げる
🎯 このステップのゴール: 全部を report にまとめ、締めのひとことまで出す
最後の仕上げです。ここまでの部品をreportという1つのメソッドにまとめます。
並べる順番に意味を持たせます。まず1冊ずつの進捗、次に区切り線、最後に全体のまとめ——細かいものから大きいものへという流れです。レシートも決算書も、たいていこの順に並んでいます。
"-" * 40で区切り線を引きます。ステップ5の"#" * filledと同じ仕組みで、覚えたことがそのまま別の場所で効いています。
そして最後のひとことをifで分けます。全部読み終わっていれば「おつかれさまでした」、そうでなければ残りが何冊あるかを出します。
この一文があるかないかで、道具としての手触りが変わります。数字だけを出して終わるのではなく、「それで、どうなの?」に答える——使う人のことを考えたプログラムは、たいていここが違います。
完成です。booksの中身を書き換えて、全部を読了にしてみてください。達成率が100.0%になり、締めの言葉のほうが表示されるはずです。
バーの幅BAR_WIDTHを40に変えると、グラフだけが伸びます。1か所直すだけで全体が変わる——定数に切り出しておいた意味が、ここで確かめられます。
===== 今年の読書記録 ===== はじめての統計学 #################### 100% 小さな家の設計 #################### 100% デザインの教科書 ########------------ 41% 土を育てる暮らし #################### 100% 夜明けの航海記 ######-------------- 33% ---------------------------------------- 記録した本 : 5冊 読み終えた本: 3冊(達成率 60.0%) 読んだページ: 1026ページ いちばん厚い本: デザインの教科書(432ページ) 読みかけが2冊あります。
よくある質問
このページではコードを実行しないのですか?
していません。Rubyはブラウザの中では動かせない言語だからです。このページではお手本と同じコードを書けたかどうかを判定する形にしています(判定はあなたのブラウザの中だけで行われ、コードはどこにも送信されません)。
ただし表示している実行結果は本物です。このページを作るときに実際のRubyで走らせて得た出力を、そのまま載せています。自分のパソコンにRubyを入れれば、まったく同じ結果が出ます。
割り算の答えが0になってしまいます
整数どうしを割っているからです。Rubyでは3 / 5は0.6ではなく0になります。小数点以下が切り捨てられる決まりだからです。
3.to_f / 5のように片方を小数にしてから割ります。3.0 / 5と書いても同じです。
逆に切り捨てを利用したい場面もあります。ステップ5のバーの長さは、あえて整数の割り算のままにしています——文字数は整数でなければ意味がないからです。
each と select と map は、どう使い分けますか
「何を返してほしいか」で決まります。
eachは何も集めません。表示するだけ、外の変数に足すだけ、というときに使います。
selectは条件に合う要素だけを集めた配列を返します。件数は減りますが、中身は元のままです。
mapは1つずつ変換した結果を集めた配列を返します。件数は変わらず、中身が変わります。タイトルだけの配列がほしいならbooks.map { |b| b[:title] }です。
日本語の桁揃えがきれいに揃いません
文字の幅が違うためです。%-10sは文字数で数えますが、日本語は1文字が英数字2つぶんの幅で表示されます。だから同じ10文字でも、見た目の長さが変わります。
等幅フォントの端末なら、日本語の文字数を2倍に数えて調整する手はあります。ただし厳密に揃えたいならHTMLの表を使うのが本筋です。文字で表を組むのは、あくまで手軽な確認用と考えてください。
🚩 次はこのカテゴリへ
同じ「集めて、絞って、整える」をPythonでも書いてみると、言語ごとの考え方の違いがはっきり見えてきます。Pythonの最終演習では家計簿アプリを作ります。リストと辞書はRubyの配列とハッシュにそっくりですが、内包表記など書き味の違うところがあり、比べると両方の理解が深まります。
Pythonのレッスンへ進む