LoginSignup
6
4

More than 1 year has passed since last update.

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

Last updated at Posted at 2017-11-23

はじめに

前回Swift4で言語処理100本ノック 2015 第1章,第2章を書いて大分日が経ってしまいました。
第3章は途中までですが[^1]、折角なので得たことをまとめておこうと思います。
今回もテストコードを書きながら問題20〜24までを進めました、リポジトリはこちらです。

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

JSONのデコードにCodableを利用

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

// 利用したい型にCodableを採用する  
struct WikiItem: Codable {
    let title: String
    let text: String
}
/// 入力された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

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

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

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

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

/// 入力された正規表現文字列にマッチした結果を配列で返す
///
///     //(例)下記のように正規表現を引数に渡して利用します。
///     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: マッチ結果

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

スクリーンショット 2017-11-23 17.04.30.png

答え合わせ

答え合わせにはPythonで書かれている、素人の言語処理100本ノック:まとめ 第3章: 正規表現を参考にしました。
正規表現しっかり頑張ってて偉いなと思いました。

余談(第1章の副産物)

第1章で学んだNグラムを使って2つ文字列の類似度を出すコードを書きました。
こんな感じに使います。類似度が最も低い時0.0、もっとも高い時1.0を返します。

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にあるようにレーベンシュタイン距離を使うよりはパフォーマンスが出るとのことです。

コード全文


//
//  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 []
        }
    }
}

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4