

どうしてもRangeの仕組みが理解できず、開始インデックス～取得文字数を算出する方法が分かりません。  
実装したい内容としては、開始インデックス～終了インデックスまでを取得するという内容です。  
JAVAで実装している以下の構文をSwiftで実装する方法が分かりません。  
教えて頂けますでしょうか。

[code]

    // argContentHTML = ホームページのHTML
    // HTMLをParse
    // 先頭・最後インデックスを取得
    intIDSearchIndex = argContentHTML.indexOf("tag1");
    intIDFootSearchIndex = argContentHTML.indexOf("tag2",
                                                intIDSearchIndex + "tag1".length());
    
    // インデックスが見つかったか判定
    if(intIDSearchIndex == -1 ||
       intIDFootSearchIndex == -1){
    
        // どちらかのインデックスが見つからなかった場合、データ追加しない
    }else{
        // 正常の場合、データ設定
        // Entry部からIDを取得
        strRet = argContentHTML.substring(intIDSearchIndex + "tag1".length(),
                                          intIDFootSearchIndex);
    }
    
[/code]


CC BY-SA 3.0
2015-11-19T20:48:12.357
19057
2015-11-20T10:18:18.323
2015-11-19T21:43:41.720
3516
13337
question
ios swift
SwiftのRangeの仕組みが理解出来ず苦戦しています


おそらく、質問者さんのなさりたいことは、iOS/OS
Xにおいては、`NSScanner`クラスを使うことになるでしょう。`NSScanner`クラスに関する情報は、AppleのドキュメントさえObjective-
Cをもとに書かれたものがほとんどで、Swiftベースのものが少ないので、勉強するのはたいへんだと思いますが、慣れれば、ひじょうに少ないプログラムコードで、パースすることができるようになるでしょう。  
Playgroundで動かす、サンプルコードを載せます。

[code]

    import UIKit
    
    // パース対象のHTMLテキストを用意する。
    let htmlString = "<html>\n<head><title>Title</title>\n<body><p>Hello, world.</p>\n<p>hoge hoge</p>\n<p>fuga fuga</p>\n<p>foo foo</p>\n<p>bar bar</p>\n</body></html>"
    // htmlStringを対象とするNSScannerインスタンスを作成。
    let scanner = NSScanner(string: htmlString)
    // 結果を格納するテキストを用意する。
    var resultString = ""
    // スキャンしたテキストを一時取り置きする。NSString型で、かつOptional型。
    var tmpString: NSString? = ""
    // スキャン開始。テキストの最後が来るまで繰り返し。
    while scanner.atEnd == false {
        // <p>を検索して、その手前でスキャン停止。
        scanner.scanUpToString("<p>", intoString: nil)
        // <p>を読み飛ばす。
        scanner.scanString("<p>", intoString: nil)
        // </p>まで移動して、その間のテキストを取得。アドレス演算子（らしきもの）&があるのに注意。
        scanner.scanUpToString("</p>", intoString: &tmpString)
        // tmpStringをresultStringに追加。
        if let theString = tmpString as? String {
            resultString += theString
            // 末尾にコンマとスペースを追加。
            resultString += ", "
        }
        // tmpStringを初期化する。
        tmpString = nil
    }
    print(resultString)
    
[/code]

結果：

> Hello, world., hoge hoge, fuga fuga, foo foo, bar bar,

※似たクラスに`NSXMLParser`というものがあります。XML⊃HTMLなので、HTMLのパースにも使えるんじゃないかと思われますが、単独タグ、`<br>`、`<img>`などを使っていると、うまく動かないので、お勧めになりません。（`<br/>`、`<img/>`を求められる＞）


CC BY-SA 3.0
2015-11-20T03:53:15.643
19064
2015-11-20T05:55:08.213
2015-11-20T05:55:08.213
7362
7362
19057
answer


Swift2/Xcode7を前提として、

[code]

    import Foundation
    
    let str = "AAAtag2BBBtag1CCCtag2DDD"
    if let
        tag1 = str.rangeOfString("tag1"),
        tag2 = str.rangeOfString("tag2", range: tag1.endIndex ..< str.endIndex) {
    
            let strRet = String(str[tag1.endIndex ..< tag2.startIndex])
            // -> "CCC"
            // str.substringWithRange(tag1.endIndex ..< tag2.startIndex) も可
    }
    
[/code]

`str.rangeOfString("tag1")` で `str` 中の `"tag1"` が現れる範囲（`Range`）を得ます。  
`str.rangeOfString("tag2", range: tag1.endIndex ..< str.endIndex)` は `"tag2"`
が現れる範囲を`str`の `"tag1"` の終了位置から最後までの範囲で検索します。  
で、標準の `subscript` を使ってスライスを得るのですが、これの型は `String`ではなく、 `String.CharacterView`
なので、`String()` イニシャライザで `String` 型に変換します。

Foundationに頼らずにSwift標準ライブラリだけでやるならこんな感じでしょうか:

[code]

    let str = "AAAtag2BBBtag1CCCtag2DDD"
    
    let tag1 = "tag1".characters
    let tag2 = "tag2".characters
    let str_ = str.characters
    
    if let
        start = str_.indices.indexOf({str_.suffixFrom($0).startsWith(tag1)})?.advancedBy(tag1.count),
        end = (start ..< str_.endIndex).indexOf({str_.suffixFrom($0).startsWith(tag2)}) {
    
            let strRet = String(str_[start ..< end])
            // -> "CCC"
    }
    
[/code]

可読性ひどいですね。  
もうちょっと可読性高めるために、サブストリングの開始インデックスを探すところをファクターアウトして:

[code]

    extension CollectionType where SubSequence.Generator.Element: Equatable {
        /// `self` 中で `sub` の開始インデックスを返す。見つからなければ `nil`。
        /// デフォルトでは `self` 全体から、rangeが与えられたときはその範囲で検索する。
        func startIndexOf(sub: SubSequence, range: Range<Index>? = nil) -> Index? {
            let range = range ?? indices
            return range.indexOf {
                self.suffixFrom($0).startsWith(sub)
            }
        }
    }
    
    let str = "AAAtag2BBBtag1CCCtag2DDD"
    
    let tag1 = "tag1".characters
    let tag2 = "tag2".characters
    let str_ = str.characters
    
    if let
        start = str_.startIndexOf(tag1)?.advancedBy(tag1.count),
        end = str_.startIndexOf(tag2, range: start ..< str_.endIndex) {
    
            let strRet = String(str_[start ..< end])
            // -> "CCC"
    }
    
[/code]

* * *

せっかくなのでベンチマーク  
質問と関係ないですが自分の備忘録のために書かせてください

[code]

    import XCTest
    
    extension CollectionType where SubSequence.Generator.Element: Equatable {
        /// `self` 中で `sub` の開始インデックスを返す。見つからなければ `nil`。
        /// デフォルトでは `self` 全体から、rangeが与えられたときはその範囲で検索する。
        func startIndexOf(sub: SubSequence, range: Range<Index>? = nil) -> Index? {
            let range = range ?? indices
            return range.indexOf {
                self.suffixFrom($0).startsWith(sub)
            }
        }
    }
    
    // オリジナル回答
    func find_rangeOfString(str: String, s1: String, s2: String) -> String? {
        if let
            tag1 = str.rangeOfString(s1),
            tag2 = str.rangeOfString(s2, range: tag1.endIndex ..< str.endIndex) {
                return String(str[tag1.endIndex ..< tag2.startIndex])
        }
        return nil
    }
    
    // 標準ライブラリのみ String.CharacterView で検索
    func find_characters(str: String, s1: String, s2: String) -> String? {
        let s1 = s1.characters
        let s2 = s2.characters
        let str = str.characters
        if let
            start = str.startIndexOf(s1)?.advancedBy(s1.count),
            end = str.startIndexOf(s2, range: start ..< str.endIndex) {
                return String(str[start ..< end])
        }
        return nil
    }
    
    // 標準ライブラリのみ String.UTF16View で検索
    func find_utf16(str: String, s1: String, s2: String) -> String? {
        let s1 = s1.utf16
        let s2 = s2.utf16
        let str = str.utf16
        if let
            start = str.startIndexOf(s1)?.advancedBy(s1.count),
            end = str.startIndexOf(s2, range: start ..< str.endIndex) {
                return String(str[start ..< end])
        }
        return nil
    }
    
    // NSScanner で検索
    func find_scanner(str: String, s1: String, s2: String) -> String? {
        let scanner = NSScanner(string: str)
        var result: NSString?
        scanner.scanUpToString(s1, intoString: nil)
        scanner.scanString(s1, intoString: nil)
        scanner.scanUpToString(s2, intoString: &result)
        return scanner.atEnd ? nil : result as? String
    }
    
    // NSRegularExpression で検索
    func find_regex(str: String, s1: String, s2:String) -> String? {
        let regex = try! NSRegularExpression(
            pattern: NSRegularExpression.escapedPatternForString(s1)
                + "(.*?)"
                + NSRegularExpression.escapedPatternForString(s2),
            options: []
        )
        if let range = regex.firstMatchInString(str, options: [], range: NSMakeRange(0, str.utf16.count))?.rangeAtIndex(1) {
            return (str as NSString).substringWithRange(range)
        }
        return nil
    }
    
    class StringTestTests: XCTestCase {
    
        private func doTest(fn: (String, String, String) -> String?) {
            let str = "私の好物は<em>焼き魚</em>です。"
            let tag1 = "<em>"
            let tag2 = "</em>"
            XCTAssertEqual(fn(str, tag1, tag2), "焼き魚")
            measureBlock {
                for _ in 0 ..< 10000 {
                    fn(str, tag1, tag2)
                }
            }
        }
    
        func testRangeOfString() { doTest(find_rangeOfString) }
        func testCharacters() { doTest(find_characters) }
        func testUTF16() { doTest(find_utf16) }
        func testScanner() { doTest(find_scanner) }
        func testRegex() { doTest(find_regex) }
    }
    
[/code]

結果 iPhone6 / iOS9.1 / Releaseビルド（複数回実行して同じ傾向 ）

[code]

    testRangeOfString: 0.060 sec
    testCharacters:    0.453 sec
    testUTF16:         0.020 sec
    testScanner:       0.144 sec
    testRegex:         0.498 sec
    
[/code]

当然、元文字列の質にも依りますので、どれかが常に一番速いわけではないはずですが、このパターンではこんな結果でした。  
正規表現版は毎回パターンを作っているので遅いのは当たり前なんですが、意外にも `find_utf16` が最速。`CharacterView` を使うか
`UTF16View`
を使うかでこれだけの違いが出ます。何を使うべきかというのも、やりたいことと元文字列の性質によるのでなんとも言えないですが、今回のバリエーションでは、微妙に機能的な違いがあります。

[code]

    find_rangeOfString("私の好物は焼き魚です。", s1: "", s2: "") // -> "焼き魚"
    find_characters("私の好物は焼き魚です。", s1: "", s2: "") // -> nil
    find_utf16("私の好物は焼き魚です。", s1: "", s2: "") // -> "焼き魚"
    find_scanner("私の好物は焼き魚です。", s1: "", s2: "") // -> "焼き魚"
    find_regex("私の好物は焼き魚です。", s1: "", s2: "") // -> "焼き魚"
    
    find_rangeOfString("私の好物は焼き魚です。", s1: "", s2: "") // -> "焼き魚"
    find_characters("私の好物は焼き魚です。", s1: "", s2: "") // -> nil
    find_utf16("私の好物は焼き魚です。", s1: "", s2: "") // -> nil
    find_scanner("私の好物は焼き魚です。", s1: "", s2: "") // -> "焼き魚"
    find_regex("私の好物は焼き魚です。", s1: "", s2: "") // -> "焼き魚"
    
[/code]

なんでこうなるかというと、CharacterView では 連続する国旗Emojiは 一文字として扱われるからなのですが、詳しくは
<https://stackoverflow.com/q/26862282/3804019>


CC BY-SA 3.0
2015-11-20T04:13:06.000
19067
2015-11-20T10:18:18.323
2017-05-23T12:38:55.250
-1
3489
19057
answer