pythonでCLI形式の簡単なTODOアプリを作成してみた

## はじめに
pythonの基本的な文法は学習したが、実際にどういう場面で使うのかイメージしずらかったので簡単なTODOアプリを作成してみました。
いきなりWEBアプリを作成しようとすると、python以外のフロントの書き方とかで躓きそうだったので、まずは簡単にCLI形式で作成していきます。

## TODOアプリの機能について
今回作成するTODOアプリの機能としては以下です。
 - 追加
 - 表示
 - 削除
 - 更新
 - 保存

めちゃくちゃシンプルです。もはやデフォルトのメモ帳使った方がいいレベルなのですが、今回はpythonの基本的な文法をどのような場面で使うのか実践したかったのでよしとします。

## 動かしてみる
まずは完成形でどのような挙動をするのか動かしてみます。

```
#ファイルの実行
python3 main.py
```

```
#実行したいコマンドを入力する
#addでタスクの追加を行う
Enter command 'add' or 'delete' or 'show' or 'update' 'exit' to quit:add
Enter task name: qiitaで記事を書く
Enter due date (YYYY-MM-DD): 2024-10-14
Enter command 'add' or 'delete' or 'show' or 'update' 'exit' to quit: #`exit`を入力するまで繰り返し
```

追加したタスクを確認してみます。
```
Enter command 'add' or 'delete' or 'show' or 'update' 'exit' to quit: show
ID:1 | Task:qiitaで記事を書く | Due:2024-10-14 | Status:False 
```
IDが振られていて、先ほど入力したタスクが表示されました。
Statusも振られていて、updateで変更できます。内容を変更するにはIDを指定します。

```
Enter command 'add' or 'delete' or 'show' or 'update' 'exit' to quit: update
Enter task ID to update: 1
Enter new task name (current: qiitaで記事を書く): qiitaで記事を 2本書く    
Enter new due date (current: 2024-10-14): 2024-10-14
Is the task completed? (yes/no, current: no): yes
Task 1 updated.
Enter command 'add' or 'delete' or 'show' or 'update' 'exit' to quit: show
ID:1 | Task:qiitaで記事2本書く | Due:2024-10-14 | Status:True 
```

 タスクのStatusを`Is the task completed? (yes/no, current: no): yes`でTrueに変わります。

 jsonには以下のような形で保存されます。
```
{
    "1": {
        "name": "qiita\u3067\u8a18\u4e8b2\u672c\u66f8\u304f",
        "due_date": "2024-10-14",
        "completed": true
    }
}
```
## コードの全体
'add'
'show'
'delete'
'update'
のいずれかを入力してタスクを操作し、結果をjsonファイルに反映させます。

```
import json
import os

# JSONファイル名
TODO_FILE = 'todo_list.json'

# タスクデータのロード
def load_tasks():
    if os.path.exists(TODO_FILE):
        with open(TODO_FILE, 'r') as file:
            return json.load(file)
    return {}

# タスクデータの保存
def save_tasks(tasks):
    with open(TODO_FILE, 'w') as file:
        json.dump(tasks, file, indent=4)

# タスクを追加
def add_task(tasks):
    # ユーザーからタスク名と期限日の入力を受け取る
    task_id = str(len(tasks) + 1)
    task_name = input("Enter task name: ")
    due_date = input("Enter due date (YYYY-MM-DD): ")
    
    # 入力のバリデーション（タスク名と期限日が空でないか確認）
    if not task_name or not due_date:
        print("Task name and due date cannot be empty.")
        return
    
    # タスクを辞書形式で追加
    tasks[task_id] = {
        'name': task_name,
        'due_date': due_date,
        'completed': False
    }
    print(f"Task '{task_name}' added.")
    
    # タスクをJSONファイルに保存
    save_tasks(tasks)
    
#タスクを表示
def show_task(tasks):
    if not tasks:
        print("No tasks.")
        return
    for task_id, task_info in tasks.items():
        print(f"ID:{task_id} | Task:{task_info['name']} | Due:{task_info['due_date']} | Status:{task_info['completed']} ")

#タスクを削除
def delete_task(tasks):
    task_id = input("Enter task ID to delete: ")
    if task_id in tasks:
        del tasks[task_id]
        print(f"Task {task_id} deleted.")
        save_tasks(tasks)
    else:
        print(f"Task {task_id} not found.")
    
#タスクを更新
def update_task(tasks):
    task_id = input("Enter task ID to update: ")
    if task_id in tasks:
        task_name = input(f"Enter new task name (current: {tasks[task_id]['name']}): ")
        due_date = input(f"Enter new due date (current: {tasks[task_id]['due_date']}): ")
        completed = input(f"Is the task completed? (yes/no, current: {'yes' if tasks[task_id]['completed'] else 'no'}): ")

        if task_name:
            tasks[task_id]['name'] = task_name
        if due_date:
            tasks[task_id]['due_date'] = due_date
        if completed.lower() == 'yes':
            tasks[task_id]['completed'] = True
        elif completed.lower() == 'no':
            tasks[task_id]['completed'] = False
        
        print(f"Task {task_id} updated.")
        save_tasks(tasks)
    else:
        print(f"Task {task_id} not found.")

# メイン関数
def main():
    tasks = load_tasks()  # タスクのロード
    
    while True:
        command = input("Enter command 'add' or 'delete' or 'show' or 'update' 'exit' to quit: ").lower()
        
        if command == 'add':
            add_task(tasks)
        elif command == 'show':
            show_task(tasks)
        elif command == 'delete':
            delete_task(tasks)
        elif command == 'update':
            update_task(tasks)
        elif command == 'exit':
            break
        else:
            print("Invalid command.")

if __name__ == "__main__":
    main()

```
関数毎に各機能を定義していて、ユーザーからのコマンド入力で関数を呼び出しています。
以下で各機能について解説していきます

## メイン部分の関数

```
# メイン関数
def main():
    tasks = load_tasks()  # タスクのロード
    
    while True:
        command = input("Enter command 'add' or 'delete' or 'show' or 'update' 'exit' to quit: ").lower()
        
        if command == 'add':
            add_task(tasks)
        elif command == 'show':
            show_task(tasks)
        elif command == 'delete':
            delete_task(tasks)
        elif command == 'update':
            update_task(tasks)
        elif command == 'exit':
            break
        else:
            print("Invalid command.")

if __name__ == "__main__":
    main()
```
この部分で、これから解説する関数毎に定義した機能を呼び出します。
ファイルを実行すると、入力に応じた関数が呼びだされ、タスクの追加や削除がされます。
また関数部分以外だと、`json`を扱えるようにする`json`と、ディレクトリ内のファイルを読み込むのに必要な`os`をimportしています。

## タスクの読み込み

```
# タスクデータのロード
def load_tasks():
    if os.path.exists(TODO_FILE):
        with open(TODO_FILE, 'r') as file:
            return json.load(file)
    return {}
```
メイン関数の以下部分で最初に読み込まれます。
```
def main():
    tasks = load_tasks()  # タスクのロード
```
細かく解説します。
```
if os.path.exists(TODO_FILE):
```
ファイル実行時に、jsonファイルが存在するか確認し、
```
        with open(TODO_FILE, 'r') as file:
            return json.load(file)
```
ファイルが存在すればファイルを開き、ファイルの内容を辞書として返してくれます。
```
    return {}
```
存在しない場合は、空の辞書を返します。
そして読み込んだデータに対して、addやdelete等でタスクの操作をしていきます。

## タスクデータの保存
```
def save_tasks(tasks):
    with open(TODO_FILE, 'w') as file:
        json.dump(tasks, file, indent=4
```
関数に渡される`tasks`には、add,delete,updateで操作した結果の辞書が渡されます。
```
    with open(TODO_FILE, 'w') as file:
```
書き込み権限でjsonファイルを読み込み、
```
json.dump(tasks, file, indent=4
```
`tasks`(操作結果の辞書)を`file`(TODO_FILE = 'todo_list.json)に`indent=4`（インデントを４つつけて）上書きします。

この関数は、結果を保存するためにタスクを操作する関数の中で必ず呼び出します。

## タスクの追加

```
# タスクを追加
def add_task(tasks):
    # ユーザーからタスク名と期限日の入力を受け取る
    task_id = str(len(tasks) + 1)
    task_name = input("Enter task name: ")
    due_date = input("Enter due date (YYYY-MM-DD): ")
    
    # 入力のバリデーション（タスク名と期限日が空でないか確認）
    if not task_name or not due_date:
        print("Task name and due date cannot be empty.")
        return
    
    # タスクを辞書形式で追加
    tasks[task_id] = {
        'name': task_name,
        'due_date': due_date,
        'completed': False
    }
    print(f"Task '{task_name}' added.")
    
    # タスクをJSONファイルに保存
    save_tasks(tasks)

```

以下の部分で、ユーーからの入力をinputで受け取り、変数に代入します。
task_idは、読み込んだ辞書の長さに+1した値を入れていきます。
```
    # ユーザーからタスク名と期限日の入力を受け取る
    task_id = str(len(tasks) + 1)
    task_name = input("Enter task name: ")
    due_date = input("Enter due date (YYYY-MM-DD): ")
```
以下の部分で、入力内容の確認を行います。

```
    if not task_name or not due_date:
        print("Task name and due date cannot be empty.")
        return
```
入力内容が正しければ、以下の部分で辞書に追加して、jsonファイルに保存されます。
```
    # タスクを辞書形式で追加
    tasks[task_id] = {
        'name': task_name,
        'due_date': due_date,
        'completed': False
    }
    print(f"Task '{task_name}' added.")
    
    # タスクをJSONファイルに保存
    save_tasks(tasks)
```

## タスクの削除

```
def delete_task(tasks):
    task_id = input("Enter task ID to delete: ")
    if task_id in tasks:
        del tasks[task_id]
        print(f"Task {task_id} deleted.")
        save_tasks(tasks)
    else:
        print(f"Task {task_id} not found.")
```

以下の部分で、削除したいタスクIDの入力をします。
```
    task_id = input("Enter task ID to delete: ")
```

以下の部分で、入力したIDを持つタスクが存在するか確認します。
```
    if task_id in tasks:
```
IDが存在する場合は以下が実行され、tasks（jsonファイル内のデータを辞書に変換したもの）内の指定したidのタスクを削除します。
```
        del tasks[task_id]
        print(f"Task {task_id} deleted.")
        save_tasks(tasks)
```

## タスクの更新

削除の時と同じで、まずはinputでIDを入力します。
そして受け取ったIDを持つタスクの内容を再度inputで入力してもらい、上書きします。
```
def update_task(tasks):
    task_id = input("Enter task ID to update: ")
    if task_id in tasks:
        task_name = input(f"Enter new task name (current: {tasks[task_id]['name']}): ")
        due_date = input(f"Enter new due date (current: {tasks[task_id]['due_date']}): ")
        completed = input(f"Is the task completed? (yes/no, current: {'yes' if tasks[task_id]['completed'] else 'no'}): ")

        if task_name:
            tasks[task_id]['name'] = task_name
        if due_date:
            tasks[task_id]['due_date'] = due_date
        if completed.lower() == 'yes':
            tasks[task_id]['completed'] = True
        elif completed.lower() == 'no':
            tasks[task_id]['completed'] = False
        
        print(f"Task {task_id} updated.")
        save_tasks(tasks)
    else:
        print(f"Task {task_id} not found.")
```

## タスクの表示

シンプルにタスクの一覧を表示します。
検索機能はないので、全て表示します。
```
def show_task(tasks):
    if not tasks:
        print("No tasks.")
        return
    for task_id, task_info in tasks.items():
        print(f"ID:{task_id} | Task:{task_info['name']} | Due:{task_info['due_date']} | Status:{task_info['completed']} ")
```

## 終わりに
成果物に対して文章量が多くなってしまいました。
最近ではChat-GPT等でこのくらいのものなら数行のプロンプトですぐに答えを出してくれます。
しかし、本当にAIが出してくれたコードは正しいのか判断するためにも、コードの各行で何が行われているのか理解することが重要で、
例えば数百行の、ぱっと見では何をしているのか理解できないコードだったとしても、一行ずつ紐解いて理解することが重要だと感じました。

今回はCLIで簡易なTODOアプリを作成しましたが、実際にこのTODOアプリを実際に使ってみてこんな機能が欲しいなとか、ここは使いずらいから変えた方がいいなとか色々改修を加えていきたいと思います。

読んでくれた誰かの参考になれば幸いです。