2週間で身につく、アプリを作りながら学ぶRubyの基本 12日目
12日目のゴールとテーマ
12日目のテーマは「タスク管理アプリを“使いやすくする”:絞り込み・並べ替え・完了タスクの扱い」です。
11日目までで、タスクに締め切りを持たせ、ファイル保存・読み込みもできるようになりました。
今日はそこから一歩進めて、
未完了タスクだけを表示する
完了済みタスクだけを表示する
締め切りが近いタスクを上に表示する(並べ替え)
という、「実際に使うときに欲しくなる操作」を追加していきます。
いまのタスク管理アプリを整理する
すでにできていることを言葉で確認する
ここまでで、あなたのタスク管理アプリはだいたい次のような状態です。
Task クラス
title, memo, deadline, done を持つ。
summary で一覧用の1行表示、detail で詳細表示を返す。
mark_done / mark_undone で完了状態を切り替えられる。
to_csv_line / task_from_csv_line で保存・復元ができる。
tasks 配列
Task オブジェクトの集まりとして、タスク一覧を表す。
メニューとメインループ
「一覧表示」「追加」「完了にする」「終了」などが選べる。
起動時に load_tasks、終了時に save_tasks を呼んでいる。
今日はこの土台の上に、「絞り込み」と「並べ替え」を乗せていきます。
未完了タスクだけを表示する機能
まずは“欲しい動き”を日本語で決める
やりたいことを先に言葉にしておきます。
タスク一覧の中から、done が false のものだけを取り出す
それを一覧表示する
タスクが1件もなければ、その旨を表示する
Ruby では、配列の select を使うと「条件に合うものだけ」を取り出せます。
unfinished_tasks = tasks.select { |task| task.done == false }
Rubyあるいは、もっと Ruby らしく書くならこうです。
unfinished_tasks = tasks.reject { |task| task.done }
Rubyreject は「条件が true のものを除外する」メソッドなので、
「done が true(完了済み)のものを除いて、残り(未完了)だけを取る」という意味になります。
メニューに項目を追加する
メニューを少し拡張します。
def show_menu
puts "========================"
puts "タスク管理アプリ メニュー"
puts "1: すべてのタスクを表示"
puts "2: 新しいタスクを追加"
puts "3: タスクを完了にする"
puts "4: 未完了のタスクだけ表示"
puts "5: 完了済みのタスクだけ表示"
puts "0: 終了"
puts "番号を入力してください:"
end
Rubyメインループ側も対応します。
elsif choice == 4
handle_show_unfinished(tasks)
elsif choice == 5
handle_show_done(tasks)
Ruby未完了タスク表示の処理を書く
def handle_show_unfinished(tasks)
unfinished = tasks.reject { |task| task.done }
if unfinished.empty?
puts "未完了のタスクはありません。"
return
end
puts "========================"
puts "未完了のタスク一覧:"
unfinished.each_with_index do |task, idx|
puts "#{idx + 1}. #{task.summary}"
end
end
Rubyここで深掘りしたいポイントは二つです。
一つ目は、reject の使い方です。tasks.reject { |task| task.done } は、「done が true のタスクを除いた配列」を返します。
つまり、done が false(未完了)のタスクだけが残ります。
二つ目は、「元の tasks を壊していない」ことです。
unfinished は新しい配列で、tasks 自体はそのままです。
表示のために一時的に絞り込んでいるだけ、というのが大事な感覚です。
完了済みタスクだけを表示する機能
完了済みだけを取り出す
今度は逆に、「done が true のタスクだけ」を取り出します。
ここでは select を使います。
def handle_show_done(tasks)
done_tasks = tasks.select { |task| task.done }
if done_tasks.empty?
puts "完了済みのタスクはありません。"
return
end
puts "========================"
puts "完了済みのタスク一覧:"
done_tasks.each_with_index do |task, idx|
puts "#{idx + 1}. #{task.summary}"
end
end
Rubyここでの重要ポイントは、
未完了と完了済みで「select と reject を使い分けている」ことです。
未完了 → reject { |task| task.done }
完了済み → select { |task| task.done }
どちらも「条件に基づいて配列を絞り込む」という同じパターンです。
このパターンを体で覚えておくと、他のアプリでもすぐに使えます。
締め切りが近い順に並べ替える
まずは「締め切りをどう比較するか」を決める
締め切り(deadline)は、今は文字列として扱っています。
“2025-03-20” のような形式であれば、Date クラスに変換して比較できます。
ここでは、次のような方針にします。
deadline が空のタスクは「締め切りなし」として、いちばん後ろに回す
deadline があるタスク同士は、日付として早い順に並べる
Ruby には Date クラスがあり、require “date” で使えます。
require "date"
d = Date.parse("2025-03-20")
Rubyただし、変な文字列を渡すと例外が出るので、
「パースに失敗したら締め切りなし扱いにする」という保険も必要です。
Taskに「締め切りを日付として解釈するメソッド」を足す
require "date"
class Task
# 既存の定義に続けて…
def deadline_date
return nil if @deadline.nil? || @deadline.strip == ""
begin
Date.parse(@deadline)
rescue ArgumentError
nil
end
end
end
Rubyここで深掘りしたいポイントは三つです。
一つ目は、「nil を返す」という設計です。
締め切りが空、またはパースできない場合は nil を返します。
nil は「日付として扱えない(締め切りなし)」という意味になります。
二つ目は、Date.parse を begin…rescue で囲んでいることです。
“明日” のような文字列を渡すと ArgumentError が出るので、
rescue で受け止めて nil を返しています。
これで、ユーザーが自由に書いてもアプリが落ちにくくなります。
三つ目は、「Task 自身が自分の締め切りをどう解釈するかを知っている」ことです。
外側のコードは、deadline_date を呼ぶだけでよくなります。
締め切り順に並べ替えて表示する
並べ替えのルールをコードに落とす
締め切り順に並べるメニュー項目を追加します。
puts "6: 締め切りが近い順にタスクを表示"
Rubyメインループ側も対応します。
elsif choice == 6
handle_show_sorted_by_deadline(tasks)
Ruby処理本体を書きます。
def handle_show_sorted_by_deadline(tasks)
if tasks.empty?
puts "タスクがまだ登録されていません。"
return
end
sorted = tasks.sort_by do |task|
date = task.deadline_date
if date
[0, date]
else
[1, Date.new(9999, 12, 31)]
end
end
puts "========================"
puts "締め切りが近い順のタスク一覧:"
sorted.each_with_index do |task, idx|
puts "#{idx + 1}. #{task.summary}"
end
end
Rubyここが今日いちばん“深掘りしたい”ポイントです。
sort_by のブロックで配列 [0, date] や [1, Date.new(...)] を返しているのは、
「並べ替えの優先順位を二段階にしている」からです。
締め切りがあるタスク → [0, date]
締め切りがないタスク → [1, 9999-12-31]
sort_by は、まず配列の1番目の要素(0 or 1)で比較し、
同じなら2番目の要素(date)で比較します。
つまり、
締め切りがあるタスク(0, 日付)
締め切りがないタスク(1, 9999-12-31)
という順番になり、
締め切りがあるタスクが先に並び、その中では日付の早い順になります。
「締め切りなしをいちばん後ろに回す」というルールを、
この [0, date] / [1, 9999-12-31] という形で表現しているわけです。
完了状態を切り替える機能を少しリッチにする
「完了にする」だけでなく「未完了に戻す」も欲しくなる
使っていると、こう思うはずです。
一度完了にしたタスクを「やっぱり未完了に戻したい」
完了・未完了をトグル(切り替え)したい
そこで、「完了状態を反転させる」機能を足してみます。
メニューに項目を追加します。
puts "7: タスクの完了状態を切り替える"
Rubyメインループ側も対応します。
elsif choice == 7
handle_toggle_task_done(tasks)
Ruby処理本体を書きます。
def handle_toggle_task_done(tasks)
if tasks.empty?
puts "まだタスクが登録されていません。"
return
end
puts "完了状態を切り替えたいタスクの番号を選んでください。"
tasks.each_with_index do |task, idx|
puts "#{idx + 1}. #{task.summary}"
end
print "番号:"
number_text = gets&.chomp.to_s
unless number_text.match?(/\A[0-9]+\z/)
puts "数字で入力してください。"
return
end
index = number_text.to_i - 1
if index < 0 || index >= tasks.length
puts "その番号のタスクは存在しません。"
return
end
task = tasks[index]
if task.done
task.mark_undone
puts "タスクを未完了に戻しました:#{task.summary}"
else
task.mark_done
puts "タスクを完了にしました:#{task.summary}"
end
end
Rubyここでの重要ポイントは、「状態によって呼ぶメソッドを変えている」ことです。
task.done が true なら mark_undone
task.done が false なら mark_done
このように、「今の状態を見て、次の状態を決める」というのは、
アプリ全体でよく出てくるパターンです。
12日目で到達した“使いやすさ”を俯瞰する
いまのタスクアプリができること
12日目の時点で、あなたのタスク管理アプリはこうなっています。
Task クラスで、タイトル・メモ・締め切り・完了状態を表現している
タスクを追加・一覧表示・完了にすることができる
未完了だけ、完了済みだけを絞り込んで表示できる
締め切りが近い順に並べて表示できる
完了状態をトグル(完了⇔未完了)できる
タスク一覧をファイルに保存・読み込みできる
これはもう、「自分の生活を支える小さなツール」として十分に機能するレベルです。
12日目のまとめ
今日の大事なポイントを短く整理します。
配列の select / reject を使って、「条件に合うものだけ」「条件に合うもの以外」を取り出した。
Task#deadline_date で、締め切り文字列を Date に変換し、
パースできない場合は nil を返すようにした。
sort_by と「配列を返すキー」を組み合わせて、
「締め切りありを先、なしを後ろ」「その中で日付順」という並べ替えルールを表現した。
完了状態の切り替えでは、task.done を見て mark_done / mark_undone を呼び分けることで、
「今の状態に応じて次の状態を決める」パターンを体験した。
ここまで来ると、
「こういう条件で絞り込みたい」「こういう順番で並べたい」という欲求を、
Ruby のコードに落とし込む筋力がかなりついてきています。
次のステップでは、
App クラスにまとめて「タスクアプリ全体」を1つのオブジェクトにする
名簿アプリとタスクアプリを並べて、「共通パターン」を言語化する
といった方向に進むと、
「Rubyでアプリを設計する」という感覚がさらにクリアになっていきます。
