Day 77 のゴールと全体像
Day 77 では、タスク管理アプリに Search(検索)・Filter(絞り込み)機能を追加していきます。 ここまでで、タスクの追加・一覧・編集・削除といった基本操作は一通り揃いました。 しかし、タスクが増えてくると、一覧画面にずらっと並んだタスクの中から、
- 「今日やるべきタスクだけ見たい」
- 「未完了のタスクだけ知りたい」
- 「特定のキーワードを含むタスクを探したい」
といったニーズが自然と出てきます。
そこで登場するのが 検索・フィルター機能です。 今日は、初心者の方でも「なるほど、こうやって絞り込むのか」と腑に落ちるように、 ステップバイステップで実装していきます。
検索・フィルター機能の考え方を整理する
「検索」と「フィルター」の違い
まず、ざっくり言葉の整理をしておきます。
- 検索(Search) キーワードなどを使って、 「条件に合うものだけを探す」イメージです。 例:タイトルに「レポート」を含むタスクを探す
- フィルター(Filter) 一覧の中から、 「特定の条件に合うものだけを表示する」イメージです。 例:未完了のタスクだけ表示する、優先度が高いものだけ表示する
実装としてはどちらも「条件に合うタスクだけを取り出す」という点で同じですが、 ユーザーにとっての使い方のイメージが少し違います。
今日は、
- キーワード検索(タイトル・詳細に含まれる文字列で探す)
- 状態フィルター(完了/未完了)
- 優先度フィルター(高・中・低)
あたりを中心に作っていきます。
TaskRepository に検索・フィルターメソッドを追加する
キーワード検索メソッド
まずは、タイトルや詳細にキーワードを含むタスクを探すメソッドを作ります。
using System;
using System.Collections.Generic;
using System.Linq;
// タスク一覧を管理するクラス
class TaskRepository
{
private readonly List<TaskItem> _tasks = new List<TaskItem>();
private int _nextId = 1;
public TaskItem Add(string title, string description, int priority, DateTime? dueDate)
{
var task = new TaskItem
{
Id = _nextId++,
Title = title,
Description = description,
Priority = priority,
DueDate = dueDate,
IsCompleted = false,
CreatedAt = DateTime.Now
};
_tasks.Add(task);
return task;
}
public IEnumerable<TaskItem> GetAll()
{
return _tasks;
}
public TaskItem FindById(int id)
{
return _tasks.FirstOrDefault(t => t.Id == id);
}
// キーワード検索:タイトルまたは詳細にキーワードを含むタスクを返す
public IEnumerable<TaskItem> SearchByKeyword(string keyword)
{
if (string.IsNullOrWhiteSpace(keyword))
{
// キーワードが空なら、全件返すことにしてもよい
return _tasks;
}
keyword = keyword.ToLower(); // 大文字・小文字を区別しないようにする
return _tasks.Where(t =>
(!string.IsNullOrEmpty(t.Title) && t.Title.ToLower().Contains(keyword)) ||
(!string.IsNullOrEmpty(t.Description) && t.Description.ToLower().Contains(keyword))
);
}
// 完了状態でフィルターする
public IEnumerable<TaskItem> FilterByCompletion(bool isCompleted)
{
return _tasks.Where(t => t.IsCompleted == isCompleted);
}
// 優先度でフィルターする
public IEnumerable<TaskItem> FilterByPriority(int priority)
{
return _tasks.Where(t => t.Priority == priority);
}
}
C#重要ポイントの解説
SearchByKeywordのキーワードチェック 空文字やスペースだけの入力の場合は、 「全件返す」という挙動にしておくと扱いやすいです。- 大文字・小文字を区別しない検索
ToLower()を使って、 タスク側・キーワード側の両方を小文字に揃えてからContainsしています。 これにより、「Report」と「report」の違いを気にせず検索できます。 - タイトルと詳細の両方を対象にしていること タイトルだけでなく、詳細(Description)も検索対象にすることで、 「メモに書いたキーワード」でもヒットするようになります。
- フィルターメソッドのシンプルさ
FilterByCompletionとFilterByPriorityは、 それぞれWhereで条件に合うものだけを返す、 とてもシンプルなメソッドです。
検索結果を表示するための共通メソッドを作る
検索やフィルターの結果を表示するとき、 一覧表示とほぼ同じロジックを使います。
そこで、「タスクのコレクションを表示する共通メソッド」を作っておくと便利です。
void ShowTaskList(IEnumerable<TaskItem> tasks, string header)
{
Console.WriteLine(header);
var list = tasks.ToList();
if (!list.Any())
{
Console.WriteLine("条件に合うタスクがありませんでした。");
return;
}
foreach (var t in list)
{
string status = t.IsCompleted ? "[完了]" : "[未完了]";
string priorityText = t.Priority switch
{
1 => "高",
2 => "中",
3 => "低",
_ => "不明"
};
string dueText = t.DueDate.HasValue
? t.DueDate.Value.ToString("yyyy-MM-dd")
: "期限なし";
Console.WriteLine($"ID: {t.Id}");
Console.WriteLine($" 状態 : {status}");
Console.WriteLine($" 優先度 : {priorityText}");
Console.WriteLine($" 期限 : {dueText}");
Console.WriteLine($" タイトル: {t.Title}");
Console.WriteLine($" 詳細 : {t.Description}");
Console.WriteLine();
}
}
C#共通メソッドにするメリット
- 一覧表示・検索結果表示・フィルター結果表示で 同じ見た目を簡単に再利用できる
- 表示形式を変えたいときに、 このメソッドだけ直せば全部に反映される
こういう「共通化」は、 アプリが少しずつ大きくなってきたときに、 コードの見通しを良くしてくれる大事な考え方です。
キーワード検索機能を作る
キーワード検索の流れ
- キーワードを入力してもらう
SearchByKeywordで該当タスクを取得する- 共通表示メソッドで結果を表示する
この流れをコードにしてみます。
void SearchTaskByKeyword()
{
Console.WriteLine("=== キーワード検索 ===");
Console.Write("検索したいキーワードを入力してください: ");
string keyword = Console.ReadLine();
// リポジトリに検索を依頼
var result = repository.SearchByKeyword(keyword);
// 結果を表示
ShowTaskList(result, "=== 検索結果 ===");
}
C#ここで意識していること
- キーワードが空でも
SearchByKeyword側で全件返すようにしているので、 「とりあえず Enter を押したら全部出てくる」という挙動も自然です。 - 表示部分は
ShowTaskListに任せることで、 検索機能のコードがとてもスッキリしています。
完了状態フィルター機能を作る
未完了/完了だけを見たいとき
「終わっていないタスクだけ見たい」 「完了したタスクだけ確認したい」
というニーズは、タスク管理ではよくあります。
これをコードにすると、次のようになります。
void FilterTasksByCompletion()
{
Console.WriteLine("=== 完了状態で絞り込み ===");
Console.WriteLine("1: 未完了のタスクのみ表示");
Console.WriteLine("2: 完了したタスクのみ表示");
Console.Write("選択してください: ");
string input = Console.ReadLine();
bool isCompleted;
if (input == "1")
{
isCompleted = false; // 未完了
}
else if (input == "2")
{
isCompleted = true; // 完了
}
else
{
Console.WriteLine("不正な入力です。");
return;
}
var result = repository.FilterByCompletion(isCompleted);
string header = isCompleted
? "=== 完了したタスク一覧 ==="
: "=== 未完了のタスク一覧 ===";
ShowTaskList(result, header);
}
C#ポイント
- ユーザーには「1/2」で選んでもらい、 内部では
boolに変換して処理しています。 - 見出し(header)を状態に応じて変えることで、 「今何を見ているのか」が分かりやすくなっています。
優先度フィルター機能を作る
高・中・低で絞り込む
「優先度が高いタスクだけ見たい」というのも、 タスク管理ではよくあるパターンです。
void FilterTasksByPriority()
{
Console.WriteLine("=== 優先度で絞り込み ===");
Console.WriteLine("1: 優先度 高 のタスク");
Console.WriteLine("2: 優先度 中 のタスク");
Console.WriteLine("3: 優先度 低 のタスク");
Console.Write("選択してください: ");
string input = Console.ReadLine();
if (!int.TryParse(input, out int priority))
{
Console.WriteLine("数値で入力してください。");
return;
}
if (priority != 1 && priority != 2 && priority != 3)
{
Console.WriteLine("優先度は 1, 2, 3 のいずれかを選択してください。");
return;
}
var result = repository.FilterByPriority(priority);
string priorityText = priority switch
{
1 => "高",
2 => "中",
3 => "低",
_ => "不明"
};
string header = $"=== 優先度 {priorityText} のタスク一覧 ===";
ShowTaskList(result, header);
}
C#バリデーションの大切さ
int.TryParseで数値チェック- 1/2/3 以外はエラー扱い
こうした「入力チェック」は、 アプリを安全に、そして気持ちよく使ってもらうための大事な要素です。
メニューに Search / Filter 機能を組み込む
コンソールメニューの拡張
Day 73〜76 のメニューに、検索・フィルター機能を追加していきます。
void Run()
{
while (true)
{
Console.WriteLine("=== タスク管理アプリ ===");
Console.WriteLine("1: タスク追加");
Console.WriteLine("2: タスク一覧");
Console.WriteLine("3: タスク編集");
Console.WriteLine("4: 完了状態の変更");
Console.WriteLine("5: タスク削除");
Console.WriteLine("6: キーワード検索");
Console.WriteLine("7: 完了状態で絞り込み");
Console.WriteLine("8: 優先度で絞り込み");
Console.WriteLine("0: 終了");
Console.Write("選択してください: ");
string input = Console.ReadLine();
switch (input)
{
case "1":
CreateTask();
break;
case "2":
ShowTasks();
break;
case "3":
EditTask();
break;
case "4":
ToggleComplete();
break;
case "5":
DeleteTask();
break;
case "6":
SearchTaskByKeyword();
break;
case "7":
FilterTasksByCompletion();
break;
case "8":
FilterTasksByPriority();
break;
case "0":
Console.WriteLine("終了します。");
return;
default:
Console.WriteLine("不正な入力です。");
break;
}
Console.WriteLine();
}
}
C#これで、タスク管理アプリは、
- 追加(Create)
- 一覧(Read)
- 編集・完了変更(Update)
- 削除(Delete)
- 検索・フィルター(Search / Filter)
という、かなり本格的な機能セットを持つようになります。
Day 77 用ミニテンプレート(まとめて貼れるサンプル)
長くなりますが、「Day 73〜76 のコードに検索・フィルターを足した形」のサンプルです。 必要なところだけ切り出して使っていただいても大丈夫です。
using System;
using System.Collections.Generic;
using System.Linq;
// タスクを表すクラス
class TaskItem
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public int Priority { get; set; }
public DateTime? DueDate { get; set; }
public bool IsCompleted { get; set; }
public DateTime CreatedAt { get; set; }
}
// タスク一覧を管理するクラス
class TaskRepository
{
private readonly List<TaskItem> _tasks = new List<TaskItem>();
private int _nextId = 1;
public TaskItem Add(string title, string description, int priority, DateTime? dueDate)
{
var task = new TaskItem
{
Id = _nextId++,
Title = title,
Description = description,
Priority = priority,
DueDate = dueDate,
IsCompleted = false,
CreatedAt = DateTime.Now
};
_tasks.Add(task);
return task;
}
public IEnumerable<TaskItem> GetAll()
{
return _tasks;
}
public TaskItem FindById(int id)
{
return _tasks.FirstOrDefault(t => t.Id == id);
}
public bool Remove(int id)
{
var task = FindById(id);
if (task == null)
{
return false;
}
_tasks.Remove(task);
return true;
}
// キーワード検索
public IEnumerable<TaskItem> SearchByKeyword(string keyword)
{
if (string.IsNullOrWhiteSpace(keyword))
{
return _tasks;
}
keyword = keyword.ToLower();
return _tasks.Where(t =>
(!string.IsNullOrEmpty(t.Title) && t.Title.ToLower().Contains(keyword)) ||
(!string.IsNullOrEmpty(t.Description) && t.Description.ToLower().Contains(keyword))
);
}
// 完了状態フィルター
public IEnumerable<TaskItem> FilterByCompletion(bool isCompleted)
{
return _tasks.Where(t => t.IsCompleted == isCompleted);
}
// 優先度フィルター
public IEnumerable<TaskItem> FilterByPriority(int priority)
{
return _tasks.Where(t => t.Priority == priority);
}
}
class Program
{
static TaskRepository repository = new TaskRepository();
static void Main()
{
Run();
}
static void Run()
{
while (true)
{
Console.WriteLine("=== タスク管理アプリ ===");
Console.WriteLine("1: タスク追加");
Console.WriteLine("2: タスク一覧");
Console.WriteLine("3: タスク編集");
Console.WriteLine("4: 完了状態の変更");
Console.WriteLine("5: タスク削除");
Console.WriteLine("6: キーワード検索");
Console.WriteLine("7: 完了状態で絞り込み");
Console.WriteLine("8: 優先度で絞り込み");
Console.WriteLine("0: 終了");
Console.Write("選択してください: ");
string input = Console.ReadLine();
switch (input)
{
case "1":
CreateTask();
break;
case "2":
ShowTasks();
break;
case "3":
EditTask();
break;
case "4":
ToggleComplete();
break;
case "5":
DeleteTask();
break;
case "6":
SearchTaskByKeyword();
break;
case "7":
FilterTasksByCompletion();
break;
case "8":
FilterTasksByPriority();
break;
case "0":
Console.WriteLine("終了します。");
return;
default:
Console.WriteLine("不正な入力です。");
break;
}
Console.WriteLine();
}
}
// 共通表示メソッド
static void ShowTaskList(IEnumerable<TaskItem> tasks, string header)
{
Console.WriteLine(header);
var list = tasks.ToList();
if (!list.Any())
{
Console.WriteLine("条件に合うタスクがありませんでした。");
return;
}
foreach (var t in list)
{
string status = t.IsCompleted ? "[完了]" : "[未完了]";
string priorityText = t.Priority switch
{
1 => "高",
2 => "中",
3 => "低",
_ => "不明"
};
string dueText = t.DueDate.HasValue
? t.DueDate.Value.ToString("yyyy-MM-dd")
: "期限なし";
Console.WriteLine($"ID: {t.Id}");
Console.WriteLine($" 状態 : {status}");
Console.WriteLine($" 優先度 : {priorityText}");
Console.WriteLine($" 期限 : {dueText}");
Console.WriteLine($" タイトル: {t.Title}");
Console.WriteLine($" 詳細 : {t.Description}");
Console.WriteLine();
}
}
static void CreateTask()
{
Console.WriteLine("=== タスク追加 ===");
string title = ReadTitle();
string description = ReadDescription();
int priority = ReadPriority();
DateTime? dueDate = ReadDueDate();
TaskItem task = repository.Add(title, description, priority, dueDate);
Console.WriteLine($"タスクを追加しました。ID: {task.Id}");
}
static string ReadTitle()
{
while (true)
{
Console.Write("タスク名を入力してください: ");
string title = Console.ReadLine();
if (!string.IsNullOrWhiteSpace(title))
{
return title;
}
Console.WriteLine("タスク名は必須です。空では登録できません。");
}
}
static string ReadDescription()
{
Console.Write("詳細(メモ)を入力してください(空でも構いません): ");
string description = Console.ReadLine();
return description ?? string.Empty;
}
static int ReadPriority()
{
while (true)
{
Console.Write("優先度を入力してください (1: 高, 2: 中, 3: 低): ");
string text = Console.ReadLine();
if (int.TryParse(text, out int value))
{
if (value == 1 || value == 2 || value == 3)
{
return value;
}
}
Console.WriteLine("優先度は 1, 2, 3 のいずれかで入力してください。");
}
}
static DateTime? ReadDueDate()
{
while (true)
{
Console.Write("期限を入力してください (yyyy-MM-dd、未入力なら期限なし): ");
string text = Console.ReadLine();
if (string.IsNullOrWhiteSpace(text))
{
return null;
}
if (DateTime.TryParse(text, out DateTime date))
{
return date;
}
Console.WriteLine("日付の形式が正しくありません。例: 2026-09-10 のように入力してください。");
}
}
static void ShowTasks()
{
var tasks = repository.GetAll();
ShowTaskList(tasks, "=== タスク一覧 ===");
}
static void EditTask()
{
Console.WriteLine("=== タスク編集 ===");
Console.Write("編集したいタスクのIDを入力してください: ");
string idText = Console.ReadLine();
if (!int.TryParse(idText, out int id))
{
Console.WriteLine("IDは数値で入力してください。");
return;
}
TaskItem task = repository.FindById(id);
if (task == null)
{
Console.WriteLine("指定されたIDのタスクが見つかりませんでした。");
return;
}
Console.WriteLine($"現在のタイトル: {task.Title}");
Console.Write("新しいタイトル(変更しない場合は空のまま): ");
string newTitle = Console.ReadLine();
if (!string.IsNullOrWhiteSpace(newTitle))
{
task.Title = newTitle;
}
Console.WriteLine($"現在の詳細: {task.Description}");
Console.Write("新しい詳細(変更しない場合は空のまま): ");
string newDescription = Console.ReadLine();
if (!string.IsNullOrWhiteSpace(newDescription))
{
task.Description = newDescription;
}
Console.WriteLine($"現在の優先度: {task.Priority} (1: 高, 2: 中, 3: 低)");
Console.Write("新しい優先度 (1/2/3、変更しない場合は空のまま): ");
string newPriorityText = Console.ReadLine();
if (!string.IsNullOrWhiteSpace(newPriorityText))
{
if (int.TryParse(newPriorityText, out int newPriority))
{
if (newPriority == 1 || newPriority == 2 || newPriority == 3)
{
task.Priority = newPriority;
}
else
{
Console.WriteLine("優先度は 1, 2, 3 のいずれかである必要があります。変更は反映されませんでした。");
}
}
else
{
Console.WriteLine("優先度は数値で入力してください。変更は反映されませんでした。");
}
}
Console.WriteLine($"現在の期限: {(task.DueDate.HasValue ? task.DueDate.Value.ToString("yyyy-MM-dd") : "期限なし")}");
Console.Write("新しい期限 (yyyy-MM-dd、空なら期限なしに変更): ");
string newDueText = Console.ReadLine();
if (string.IsNullOrWhiteSpace(newDueText))
{
task.DueDate = null;
}
else
{
if (DateTime.TryParse(newDueText, out DateTime newDue))
{
task.DueDate = newDue;
}
else
{
Console.WriteLine("日付の形式が正しくありません。期限は変更されませんでした。");
}
}
Console.WriteLine("タスクを更新しました。");
}
static void ToggleComplete()
{
Console.WriteLine("=== 完了状態の変更 ===");
Console.Write("完了状態を切り替えたいタスクのIDを入力してください: ");
string idText = Console.ReadLine();
if (!int.TryParse(idText, out int id))
{
Console.WriteLine("IDは数値で入力してください。");
return;
}
TaskItem task = repository.FindById(id);
if (task == null)
{
Console.WriteLine("指定されたIDのタスクが見つかりませんでした。");
return;
}
Console.WriteLine($"現在の状態: {(task.IsCompleted ? "完了" : "未完了")}");
Console.Write("状態を切り替えますか? (y/n): ");
string answer = Console.ReadLine();
if (answer?.ToLower() != "y")
{
Console.WriteLine("完了状態の変更をキャンセルしました。");
return;
}
task.IsCompleted = !task.IsCompleted;
Console.WriteLine($"完了状態を変更しました。現在の状態: {(task.IsCompleted ? "完了" : "未完了")}");
}
static void DeleteTask()
{
Console.WriteLine("=== タスク削除 ===");
Console.Write("削除したいタスクのIDを入力してください: ");
string idText = Console.ReadLine();
if (!int.TryParse(idText, out int id))
{
Console.WriteLine("IDは数値で入力してください。");
return;
}
TaskItem task = repository.FindById(id);
if (task == null)
{
Console.WriteLine("指定されたIDのタスクが見つかりませんでした。");
return;
}
Console.WriteLine("削除対象のタスク:");
string status = task.IsCompleted ? "[完了]" : "[未完了]";
string priorityText = task.Priority switch
{
1 => "高",
2 => "中",
3 => "低",
_ => "不明"
};
string dueText = task.DueDate.HasValue
? task.DueDate.Value.ToString("yyyy-MM-dd")
: "期限なし";
Console.WriteLine($"ID: {task.Id}");
Console.WriteLine($" 状態 : {status}");
Console.WriteLine($" 優先度 : {priorityText}");
Console.WriteLine($" 期限 : {dueText}");
Console.WriteLine($" タイトル: {task.Title}");
Console.WriteLine($" 詳細 : {task.Description}");
Console.WriteLine();
Console.Write("本当に削除しますか? (y/n): ");
string answer = Console.ReadLine();
if (answer?.ToLower() != "y")
{
Console.WriteLine("削除をキャンセルしました。");
return;
}
bool removed = repository.Remove(id);
if (removed)
{
Console.WriteLine("タスクを削除しました。");
}
else
{
Console.WriteLine("削除に失敗しました。");
}
}
static void SearchTaskByKeyword()
{
Console.WriteLine("=== キーワード検索 ===");
Console.Write("検索したいキーワードを入力してください: ");
string keyword = Console.ReadLine();
var result = repository.SearchByKeyword(keyword);
ShowTaskList(result, "=== 検索結果 ===");
}
static void FilterTasksByCompletion()
{
Console.WriteLine("=== 完了状態で絞り込み ===");
Console.WriteLine("1: 未完了のタスクのみ表示");
Console.WriteLine("2: 完了したタスクのみ表示");
Console.Write("選択してください: ");
string input = Console.ReadLine();
bool isCompleted;
if (input == "1")
{
isCompleted = false;
}
else if (input == "2")
{
isCompleted = true;
}
else
{
Console.WriteLine("不正な入力です。");
return;
}
var result = repository.FilterByCompletion(isCompleted);
string header = isCompleted
? "=== 完了したタスク一覧 ==="
: "=== 未完了のタスク一覧 ===";
ShowTaskList(result, header);
}
static void FilterTasksByPriority()
{
Console.WriteLine("=== 優先度で絞り込み ===");
Console.WriteLine("1: 優先度 高 のタスク");
Console.WriteLine("2: 優先度 中 のタスク");
Console.WriteLine("3: 優先度 低 のタスク");
Console.Write("選択してください: ");
string input = Console.ReadLine();
if (!int.TryParse(input, out int priority))
{
Console.WriteLine("数値で入力してください。");
return;
}
if (priority != 1 && priority != 2 && priority != 3)
{
Console.WriteLine("優先度は 1, 2, 3 のいずれかを選択してください。");
return;
}
var result = repository.FilterByPriority(priority);
string priorityText = priority switch
{
1 => "高",
2 => "中",
3 => "低",
_ => "不明"
};
string header = $"=== 優先度 {priorityText} のタスク一覧 ===";
ShowTaskList(result, header);
}
}
C#Day 77 のまとめ
Day 77 では、タスク管理アプリに 検索・フィルター機能を追加しました。
- タイトル・詳細にキーワードを含むタスクを探す
SearchByKeyword - 完了状態で絞り込む
FilterByCompletion - 優先度で絞り込む
FilterByPriority - 結果表示を共通化する
ShowTaskList - メニューから「検索」「絞り込み」を選べるように拡張
これで、タスクが増えてきても、 「今見たいタスクだけ」に視点を絞ることができるようになります。
検索・フィルターは、 ただの“おまけ機能”ではなく、 「情報の海から、自分にとって大事なものだけをすくい上げるための道具」です。
ぜひ、いくつかタスクを登録してから、 キーワード検索や状態フィルターを試してみてください。 「あ、ちゃんと探せるって気持ちいいな」と感じてもらえたら、 それはもう立派な“自分の手で作ったアプリ”です。
