Swiftでコーディングテスト対策 - 応用編 -

## はじめに
初めまして、26卒の就活生です。
今回は、前回のアルゴリズム問題と同じ形式で、Swiftにおけるコーディングテスト対策を残します。

## 準備
応用問題は、落ち着いて綺麗なコードを書けば後々自分が楽になると思います。
手順に沿って準備していきましょう。

### ディレクトリ構成
応用問題は、一つのファイルで作成するのでなく、複数のフォルダを作成して解くのがおすすめです。
ディレクトリ構成は、完全に個人の好みではありますが、自分なら下記のようなディレクトリ構成をします。

```
.
└── App/
    ├── main.swift  //メイン関数
    ├── Extensions/
    │   └── //「型名+Extensions.swift」
    ├── Models/
    │   └── //「モデル名.swift」
    └── Managers/
        └── //「モデル名+Manager.swift」
```
- Extensions
    - 主に型変換の処理をここに書きます
- Models
    - ユーザーやチケット情報など、情報をひとまとめにするモデルをここに書きます
- Managers
    - モデルの追加、取得、更新、削除処理（CRUD）をここに書きます


### 文字列の受け取り
それではモデルを構築していくために、標準入力をみて問題に適した型で受け取っていきましょう。
例として、ユーザー情報をモデルに当てはめていきます。

```input.txt
register user1 taro 2024/12/13-16:13 21
```
という入力を、Userモデルを作成して当てはめていきましょう。
ちなみに、標準入力の内容は、左から、
- クエリー内容(register)
- ユーザーID(user1)
- ユーザー名(taro)
- 登録日時(2024/12/13-16:13)
- 年齢(21)

### モデルの作成
それでは、Userという名前でモデルを作成していきましょう。

```User.swift
import Foundation

struct User {
    let id: String
    let name: String
    let registrationDate: Date
    let age: Int
}
```
ここで、```import Foudation```についてですが、Date型を扱うために必要です。日時が入力で与えられた場合、Intに変換して秒単位で管理する方法もありますが、一年前とか簡単に取得できるので、Dateで受け取ることをお勧めします。

### 型変換処理を宣言
モデルの代入に早くいきたいところですが、今回のデータには、Date形式の入力が含まれていますので、入力をDate型に変換しなければなりません。

String型からDate型への変換処理を、```Extensions```フォルダ内に作成します。フォルダ名は、Stringに対して拡張処理を行いたいので、```String+Extensions.swift```としました。

```String+Extensions.swift
import Foundation

extension String {
    func toDate(format: String = "yyyy/MM/dd-HH:mm") -> Date? {
        let formatter = DateFormatter()
        formatter.dateFormat = format
        formatter.timeZone = .current
        return formatter.date(from: self)
    }
}
```

この時、formatは標準入力に従います。今回の日時の標準入力が、
```2024/12/13-16:13```なので、上記のように設定しています。
秒まである場合は、```ss```を追加宣言してあげてください。

### モデルに代入
やっとです、入力をモデルに入れていきましょう。
入力は下記を想定します。
```input.txt
register user1 taro 2024/12/13-16:13 21
```
これを先ほど作ったUserモデルに入れていきます。

```main.swift
let line = readLine()!.split(separator: " ").map { String($0) }
let id = line[1]
let name = line[2]
let registrationDate = line[3].toDate()!
let age = Int(line[4])!

let user: User = User(
    id: id, 
    name: name, 
    registrationDate: registrationDate, 
    age: age
)
```
最初の行で、空白区切りで配列として受け取ります。splitをすると```Substring型```に変わるので、```map```でString型に変換して扱いやすくしています。
この時、Int変換やDate変換はnilを許容するoptionalを返すので、!をつけてoptionalを消し去ります。本来のSwift開発ではお勧めしませんが、コーディングテストならいいでしょう。

### Managerでユーザー情報を収める
ユーザー情報を入れることができたら、ユーザー一覧を管理するユーザーマネージャーを作成します。
主に必要になってくる処理は、追加、取得、更新、削除処理（CRUD）と検索処理です。書いていきましょう。

```UserManager.swift
final class UserManager {
    private var userList = [String : User]()

    //ユーザー追加処理
    func addUser(user: User) {
        userList[user.id] = user
    }

    //ユーザー取得処理
    func getUser(id: String) -> User? {
        return userList[id]
    }

    // ユーザー更新処理
    func updateUser(newUser: User) {
        if let _ = userList[newUser.id] {
            userList[newUser.id] = newUser  // 正しいキーで更新
        } else {
            print("ユーザーが見つかりません")
        }
    }

    //ユーザー削除処理
    func deleteUser(id: String) {
        if userList.removeValue(forKey: id) == nil {
            print("ユーザーが見つかりません")
        }
    }

    //呼び出し側で条件を指定する
    func search(filter: (User) -> Bool) -> [User] {
        return userList.values.filter(filter)
    }
}
```
工夫したポイントは、ユーザーリストをidとuserの辞書型にしたことです。こうすることで、ユーザーの取得処理、更新処理において非常に高速に処理を行うことができます。もし、userListだけだったら、ユーザーのidを取得して、一致しているidを探して...　という風に、取得や削除に時間がかかってしまうことがわかります。
(final classは、継承する予定がないので一応つけてます。)

#### Managerの使い方
自分のmain関数の最初に宣言しています。
```main.swift
let userManager = UserManager()
```
では、先ほど作成したユーザー情報をマネージャーに追加してみましょう！
```main.swift
let userManager = UserManager()

let user: User = User(
    id: id, 
    name: name, 
    registrationDate: registrationDate, 
    age: age
)

userManager.addUser(user: user)
```
お疲れ様でした。これで準備は完了です

### クエリごとに処理を分ける
コーディングテストで与えられる問題には、おそらくこの処理をして、あの処理をしてって優しく指定してくれますので、そのクエリ毎に処理を分ける関数を書いていきます。
クエリの種類として、
- register
- delete
- update
- search

の4種類にしていきます。
```main.swift
//クエリ毎処理を分ける
func processLine(_ line: String) {
  let items = line.split(separator: " ").map { String($0) }
  guard let query = items.first else { return }

  switch query {
    case "register":
        register(items)
    case "delete":
        delete(items)
    case "update":
        update(items)
    case "search":
        search(items)
    default:
        print("クエリーが見つかりません")
  }
}
```
こちらの関数は、与えられた標準入力を1行ずつforさせるときに毎回呼びます。

### 解いてみる
それでは、本番の問題みたいに出題しながら、解いていきましょう。
まず、今回与える標準入力として、ユーザーデータの操作をしていきます。
具体的には、クエリーとして、
- register: ユーザーの登録
- update: ユーザーデータの更新(ユーザー名の更新)
- delete: ユーザーの削除
- search: ユーザーの検索(年齢による検索)

以上の4つのクエリーを入力として与えられる形にします。

コーディングテストほど詳しくは書かないですが、今回想定する標準入力のフォーマットは以下の通りです。
```input.txt
register <id> <name> <registrationDate> <age>
delete <id>
update <id> <new_name>
search <age>
```
そして、それぞれの処理のアウトプットは以下のようにします。
```output.txt
register id: <id>, age: <age>, <name>さんが追加されました
delete <id>を削除しました
update <id>情報を更新しました
search <age>歳
- id: <id>, age: <age>, <name>さん
- id: <id>, age: <age>, <name>さん...(複数)
```
#### 与えられる標準入力
```input.txt
20
register user1 taro 2024/12/13-16:13 21
register user2 hanako 2023/11/27-08:45 30
register user3 ichiro 2020/05/10-10:00 21
update user1 taroYamada
register user4 yuki 2022/06/15-14:30 25
delete user2
search 30
update user3 otani
register user5 kenji 2023/07/20-18:20 30
delete user4
search 21
register user6 miki 2024/01/05-09:00 21
update user5 kenjiTanaka
register user7 jun 2023/12/01-18:00 25
delete user1
search 25
register user8 akira 2023/03/10-09:45 30
update user8 age 31
delete user5
delete user3
```

#### 想定される出力
```output.txt
register id: user1, age: 21, taroさんが追加されました
register id: user2, age: 30, hanakoさんが追加されました
register id: user3, age: 21, ichiroさんが追加されました
update user1情報を更新しました
register id: user4, age: 25, yukiさんが追加されました
delete user2を削除しました
search 30歳
update user3情報を更新しました
register id: user5, age: 30, kenjiさんが追加されました
delete user4を削除しました
search 21歳
- id: user1, age: 21, taroYamadaさん
- id: user3, age: 21, otaniさん
register id: user6, age: 21, mikiさんが追加されました
update user5情報を更新しました
register id: user7, age: 25, junさんが追加されました
delete user1を削除しました
search 25歳
- id: user7, age: 25, junさん
register id: user8, age: 30, akiraさんが追加されました
update user8情報を更新しました
delete user5を削除しました
delete user3を削除しました
```

大変優柔不断で申し訳ございませんが、今回の想定だとnameの更新が必要になるので、
```var name```とする必要があります🙇

それでは、実装したmain関数です。
```main.swift
let userManager = UserManager()

let queryList = getQueryList()

for query in queryList {
  processLine(query)
}

//標準入力を取得
func getQueryList() -> [String]{
  let m = Int(readLine()!)!
  var result: [String] = []
  for _ in 0..<m {
    let line = readLine()!
    result.append(line)
  }
  return result
}

//クエリー毎処理を分ける
func processLine(_ line: String) {
  let items = line.split(separator: " ").map { String($0) }
  guard let query = items.first else { return }

  switch query {
    case "register":
        register(items)
    case "delete":
        delete(items)
    case "update":
        update(items)
    case "search":
        search(items)
    default:
        print("クエリーが見つかりません")
  }
}

func register(_ items: [String]) {
    let id = items[1]
    let name = items[2]
    let registrationDate = items[3].toDate()!
    let age = Int(items[4])!
    let user: User = User(
        id: id, 
        name: name, 
        registrationDate: registrationDate, 
        age: age
    )
    userManager.addUser(user: user)
    print("\(items[0]) id: \(id), age: \(age), \(name)さんが追加されました")
}

func delete(_ items: [String]) {
    let id = items[1]
    userManager.deleteUser(id: id)
    print("\(items[0]) \(id)を削除しました")
}

func update(_ items: [String]) {
    let id = items[1]
    let newName = items[2]
    guard var user = userManager.getUser(id: id) else {
        print("ユーザーが見つかりません")
        return
    }
    user.name = newName
    userManager.updateUser(newUser: user)
    print("\(items[0]) \(id)情報を更新しました")
}

func search(_ items: [String]) {
    let requestAge = Int(items[1])!
    let filteredUserList = userManager.search { user in
        user.age == requestAge
    }
    print("\(items[0]) \(requestAge)歳")
    for user in filteredUserList {
        print("- id: \(user.id), age: \(user.age), \(user.name)さん")
    }
}

```

## さいごに
Dateの期限をつける処理まではできなかった反省