Swift4で言語処理100本ノック 2015 第3章を途中まで

# はじめに
前回[Swift4で言語処理100本ノック 2015 第1章,第2章](https://qiita.com/tikidunpon/items/4fcefec2d1171e08d188#swift4%E3%81%AE%E6%96%87%E5%AD%97%E5%88%97%E3%82%84%E3%82%B3%E3%83%AC%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E6%89%B1%E3%81%84%E3%81%AB%E6%85%A3%E3%82%8C%E3%81%9F)を書いて大分日が経ってしまいました。
第3章は途中までですが[^1]、折角なので得たことをまとめておこうと思います。
今回もテストコードを書きながら問題20〜24までを進めました、リポジトリは[こちら](https://github.com/tikidunpon/nlp100)です。

# 得たこと、良かったことなど  

## JSONのデコードにCodableを利用  
Swift4から新しく追加されたエンコード/デコード用のプロトコルCodableを試しました。
以下はコードの一部ですが、Codableにより簡単にJSONが扱えることを実感しました。  
参考: [Codableについて色々まとめた[Swift4]
](https://qiita.com/_ha1f/items/bf1aad5ea3e927f59f9d)

```swift
// 利用したい型にCodableを採用する  
struct WikiItem: Codable {
    let title: String
    let text: String
}
```

```swift
/// 入力されたjsonからタイトルがイギリスのものを探し先頭の1件を返す
static func ukWikiItem(input: String) -> WikiItem {
    let lines = input.components(separatedBy: CharacterSet.newlines).filter{ !$0.isEmpty }
    let decoder = JSONDecoder()
    let ukWiki = lines.map{
        // 簡単なエンコード/デコードなら1行で出来る
        return try! decoder.decode(WikiItem.self, from: $0.data(using: .utf8)!)
        }.filter{ $0.title == "イギリス" }
        .first!
    return ukWiki
}
```

## 複数行リテラルの活用
参考: [Multiline String Literals](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/StringsAndCharacters.html#//apple_ref/doc/uid/TP40014097-CH7-ID286)

正規表現をベタ書きする時に分かりやすいよう、複数行リテラルで改行してみました。
この例ではそこまで見やすくないですが、SQLなど状況によって活用できると思います。

```swift
let categoryNameMatches = linesHasCategory.map { (line) -> String in
            let regex =
            """
            \\[Category:\
            (\
            .+[^\\]]\
            )\
            \\]
            """
            return line.matches(regex: regex)[1]
        }
``` 

## コメント内にコードサンプルを埋め込む

分かりづらい関数のコメント内にサンプルコードの利用例を追加しました。
説明の間に1行間を空け、スペース5個でインデントしたコメントは、

```swift
/// 入力された正規表現文字列にマッチした結果を配列で返す
///
///     //（例）下記のように正規表現を引数に渡して利用します。
///     let matches = "Swift Moji 9876".matches(regex: "^(.+)\\s(\\d{4})")
///
///     // 戻り値の先頭はマッチ結果の完全なキャプチャを返します。
///     matches.first // "Swift Moji 9876"
///
///     // 個別のキャプチャされたグループは[1]以降に格納されます。
///     matches[1] // "Swift Moji"
///     matches.last // "2017"
///
/// - Parameter regex: 正規表現文字列
/// - Returns: マッチ結果

```  

このようにコード部分が整形されて表示されるようになります。 

<img width="473" alt="スクリーンショット 2017-11-23 17.04.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/9628/44e12b74-7d37-03c8-4561-1a04d8beaddd.png">


# 答え合わせ  
答え合わせにはPythonで書かれている、[素人の言語処理100本ノック:まとめ 第3章: 正規表現](https://qiita.com/segavvy/items/fb50ba8097d59475f760#%E7%AC%AC3%E7%AB%A0-%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE)を参考にしました。
正規表現しっかり頑張ってて偉いなと思いました。

# 余談（第1章の副産物）  
第1章で学んだNグラムを使って[2つ文字列の類似度を出すコード](https://github.com/tikidunpon/SwiftMoji/blob/master/SwiftMoji/Classes/SwiftMoji.swift#L274)を書きました。    
こんな感じに使います。類似度が最も低い時0.0、もっとも高い時1.0を返します。  

```swift
func testSimilarity() {
    XCTAssertTrue(String.similarity("", "I") == 0.0)
    XCTAssertTrue(String.similarity("I", "I") == 1.0)
    XCTAssertTrue(String.similarity("I", "Y") == 0.0)
    XCTAssertTrue(String.similarity("I", "IY") == 0.0)
    XCTAssertTrue(String.similarity("I see", "I see") == 1.0)
    XCTAssertTrue(String.similarity("I see", "I sea") >= 0.6)
    XCTAssertTrue(String.similarity("I see", "IC") <= 0.5)
}
``` 

こちらの[文字列の類似度を計算するgem](https://qiita.com/giiko_/items/9e61f39b903c6d492296)にあるようにレーベンシュタイン距離を使うよりはパフォーマンスが出るとのことです。

# コード全文  

```swift

//
//  Chapter3.swift
//  LanguageProcessing100knock2015
//
//

import Foundation

struct WikiItem: Codable {
    let title: String
    let text: String
}

struct Section {
    let name: String
    let level: Int
}

struct Chapter3 {
    //20. JSONデータの読み込み
    //Wikipedia記事のJSONファイルを読み込み，「イギリス」に関する記事本文を表示せよ．問題21-29では，ここで抽出した記事本文に対して実行せよ．
    static func q20(input: String) -> WikiItem {
        return ukWikiItem(input: input)
    }
    
    //21. カテゴリ名を含む行を抽出
    //記事中でカテゴリ名を宣言している行を抽出せよ．
    static func q21(input: String) -> [String] {
        let ukWiki = ukWikiItem(input: input)
        let linesHasCategory = ukWiki.text.components(separatedBy: CharacterSet.newlines).filter{ $0.contains("Category") }
        return linesHasCategory
    }
    
    //22. カテゴリ名の抽出
    //記事のカテゴリ名を（行単位ではなく名前で）抽出せよ．
    static func q22(input: String) -> Set<String> {
        let linesHasCategory = q21(input: input)
        let categoryNameMatches = linesHasCategory.map { (line) -> String in
            let regex =
            """
            \\[Category:\
            (\
            .+[^\\]]\
            )\
            \\]
            """
            return line.matches(regex: regex)[1]
        }
        var categoryNames = [String]()
        categoryNameMatches.forEach { (s) in
            if s.contains("|") {
                categoryNames.append(contentsOf: s.components(separatedBy: "|"))
            } else {
                categoryNames.append(s)
            }
        }
        return Set(categoryNames)
    }
    
    //23. セクション構造
    //記事中に含まれるセクション名とそのレベル（例えば"== セクション名 =="なら1）を表示せよ．
    static func q23(input: String) -> String {
        let ukWiki = ukWikiItem(input: input)
        let lines = ukWiki.text.components(separatedBy: CharacterSet.newlines)
        let sections = lines.flatMap { (line) -> Section? in
            let matche = line.matches(regex: "^(={2,})\\s*(.+?)\\s*={2,}$")
            if !matche.isEmpty {
                return Section(name: matche[2], level: (matche.first?.count)! - 1)
            } else {
                return nil
            }
        }
        return sections.map{ "\($0.name), \($0.level)" }.joined(separator: "\n")
    }
  
    //24. ファイル参照の抽出
    //記事から参照されているメディアファイルをすべて抜き出せ．
    static func q24(input: String) -> String {
        let ukWiki = ukWikiItem(input: input)
        let matches = ukWiki.text.matches(regex: "(ファイル:(.+?)|File:(.+?))[|]")
        let matchedFileIndeices = stride(from: 2, to: matches.count, by: 3).map{$0}
        
        return matches.enumerated().reduce("") { (combined, pair) -> String in
            if matchedFileIndeices.contains(pair.offset) {
                return "\(combined)\(pair.element)\n"
            } else {
                return combined
            }
        }
    }

}

extension Chapter3 {
    /// 入力されたjsonからタイトルがイギリスのものを探し先頭の1件を返す
    static func ukWikiItem(input: String) -> WikiItem {
        let lines = input.components(separatedBy: CharacterSet.newlines).filter{ !$0.isEmpty }
        let decoder = JSONDecoder()
        let ukWiki = lines.map{
            return try! decoder.decode(WikiItem.self, from: $0.data(using: .utf8)!)
            }.filter{ $0.title == "イギリス" }
            .first!
        return ukWiki
    }
}

extension String {
    
    /// 入力された正規表現文字列にマッチした結果を配列で返す
    ///
    ///     //（例）下記のように正規表現を引数に渡して利用します。
    ///     let matches = "Swift Moji 9876".matches(regex: "^(.+)\\s(\\d{4})")
    ///
    ///     // 戻り値の先頭はマッチ結果の完全なキャプチャを返します。
    ///     matches.first // "Swift Moji 9876"
    ///
    ///     // 個別のキャプチャされたグループは[1]以降に格納されます。
    ///     matches[1] // "Swift Moji"
    ///     matches.last // "2017"
    ///
    /// - Parameter regex: 正規表現文字列
    /// - Returns: マッチ結果
    func matches(regex: String!) -> [String] {
        do {
            let regex = try NSRegularExpression(pattern: regex, options: [])
            let results = regex.matches(in: self,
                                        options: [],
                                        range: NSRange(location: 0, length: self.count))
            var matches = [String]()
            
            for result in results {
                for i in 0..<result.numberOfRanges {
                    let range = Range.init(result.range(at: i), in: self)
                    if let r = range {
                        matches.append(String(self[r]))
                    }
                }
            }
            
            return matches
        } catch let error as NSError {
            assertionFailure("invalid regex: \(error.localizedDescription)")
            return []
        }
    }
}

```