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

Ruby Ruby
スポンサーリンク

11日目のゴールとテーマ

11日目のテーマは「タスク管理アプリに“記憶”と“締め切り”を与える」です。
10日目で、Task クラスとメニュー付きのタスク管理アプリの骨格ができました。

今日はそこから一歩進めて、

タスクをファイルに保存・読み込みできるようにする
Task に「締め切り日(deadline)」を追加する
締め切りつきで表示を少しリッチにする

という流れで、「自分で使えるタスクアプリ」に近づけていきます。


10日目までのタスク管理アプリを整理する

いま持っている部品を言葉で確認する

ここまでで、だいたい次のような構成になっているはずです。

Task クラス
タイトル(title)、メモ(memo)、完了フラグ(done)を持つ。
summary で一覧用の1行表示、detail で詳細表示を返す。
mark_done / mark_undone で完了状態を切り替えられる。

tasks 配列
Task オブジェクトを複数まとめて持つ「タスク一覧」。

メニューとメインループ
「一覧表示」「追加」「完了にする」「終了」が選べる。

今日はここに「保存」「読み込み」「締め切り」を足していきます。


Taskに「締め切り日」を追加する

まずは“どう扱いたいか”を決める

締め切り日(deadline)をどう扱うか、先に決めておきます。

文字列として扱う(例: “2025-03-20” や “明日” でもOK)
必須ではなく「空でもよい」(締め切りなしタスクも許す)
表示のときに「締め切り: ○○」と出したい

最初から日付型や厳密なバリデーションを入れると難しくなるので、
11日目では「締め切りは文字列で、あくまでメモ」として扱います。

Taskクラスを書き換える

class Task
  attr_accessor :title, :memo, :done, :deadline

  def initialize(title, memo, deadline = "", done = false)
    @title    = title
    @memo     = memo
    @deadline = deadline
    @done     = done
  end

  def mark_done
    @done = true
  end

  def mark_undone
    @done = false
  end

  def done_mark
    @done ? "[x]" : "[ ]"
  end

  def summary
    if @deadline.nil? || @deadline.strip == ""
      "#{done_mark} #{@title}"
    else
      "#{done_mark} #{@title}(締め切り: #{@deadline})"
    end
  end

  def detail
    text = ""
    text += "#{done_mark} タイトル: #{@title}\n"
    text += "メモ: #{@memo}\n"
    if @deadline && @deadline.strip != ""
      text += "締め切り: #{@deadline}\n"
    else
      text += "締め切り: (なし)\n"
    end
    text
  end
end
Ruby

ここで深掘りしたいポイントは三つです。

一つ目は、initialize の引数の順番です。
title, memo は必須、deadline は任意、done も任意(デフォルト false)にしています。
Task.new(“買い物”, “牛乳を買う”) のように呼べば、締め切りなし・未完了のタスクになります。

二つ目は、summary の条件分岐です。
締め切りが空文字やスペースだけなら、締め切り表示を省略しています。
締め切りがあるときだけ「(締め切り: ○○)」を付けることで、一覧が見やすくなります。

三つ目は、detail で「締め切りなし」の場合も明示していることです。
「締め切り: (なし)」と出すことで、「設定されていないだけ」と分かります。


入力から締め切りを受け取る

build_task_from_inputを拡張する

締め切りを入力できるように、タスク入力メソッドを少し変えます。

def read_non_empty_line(message)
  loop do
    puts message
    input = gets
    return "" if input.nil?

    text = input.chomp

    if text.strip == ""
      puts "空では登録できません。何か入力してください。"
    else
      return text
    end
  end
end

def build_task_from_input
  title = read_non_empty_line("タスクのタイトルを入力してください:")
  memo  = read_non_empty_line("タスクのメモ(詳細)を入力してください:")

  puts "締め切りを入力してください(未入力なら締め切りなし):"
  deadline_input = gets
  deadline = deadline_input ? deadline_input.chomp : ""

  Task.new(title, memo, deadline)
end
Ruby

ここでの重要ポイントは、「締め切りは空でもOK」にしていることです。

タイトルとメモは read_non_empty_line で空を禁止している
締め切りは gets でそのまま受け取り、空なら空文字のまま Task に渡す

このくらいの“ゆるさ”から始めると、実装のハードルが下がります。


タスクをファイルに保存する方針を決める

どんな形式で保存するか

名簿アプリと同じように、「1タスク1行」のテキスト形式にします。

1行の中に「タイトル,メモ,締め切り,完了フラグ」をカンマ区切りで入れる
完了フラグは “0”(未完了)か “1”(完了)で表現する

例えば、こんな感じです。

Rubyの勉強,11日目をやる,2025-03-20,0
部屋の片付け,机の上をきれいにする,,1

二行目の締め切りが空なのは、「締め切りなし」を表しています。


Taskを「保存用の1行」に変換する

Taskに保存用メソッドを追加する

class Task
  # さきほどの定義に続けて…

  def to_csv_line
    done_flag = @done ? "1" : "0"
    "#{@title},#{@memo},#{@deadline},#{done_flag}"
  end
end
Ruby

ここでの重要ポイントは、done をそのまま true/false で保存しないことです。

ファイルは文字列しか扱えないので、
“1” と “0” に変換して保存しています。

読み込むときに、”1″ なら true、”0″ なら false に戻します。


ファイルからTaskを復元する

1行からTaskを作るメソッド

def task_from_csv_line(line)
  text = line.chomp
  parts = text.split(",")

  title    = parts[0] || ""
  memo     = parts[1] || ""
  deadline = parts[2] || ""
  done_str = parts[3] || "0"

  done = (done_str == "1")

  Task.new(title, memo, deadline, done)
end
Ruby

ここで深掘りしたいポイントは二つです。

一つ目は、配列 parts の要素が足りない場合の保険です。
|| ""|| "0" を入れておくことで、
多少フォーマットが崩れていても最低限動くようにしています。

二つ目は、done の復元です。
“1” なら true、そうでなければ false としています。
このように、「保存用の表現」と「アプリ内の表現」を変換するのがポイントです。


タスク一覧をファイルに保存・読み込みする

保存メソッドを書く

TASK_DATA_FILE = "tasks_data.txt"

def save_tasks(tasks)
  File.open(TASK_DATA_FILE, "w") do |file|
    tasks.each do |task|
      file.puts task.to_csv_line
    end
  end
  puts "タスクをファイルに保存しました。(#{TASK_DATA_FILE})"
end
Ruby

重要なのは、Task#to_csv_line に責任を任せていることです。
save_tasks は「全タスクを1行ずつ書く」ことだけを考えればよくなります。

読み込みメソッドを書く

def load_tasks
  tasks = []

  unless File.exist?(TASK_DATA_FILE)
    puts "タスクの保存ファイルがまだありません。(初回起動かもしれません)"
    return tasks
  end

  begin
    File.open(TASK_DATA_FILE, "r") do |file|
      file.each_line do |line|
        next if line.strip == ""
        task = task_from_csv_line(line)
        tasks << task
      end
    end
    puts "タスクをファイルから読み込みました。(#{tasks.length}件)"
  rescue => e
    puts "タスク読み込み中にエラーが発生しました。"
    puts "エラー内容: #{e.class} #{e.message}"
  end

  tasks
end
Ruby

ここでの重要ポイントは、「エラーが起きてもアプリ全体を止めない」ことです。

ファイルが壊れていても、
「読み込みに失敗したので、空のタスク一覧から始めます」といった振る舞いにできます。


メインループに保存・読み込みを組み込む

起動時に読み込み、終了時に保存する

タスク管理アプリのメイン部分を、こう書き換えます。

tasks = load_tasks

loop do
  show_menu
  choice = read_menu_number

  if choice == 0
    puts "アプリを終了します。"
    save_tasks(tasks)
    break
  elsif choice == 1
    handle_show_tasks(tasks)
  elsif choice == 2
    handle_add_task(tasks)
  elsif choice == 3
    handle_mark_task_done(tasks)
  else
    puts "不正な番号です。もう一度入力してください。"
  end
end
Ruby

これで、

アプリ起動時に前回のタスク一覧を復元する
アプリ終了時に最新のタスク一覧を保存する

という流れができあがります。


締め切りつきタスク一覧を眺めてみる

実行イメージを頭に描く

例えば、次のようなタスクを登録したとします。

タイトル: Rubyの勉強
メモ: 11日目をやる
締め切り: 2025-03-20

タイトル: 部屋の片付け
メモ: 机の上だけでもきれいにする
締め切り: (未入力)

一覧表示はこんな感じになります。

========================
タスク一覧:
1. [ ] Rubyの勉強(締め切り: 2025-03-20)
2. [ ] 部屋の片付け

detail を表示するような機能を足せば、こうなります。

[x] タイトル: Rubyの勉強
メモ: 11日目をやる
締め切り: 2025-03-20

ここで感じてほしいのは、

締め切りがあるタスクとないタスクが、自然に混ざって表示できている
完了状態も [ ] / [x] で一目で分かる
保存しても、次回起動時に同じ状態が再現される

という「小さいけれどちゃんとしたツール感」です。


11日目のまとめ

今日の大事なポイントを短く整理します。

Task に deadline を追加し、「締め切りつきタスク」を表現できるようにした。
締め切りはまず文字列として扱い、「あるか・ないか」だけを意識した。
Task#to_csv_line と task_from_csv_line で、
「アプリ内の表現」と「保存用の1行」を相互変換できるようにした。
save_tasks / load_tasks で、タスク一覧をファイルに保存・読み込みできるようにした。
起動時に load_tasks、終了時に save_tasks を呼ぶことで、
アプリに“記憶”が宿った。

ここまで来ると、
「自分の生活のために書いたタスクアプリ」が
本当に日々の中で使えるレベルに近づいてきます。

次のステップでは、

締め切りが近いタスクだけを表示する
完了済みを非表示にするモードを作る
優先度(高・中・低)を持たせる

といった方向に広げていくこともできます。

あなたが今、「自分ならどんなタスクアプリを使いたいか」を
少し具体的にイメージできているなら、
それはもう立派に“Rubyで自分の道具を作り始めている人”です。

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