Index of a substring in a string with Swift

前端 未结 11 1560
自闭症患者
自闭症患者 2020-11-22 14:24

I\'m used to do this in JavaScript:

var domains = \"abcde\".substring(0, \"abcde\".indexOf(\"cd\")) // Returns \"ab\"

Swift doesn\'t have t

相关标签:
11条回答
  • 2020-11-22 14:58

    Swift 5

        extension String {
        enum SearchDirection {
            case first, last
        }
        func characterIndex(of character: Character, direction: String.SearchDirection) -> Int? {
            let fn = direction == .first ? firstIndex : lastIndex
            if let stringIndex: String.Index = fn(character) {
                let index: Int = distance(from: startIndex, to: stringIndex)
                return index
            }  else {
                return nil
            }
        }
    }
    

    tests:

     func testFirstIndex() {
            let res = ".".characterIndex(of: ".", direction: .first)
            XCTAssert(res == 0)
        }
        func testFirstIndex1() {
            let res = "12345678900.".characterIndex(of: "0", direction: .first)
            XCTAssert(res == 9)
        }
        func testFirstIndex2() {
            let res = ".".characterIndex(of: ".", direction: .last)
            XCTAssert(res == 0)
        }
        func testFirstIndex3() {
            let res = "12345678900.".characterIndex(of: "0", direction: .last)
            XCTAssert(res == 10)
        }
    
    0 讨论(0)
  • 2020-11-22 15:02

    Leo Dabus's answer is great. Here is my answer based on his answer using compactMap to avoid Index out of range error.

    Swift 5.1

    extension StringProtocol {
        func ranges(of targetString: Self, options: String.CompareOptions = [], locale: Locale? = nil) -> [Range<String.Index>] {
    
            let result: [Range<String.Index>] = self.indices.compactMap { startIndex in
                let targetStringEndIndex = index(startIndex, offsetBy: targetString.count, limitedBy: endIndex) ?? endIndex
                return range(of: targetString, options: options, range: startIndex..<targetStringEndIndex, locale: locale)
            }
            return result
        }
    }
    
    // Usage
    let str = "Hello, playground, playground, playground"
    let ranges = str.ranges(of: "play")
    ranges.forEach {
        print("[\($0.lowerBound.utf16Offset(in: str)), \($0.upperBound.utf16Offset(in: str))]")
    }
    
    // result - [7, 11], [19, 23], [31, 35]
    
    0 讨论(0)
  • 2020-11-22 15:07

    Swift 5

       let alphabat = "abcdefghijklmnopqrstuvwxyz"
    
        var index: Int = 0
        
        if let range: Range<String.Index> = alphabat.range(of: "c") {
             index = alphabat.distance(from: alphabat.startIndex, to: range.lowerBound)
            print("index: ", index) //index: 2
        }
    
    0 讨论(0)
  • 2020-11-22 15:08

    edit/update:

    Xcode 11.4 • Swift 5.2 or later

    import Foundation
    
    extension StringProtocol {
        func index<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
            range(of: string, options: options)?.lowerBound
        }
        func endIndex<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
            range(of: string, options: options)?.upperBound
        }
        func indices<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Index] {
            ranges(of: string, options: options).map(\.lowerBound)
        }
        func ranges<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Range<Index>] {
            var result: [Range<Index>] = []
            var startIndex = self.startIndex
            while startIndex < endIndex,
                let range = self[startIndex...]
                    .range(of: string, options: options) {
                    result.append(range)
                    startIndex = range.lowerBound < range.upperBound ? range.upperBound :
                        index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
            }
            return result
        }
    }
    

    usage:

    let str = "abcde"
    if let index = str.index(of: "cd") {
        let substring = str[..<index]   // ab
        let string = String(substring)
        print(string)  // "ab\n"
    }
    

    let str = "Hello, playground, playground, playground"
    str.index(of: "play")      // 7
    str.endIndex(of: "play")   // 11
    str.indices(of: "play")    // [7, 19, 31]
    str.ranges(of: "play")     // [{lowerBound 7, upperBound 11}, {lowerBound 19, upperBound 23}, {lowerBound 31, upperBound 35}]
    

    case insensitive sample

    let query = "Play"
    let ranges = str.ranges(of: query, options: .caseInsensitive)
    let matches = ranges.map { str[$0] }   //
    print(matches)  // ["play", "play", "play"]
    

    regular expression sample

    let query = "play"
    let escapedQuery = NSRegularExpression.escapedPattern(for: query)
    let pattern = "\\b\(escapedQuery)\\w+"  // matches any word that starts with "play" prefix
    
    let ranges = str.ranges(of: pattern, options: .regularExpression)
    let matches = ranges.map { str[$0] }
    
    print(matches) //  ["playground", "playground", "playground"]
    
    0 讨论(0)
  • 2020-11-22 15:10

    There are three closely connected issues here:

    • All the substring-finding methods are over in the Cocoa NSString world (Foundation)

    • Foundation NSRange has a mismatch with Swift Range; the former uses start and length, the latter uses endpoints

    • In general, Swift characters are indexed using String.Index, not Int, but Foundation characters are indexed using Int, and there is no simple direct translation between them (because Foundation and Swift have different ideas of what constitutes a character)

    Given all that, let's think about how to write:

    func substring(of s: String, from:Int, toSubstring s2 : String) -> Substring? {
        // ?
    }
    

    The substring s2 must be sought in s using a String Foundation method. The resulting range comes back to us, not as an NSRange (even though this is a Foundation method), but as a Range of String.Index (wrapped in an Optional, in case we didn't find the substring at all). However, the other number, from, is an Int. Thus we cannot form any kind of range involving them both.

    But we don't have to! All we have to do is slice off the end of our original string using a method that takes a String.Index, and slice off the start of our original string using a method that takes an Int. Fortunately, such methods exist! Like this:

    func substring(of s: String, from:Int, toSubstring s2 : String) -> Substring? {
        guard let r = s.range(of:s2) else {return nil}
        var s = s.prefix(upTo:r.lowerBound)
        s = s.dropFirst(from)
        return s
    }
    

    Or, if you prefer to be able to apply this method directly to a string, like this...

    let output = "abcde".substring(from:0, toSubstring:"cd")
    

    ...then make it an extension on String:

    extension String {
        func substring(from:Int, toSubstring s2 : String) -> Substring? {
            guard let r = self.range(of:s2) else {return nil}
            var s = self.prefix(upTo:r.lowerBound)
            s = s.dropFirst(from)
            return s
        }
    }
    
    0 讨论(0)
提交回复
热议问题