[Swift 3] 言語処理100本ノック 2015 第1章: 準備運動

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/) がなかなか面白そうだったので、Swift 3.0(まもなくリリースですがまだベータ)にて始めてみました。

GitHubリポジトリ: [mono0926/nlp100-swift: http://www.cl.ecei.tohoku.ac.jp/nlp100/](https://github.com/mono0926/nlp100-swift)

こんな感じで書いています:
![Screen Shot 2016-09-01 at 2.17.29 PM.png](https://qiita-image-store.s3.amazonaws.com/0/19398/ef0c0176-1ad1-0065-0576-9b8d4bd9a15d.png)


# なぜ始めたか

- 何となくSwiftで色々処理書きたいことがあるものの、書くアテが無かったりする時の気分転換に良さそう
  - iOSアプリ開発では画面実装など中心で、実は細々とした処理書く機会があまり無い
  - さらに、既存プロジェクトはGM版リリースギリギリまで正式版使っているので、最新ベータ版使う機会があまり無い
- Swiftによる言語処理のサンプルコードあまり見ないので、良い例になるかも？
- 最新Swiftキャッチアップにもなりそう
  - 常にベータ版含めた最新Xcode・Swiftで書くつもりなので、アップデートによる影響を直に感じられて良さそう

# 方針

- 標準ライブラリ以外は使わない
  - 汎用処理は自分で`extension`などを書く
- TDDぽく書く
- コメントは、[Markdownぽいマークアップ](https://developer.apple.com/library/mac/documentation/Xcode/Reference/xcode_markup_formatting_ref/)を活用
- 可読性・[Swift APIガイドライン](http://qiita.com/mono0926/items/11203c8cce0d7f4548db)なども、極力意識したい
- Playgroundはどうしようか悩む
  - テストコードと役割が被るから省略でいいかな気分

# やってみた感想

- 第1章は、言語処理というより、(文字列の)コレクション操作という感じ
- Swift 3での変更の影響でやはりけっこう手間取る
- `String`や`Collection`の`Index`周りの扱い難しい
- 第1章も準備運動とはいえけっこう勉強になったし、第2章以降続けたい
- 基本GitHubで黙々と書き進めて、Qiita記事は何か気付きがあれば書く、みたいな感じにしたい

---

# Chapter1解答

以下貼り付けたコードはメンテナンスあまりしないと思うので、最新版は https://github.com/mono0926/nlp100-swift をご覧ください。

## 本体コード

- `reduce`多用した感
- `fileprivate`(Swift 2までの`private`と一緒)を初めて使った

```swift
import Foundation

struct Chapter1 {
    /**
    # 00. 文字列の逆順
     
     文字列"stressed"の文字を逆に（末尾から先頭に向かって）並べた文字列を得よ．
    */
    static func q0(_ input: String) -> String {
        return String(input.characters.reversed())
    }
    /**
    # 01. 「パタトクカシーー」
     
     「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ．
    */
    static func q1(_ input: String) -> String {
        return String(input.characters.enumerated()
            .filter { i, _ in i % 2 == 1 }
            .map { $1 })
    }
    /**
    # 02. 「パトカー」＋「タクシー」＝「パタトクカシーー」
 
    「パトカー」＋「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ．
    */
    static func q2(_ input1: String, _ input2: String) -> String {
        return zip(input1.characters, input2.characters)
            .map { String($0) + String($1) }
            .reduce("", +)
    }
    /**
    # 03. 円周率
 
    "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し，各単語の（アルファベットの）文字数を先頭から出現順に並べたリストを作成せよ．
    */
    static func q3(_ input: String) -> [Int] {
        return input.components(separatedBy: " ")
            .map { $0.trimmingCharacters(in: CharacterSet(charactersIn: ",.")).characters.count }
    }
    /**
    # 04. 元素記号
    
    "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し，1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字，それ以外の単語は先頭に2文字を取り出し，取り出した文字列から単語の位置（先頭から何番目の単語か）への連想配列（辞書型もしくはマップ型）を作成せよ．
    */
    static func q4(_ input: String, condition: [Int]) -> [String: Int] {
        return input.components(separatedBy: " ").enumerated()
            .map { (i, v) in condition.contains(i + 1) ? (i, v[0..<1]!) : (i, v[0..<2]!) }
            .reduce([String: Int]()) { sum, v in
                var sum = sum
                sum[v.1] = v.0 + 1
                return sum
        }
    }
    /**
    # 05. n-gram
     
     与えられたシーケンス（文字列やリストなど）からn-gramを作る関数を作成せよ．この関数を用い，"I am an NLPer"という文から単語bi-gram，文字bi-gramを得よ．
    */
    static func q5Word(_ input: String) -> [[String]] {
        return ngramWord(input, n: 2)
    }
    static func q5Char(_ input: String) -> [String] {
        return ngramChar(input, n: 2)
    }
    /**
    # 06. 集合
 
    "paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を，それぞれ, XとYとして求め，XとYの和集合，積集合，差集合を求めよ．さらに，'se'というbi-gramがXおよびYに含まれるかどうかを調べよ．
    */
    static func q6(_ input1: String, _ input2: String) -> (sum: Set<String>, diff1: Set<String>, diff2: Set<String>, product: Set<String>)  {
        let n = 2
        let X = ngramChar(input1, n: n)
        let Y = ngramChar(input2, n: n)
        let XSet = Set(X)
        let YSet = Set(Y)
        let sum = Set(X + Y)
        let diff1 = XSet.subtracting(YSet)
        let diff2 = YSet.subtracting(XSet)
        let product = XSet.intersection(YSet)
        return (sum: sum, diff1: diff1, diff2: diff2, product: product)
    }
    static func q6IsContainAsBiGram(sentence: String, word: String) -> Bool {
        let bigram = ngramChar(sentence, n: 2)
        return bigram.contains(word)
    }
    /**
    # 07. テンプレートによる文生成
 
    引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ．さらに，x=12, y="気温", z=22.4として，実行結果を確認せよ．
    */
    static func q7(x: Int, y: AnyObject, z: AnyObject) -> String {
        return "\(x)時の\(y)は\(z)"
    }
    /**
    # 08. 暗号文
 
    与えられた文字列の各文字を，以下の仕様で変換する関数cipherを実装せよ．
 
    - 英小文字ならば(219 - 文字コード)の文字に置換
    - その他の文字はそのまま出力
 
    この関数を用い，英語のメッセージを暗号化・復号化せよ．
    */
    static func q8(_ input: String) -> String {
        return cipher(input)
    }
    /**
    # 09. Typoglycemia
 
    スペースで区切られた単語列に対して，各単語の先頭と末尾の文字は残し，それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ．ただし，長さが４以下の単語は並び替えないこととする．適当な英語の文（例えば"I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."）を与え，その実行結果を確認せよ．
    */
    static func q9(_ input: String) -> String {
        let separator = " "
        var words = input.components(separatedBy: separator)
        guard words.count > 4 else { return input }
        let first = words.removeFirst()
        let last = words.removeLast()
        words.shuffle()
        return ([first] + words + [last]).joined(separator: separator)
    }
}

fileprivate extension Chapter1 {
    // TODO: ちょっと汚い(　´･‿･｀)
    fileprivate static func ngramWord(_ input: String, n: Int) -> [[String]] {
        let words = input.components(separatedBy: " ") + (0..<n-1).map { _ in "" }
        return words.reduce([[String]]()) { sum, word in
            var sum = sum
            var words: [String] = { () -> [String] in
                if let lasts = sum.last?.dropFirst() {
                    return Array(lasts)
                }
                return []
            }()
            words = (0..<(n - 1 - words.count)).map { _ in "" } + words
            sum.append(words + [word])
            return sum
        }
    }
    fileprivate static func ngramChar(_ input: String, n: Int) -> [String] {
        return input.characters.reduce([String]()) { sum, char in
            var sum = sum
            let count = sum.last?.characters.count ?? 0
            let first = sum.last?[count - (n - 1)..<count] ?? " "
            sum.append(first + String(char))
            return sum
            }
            .filter { !$0.contains(" ") }
    }
    fileprivate static func cipher(_ input: String) -> String {
        return input.characters.map { c in
            let s = String(c)
            let lowercased = s.lowercased()
            return lowercased == s ? String(Character(asciiCode: (219 - c.asciiCode()))!) : s
            }
            .joined(separator: "")
    }
}
```

## 本体コードのテストコード

```swift
import XCTest
@testable import NLP100Swift

class NLP100SwiftTests: XCTestCase {
    func testQ0() {
        XCTAssertEqual(Chapter1.q0("stressed"), "desserts")
    }
    func testQ1() {
        XCTAssertEqual(Chapter1.q1("パタトクカシーー"), "タクシー")
    }
    func testQ2() {
        XCTAssertEqual(Chapter1.q2("パトカー", "タクシー"), "パタトクカシーー")
    }
    func testQ3() {
        XCTAssertEqual(Chapter1.q3("Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."), [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9])
    }
    func testQ4() {
        XCTAssertEqual(
            Chapter1.q4("Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.", condition: [1, 5, 6, 7, 8, 9, 15, 16, 19]),
            ["H": 1, "Ne": 10, "Be": 4, "Al": 13, "B": 5, "O": 8, "Li": 3, "F": 9, "He": 2, "S": 16, "Cl": 17, "K": 19, "Ar": 18, "C": 6, "N": 7, "Mi": 12, "Si": 14, "Ca": 20, "P": 15, "Na": 11])
    }
    func testQ5Word() {
        let result = Chapter1.q5Word("I am an NLPer")
        print(result)
        let expected = [["", "I"], ["I", "am"], ["am", "an"], ["an", "NLPer"], ["NLPer", ""]]
        XCTAssertEqual(result.count, expected.count)
        result.enumerated().forEach {
            XCTAssertEqual($1[0], expected[$0][0])
            XCTAssertEqual($1[1], expected[$0][1])
        }
    }
    func testQ5Char() {
        let result = Chapter1.q5Char("I am an NLPer")
        let expected = ["am", "an", "NL", "LP", "Pe", "er"]
        XCTAssertEqual(result, expected)
    }
    func testQ6() {
        let input1 = "paraparaparadise"
        let input2 = "paragraph"
        let result = Chapter1.q6(input1, input2)
        XCTAssertEqual(result.sum, Set(["pa", "se", "ad", "ap", "ra", "gr", "ag", "ph", "ar", "di", "is"]))
        XCTAssertEqual(result.diff1, Set(["se", "ad", "di", "is"]))
        XCTAssertEqual(result.diff2, Set(["gr", "ag", "ph"]))
        XCTAssertEqual(result.product, Set(["pa", "ar", "ap", "ra"]))
        let word = "se"
        XCTAssertTrue(Chapter1.q6IsContainAsBiGram(sentence: input1, word: word))
        XCTAssertFalse(Chapter1.q6IsContainAsBiGram(sentence: input2, word: word))
    }
    func testQ7() {
        XCTAssertEqual(Chapter1.q7(x: 12, y: "気温" as AnyObject, z: 22.4 as AnyObject), "12時の気温は22.4")
    }
    func testQ8() {
        XCTAssertEqual(Chapter1.q8("Masayuki Ono"), "Mzhzbfpr»Oml")
        XCTAssertEqual(Chapter1.q8("Mzhzbfpr»Oml"), "Masayuki Ono")
    }
    func testQ9() {
        let inputLong = "I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."
        let resultLong = Chapter1.q9(inputLong)
        print("Q9. resultLong: \(resultLong)")
        XCTAssertNotEqual(resultLong, inputLong)
        XCTAssertTrue(resultLong.hasPrefix("I "))
        XCTAssertTrue(resultLong.hasSuffix(" ."))
        let inputShort = "I couldn't believe ."
        let resultShort = Chapter1.q9(inputShort)
        XCTAssertEqual(resultShort, inputShort)
        
    }
}
```

## `extension`

- 標準では便利メソッドがあまり無いので、自分で生やす必要あり
  - ライブラリとして作っているわけではないので汎用性はそこまで高くしていない(`String`の`subscript`の`range`でマイナスをサポートしていない、など)
- 逆に言うと、随時`extension`で機能補填していけば、言語処理もまあまあ快適に書けそう(　´･‿･｀)

```swift
import Foundation

extension String {
    
    subscript (range: Range<Int>) -> String? {
        let count = characters.count
        let lower = range.lowerBound
        let upper = range.upperBound
        if lower >= count || upper > count { return nil }
        let startIndex = characters.index(characters.startIndex, offsetBy: lower)
        let endIndex = characters.index(characters.startIndex, offsetBy: upper)
        return String(characters[startIndex..<endIndex])
    }
    func asciiCode() -> UInt32? {
        guard characters.count == 1 else { return nil }
        return characters.first!.asciiCode()
    }
}

extension Character
{
    func asciiCode() -> UInt32 {
        let characterString = String(self)
        let scalars = characterString.unicodeScalars
        
        return scalars[scalars.startIndex].value
    }
    init?(asciiCode: UInt32) {
        guard let scalar = UnicodeScalar(asciiCode) else {
            return nil
        }        
        self = Character(scalar)
    }
}

extension Collection {
    func shuffled() -> [Generator.Element] {
        var list = Array(self)
        list.shuffle()
        return list
    }
}

extension MutableCollection where Index == Int {
    mutating func shuffle() {
        let c = Int(count.toIntMax())
        guard c > 1 else { return }
        
        for i in 0..<(c - 1) {
            let j = Int(arc4random_uniform(UInt32(c - i))) + i
            guard i != j else { continue }
            swap(&self[i], &self[j])
        }
    }
}
```

## `extension`のテストコード

```swift
import XCTest
@testable import NLP100Swift

class ExtensionsTests: XCTestCase {
    func testSuscript() {
        XCTAssertEqual("foo"[0..<0], "")
        XCTAssertEqual("foo"[0..<1], "f")
        XCTAssertEqual("foo"[0..<2], "fo")
        XCTAssertEqual("foo"[0..<3], "foo")
        XCTAssertEqual("foo"[0..<4], nil)
        XCTAssertEqual("foo"[1..<1], "")
        XCTAssertEqual("foo"[1..<2], "o")
        XCTAssertEqual("foo"[1..<3], "oo")
        XCTAssertEqual("foo"[1..<4], nil)
    }
    
    func testToAsciiCode() {
        XCTAssertEqual("A".asciiCode(), 65)
        XCTAssertEqual("a".asciiCode(), 97)
    }
    func testFromAsciiCode() {
        XCTAssertEqual(Character(asciiCode: 65), "A")
        XCTAssertEqual(Character(asciiCode: 97), "a")
    }
    // TODO: ランダムなので、たまに失敗するのが課題🤔
    func testShuffle() {
        let input = ["a", "b", "c", "d", "e"]
        let result = input.shuffled()
        XCTAssertNotEqual(result, input)
        XCTAssertEqual(Set(result), Set(input))
    }
}
```