Swift4で言語処理100本ノック 2015 第1章,第2章

# はじめに
@mono0926さんの[[Swift 3] 言語処理100本ノック 2015 第1章: 準備運動](https://qiita.com/mono0926/items/c4c717cb4aeffaec8d17#extension%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88%E3%82%B3%E3%83%BC%E3%83%89)を見て[言語処理100本ノック 2015
](https://www.cl.ecei.tohoku.ac.jp/nlp100/)を知り、面白そうだと思ってSwift4でトライしてみました。完全なコードは[Github](https://github.com/tikidunpon/nlp100)にあげてあります。
続き: [Swift4で言語処理100本ノック 2015 第3章を途中まで](https://qiita.com/tikidunpon/items/2e8f052c78364dee4daa)

# 第1章: 準備運動

```swift

struct Chapter1 {
    
    // 00. 文字列の逆順
    // 文字列"stressed"の文字を逆に（末尾から先頭に向かって）並べた文字列を得よ．
    static func q0(input: String) -> String {
        return String(input.reversed())
    }
    
    
    // 01. 「パタトクカシーー」
    // 「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ．
    static func q1(input: String) -> String {
        return String(input.enumerated().filter{ $0.offset % 2 == 1 }.map{ $0.element })
    }
    
    // 02. 「パトカー」＋「タクシー」＝「パタトクカシーー」
    // 「パトカー」＋「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ．
    static func q2(input1: String, input2: String) -> String {
        return zip(input1, input2).map{ String([$0, $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: ",.")).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 Dictionary(uniqueKeysWithValues:
            input.trimmingCharacters(in: CharacterSet(charactersIn: ",."))
                .components(separatedBy: " ")
                .enumerated()
                .map{ (i, v) in
                    if condition.contains(i + 1) {
                        return (String(v.first!), i + 1)
                    } else {
                        return (String(v.prefix(2)), i + 1)
                    }
        })
    }
    
    // 05. n-gram
    // 与えられたシーケンス（文字列やリストなど）からn-gramを作る関数を作成せよ．この関数を用い，"I am an NLPer"という文から単語bi-gram，文字bi-gramを得よ．
    // 単語bi-gram
    static func q5_1(input: String, n: Int) -> [String] {
        return wordNgram(input: input, n: n)
    }
    
    // 文字bi-gram
    static func q5_2(input: String, n: Int) -> [String] {
        return charNgram(input: input, n: n)
    }
    
    //06. 集合
    //"paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を，それぞれ, XとYとして求め，XとYの和集合，積集合，差集合を求めよ．
    //さらに，'se'というbi-gramがXおよびYに含まれるかどうかを調べよ．
    static func q6_1_union(input1: String, input2: String) -> Set<String> {
        let x = Set(charNgram(input: input1, n: 2))
        let y = Set(charNgram(input: input2, n: 2))
        print("x.union(y) = \(x.union(y))")
        print("x.intersection(y) = \(x.intersection(y))")
        print("x.subtract(y) = \(x.subtracting(y))")
        x.contains("se") ? print("x contains se") : print("x does not contains x")
        y.contains("se") ? print("y contains se") : print("y does not contains x")
        
        return x.union(y)
    }
    
    //07. テンプレートによる文生成
    //引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ．さらに，x=12, y="気温", z=22.4として，実行結果を確認せよ．
    static func q7(x: Int, y: String, z: Float) -> String {
        return "\(x)時の\(y)は\(z)"
    }
    
    //    08. 暗号文
    //    与えられた文字列の各文字を，以下の仕様で変換する関数cipherを実装せよ．
    //
    //    英小文字ならば(219 - 文字コード)の文字に置換
    //    その他の文字はそのまま出力
    //    この関数を用い，英語のメッセージを暗号化・復号化せよ．
    static func q8(input: String) -> String {
        return chiper(input: 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 words = input.components(separatedBy: " ")
        let mapped = words.map { (word) -> String in
            guard word.count > 4 else { return word }
            var dropped = Array(String(word.dropFirst().dropLast()))
            dropped.forEach({_ in
                let ran = arc4random_uniform(UInt32(dropped.count - 1))
                dropped.swapAt(dropped.count - 1, Int(ran))
            })
            dropped.insert(contentsOf: String(word.first!), at: 0)
            dropped.append(contentsOf: String(word.last!))
            return String(dropped)
        }
        
        return mapped.joined(separator: " ")
    }
}

extension Chapter1 {
    static func wordNgram(input: String, n: Int) -> [String] {
        let words = input.components(separatedBy: " ").map{ $0.trimmingCharacters(in: CharacterSet(charactersIn: ",.")) }
        return words.enumerated().flatMap{ (i, v) in
            if words.indices.contains(i + n - 1) {
                return words[i..<i+n].reduce("", +)
            } else {
                return nil
            }
        }
    }
    
    static func charNgram(input: String, n: Int) -> [String] {
        let words = input.components(separatedBy: " ")
            .map{ $0.trimmingCharacters(in: CharacterSet(charactersIn: ",.")) }
            .joined().characters.map{ String($0) }
        return words.enumerated().flatMap{ (i, v) in
            if words.indices.contains(i + n - 1) {
                return words[i..<i+n].reduce("", +)
            } else {
                return nil
            }
        }
    }
    
    static func chiper(input: String) -> String {
        let asciiLowercaseSet = Set("abcdefghijklmnopqrstuvwxyz".map{ String($0) })
        return input.map{ (c) in
            if asciiLowercaseSet.contains(String(c)) {
                return String(Character(Unicode.Scalar(219 - c.unicodeScalars.first!.value)!))
            } else {
                return String(c)
            }
            }.joined(separator: "")
    }
}

```

# 第2章: UNIXコマンドの基礎
```swift

import Foundation

struct Chapter2 {
    // 10. 行数のカウント
    // 行数をカウントせよ．確認にはwcコマンドを用いよ．
    static func wc(input: String) -> Int {
        let result = input.components(separatedBy: CharacterSet.newlines)
        return result.isEmpty ? result.count - 1 : result.count
    }
    
    // 11. タブをスペースに置換
    // タブ1文字につきスペース1文字に置換せよ．確認にはsedコマンド，trコマンド，もしくはexpandコマンドを用いよ．
    static func tr(input: String, replacement: String) -> String {
        return input.replacingOccurrences(of: "\t", with: replacement, options: .regularExpression, range: nil)
    }
    
    //12. 1列目をcol1.txtに，2列目をcol2.txtに保存
    //各行の1列目だけを抜き出したものをcol1.txtに，2列目だけを抜き出したものをcol2.txtとしてファイルに保存せよ．確認にはcutコマンドを用いよ．
    static func cut(input: String, n: Int) -> String {
        let rows = input.components(separatedBy: CharacterSet.newlines)
        
        let column = rows.map {
            return !$0.isEmpty ? $0.components(separatedBy: CharacterSet.whitespaces)[n - 1] : $0
        }
        return column.joined(separator: "\n")
    }
    
    // 13. col1.txtとcol2.txtをマージ
    // 12で作ったcol1.txtとcol2.txtを結合し，元のファイルの1列目と2列目をタブ区切りで並べたテキストファイルを作成せよ．確認にはpasteコマンドを用いよ．
    static func paste(column1: String, column2: String) -> String {
        return zip(column1.components(separatedBy: "\n"),
                   column2.components(separatedBy: "\n"))
            .map{ !$0.isEmpty ? "\($0)\t\($1)" : $0 }.joined(separator: "\n")
    }
    
    //14. 先頭からN行を出力
    //自然数Nをコマンドライン引数などの手段で受け取り，入力のうち先頭のN行だけを表示せよ．確認にはheadコマンドを用いよ．
    static func head(input: String, rowNumber: Int) -> String {
        let rows = input.components(separatedBy: CharacterSet.newlines)
        guard rows.indices.contains(rowNumber - 1) else { return "" }
        return rows.prefix(rowNumber).joined(separator: "\n") + "\n"
    }
    
    //15. 末尾のN行を出力
    //自然数Nをコマンドライン引数などの手段で受け取り，入力のうち末尾のN行だけを表示せよ．確認にはtailコマンドを用いよ．
    static func tail(input: String, rowNumber: Int) -> String {
        let rows = input.components(separatedBy: CharacterSet.newlines).dropLast()
        guard rows.count >= rowNumber else { return "" }
        return rows.suffix(rowNumber).joined(separator: "\n") + "\n"
    }
    
    //16. ファイルをN分割する
    //自然数Nをコマンドライン引数などの手段で受け取り，入力のファイルを行単位でN分割せよ．同様の処理をsplitコマンドで実現せよ．
    static func split(input: String, chunkSize: Int) -> [String] {
        let rows = input.components(separatedBy: CharacterSet.newlines).dropLast()
        let splited = stride(from: 0, to: rows.count, by: chunkSize).map { (v) in
            return rows[v..<min(v + chunkSize, rows.count)].joined(separator: "\n") + "\n"
        }
        return splited
    }
    
    //17. １列目の文字列の異なり
    //1列目の文字列の種類（異なる文字列の集合）を求めよ．確認にはsort, uniqコマンドを用いよ．
    static func sortedUniq(input: String) -> String {
        let column1 = Chapter2.cut(input: input, n: 1)
        let sorted = Array(Set(column1.components(separatedBy: CharacterSet.newlines)))
            .sorted(by: <).filter{ !$0.isEmpty }
        return sorted.joined(separator: "\n" ) + "\n"
    }

    //18. 各行を3コラム目の数値の降順にソート
    //各行を3コラム目の数値の逆順で整列せよ（注意: 各行の内容は変更せずに並び替えよ）．確認にはsortコマンドを用いよ（この問題はコマンドで実行した時の結果と合わなくてもよい）．
    static func sortedDesc(input: String) -> [String] {
        let rows = input.components(separatedBy: CharacterSet.newlines).dropLast()
        let sorted = rows.sorted { (row1, row2) -> Bool in
            let elements1 = row1.components(separatedBy: CharacterSet.whitespaces)
            let elements2 = row2.components(separatedBy: CharacterSet.whitespaces)
            return elements1[2] > elements2[2]
        }
        return sorted
    }
    
    //19. 各行の1コラム目の文字列の出現頻度を求め，出現頻度の高い順に並べる
    //各行の1列目の文字列の出現頻度を求め，その高い順に並べて表示せよ．確認にはcut, uniq, sortコマンドを用いよ．
    static func sortedFrequencyDesc(input: String) -> [(String, Int)] {
        let column1 = Chapter2.cut(input: input, n: 1).components(separatedBy: CharacterSet.newlines)
        let countedSet = NSCountedSet(array: column1)
        let sortedTupleArray = Dictionary(uniqueKeysWithValues:
            countedSet.map { (o) -> (String, Int) in
                let s = o as! String
                return (s, countedSet.count(for: s))
        }).sorted(by: {$0.1 > $1.1 })
        return sortedTupleArray.filter{ !$0.key.isEmpty }
    }
    
}

```

# 得たこと  
## Swift4の文字列やコレクションの扱いに慣れた  
Stringがコレクションに戻ったので"abc".characters.map..のようにしていた所が"abc".map..と素直にかけるようになりました。またSubStringの扱いにも慣れてきました。他にも`Dictionary(uniqueKeysWithValues:`でタプル配列からDictionaryを生成できます。mapで加工した結果から素直に生成できて良い感じです。

```swift

  static func q4(input: String, condition: [Int]) -> [String : Int] {
        return Dictionary(uniqueKeysWithValues:
            input.trimmingCharacters(in: CharacterSet(charactersIn: ",."))
                .components(separatedBy: " ")
                .enumerated()
                .map{ (i, v) in
                    if condition.contains(i + 1) {
                        return (String(v.first!), i + 1)
                    } else {
                        return (String(v.prefix(2)), i + 1)
                    }
        })
    }
```

## strideを始めて使った
与えられた要素数で配列を分割するのにstrideを使いました。 [こちら](https://gist.github.com/ericdke/fa262bdece59ff786fcb)を参考にしました。

```swift

  static func split(input: String, chunkSize: Int) -> [String] {
        let rows = input.components(separatedBy: CharacterSet.newlines).dropLast()
        let splited = stride(from: 0, to: rows.count, by: chunkSize).map { (v) in
            return rows[v..<min(v + chunkSize, rows.count)].joined(separator: "\n") + "\n"
        }
        return splited
    }
```

## Unixコマンドに少し慣れた  

ソートしてランキングを出すのは[定石](http://blog.nomadscafe.jp/2012/07/sort-uniq.html)らしいです、知らなかったです。  

```
cut -f 1 hightemp.txt | sort | uniq -c | sort -r > q19_a1
```

# 足りないこと  
## 異常系のテスト  
とりあえずテストを通すことが優先で正常系の1パターンのみの所がありました。妥当性含め少しリファクタしつつ足したい所。

## もっと良い書き方
正直汚いので、誰か教えて下さい。

# 結論
確かにコレクションの操作中心でしたがあまり使わないzipに親しめましたし、十分勉強になりました。楽しいので次の第3章: 正規表現も進めようと思います。3章ではCodableを活用する場面もありそうです。