Ruby | 2週間で身につく、アプリを作りながら学ぶRubyの基本 - 12日目

Ruby Ruby
スポンサーリンク

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 }
Ruby

reject は「条件が 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でアプリを設計する」という感覚がさらにクリアになっていきます。

タイトルとURLをコピーしました