Swift find all occurrences of a substring

前端 未结 7 1927
无人及你
无人及你 2020-12-06 00:46

I have an extension here of the String class in Swift that returns the index of the first letter of a given substring.

Can anybody please help me make it so it will

相关标签:
7条回答
  • 2020-12-06 00:59

    I have tweaked the accepted answer so that case sensitivity can be configured

    extension String {
        func allIndexes(of subString: String, caseSensitive: Bool = true) -> [Int] {
            let subString = caseSensitive ? subString : subString.lowercased()
            let mainString = caseSensitive ? self : self.lowercased()
            var indices = [Int]()
            var searchStartIndex = mainString.startIndex
            while searchStartIndex < mainString.endIndex,
                let range = mainString.range(of: subString, range: searchStartIndex..<mainString.endIndex),
                !range.isEmpty
            {
                let index = distance(from: mainString.startIndex, to: range.lowerBound)
                indices.append(index)
                searchStartIndex = range.upperBound
            }
    
            return indices
        }
    }
    
    0 讨论(0)
  • 2020-12-06 01:00

    I know we aren't playing code golf here, but for anyone interested in a functional style one-line implementation that doesn't use vars or loops, this is another possible solution:

    extension String {
        func indices(of string: String) -> [Int] {
            return indices.reduce([]) { $1.encodedOffset > ($0.last ?? -1) && self[$1...].hasPrefix(string) ? $0 + [$1.encodedOffset] : $0 }
        }
    }
    
    0 讨论(0)
  • 2020-12-06 01:04

    This could be done with recursive method. I used a numeric string to test it. It returns an optional array of Int, meaning it will be nil if no substring can be found.

    extension String {
        func indexes(of string: String, offset: Int = 0) -> [Int]? {
            if let range = self.range(of : string) {
                if !range.isEmpty {
                    let index = distance(from : self.startIndex, to : range.lowerBound) + offset
                    var result = [index]
                    let substr = self.substring(from: range.upperBound)
                    if let substrIndexes = substr.indexes(of: string, offset: index + distance(from: range.lowerBound, to: range.upperBound)) {
                        result.append(contentsOf: substrIndexes)
                    }
                    return result
                }
            }
            return nil
        }
    }
    
    let numericString = "01234567890123456789012345678901234567890123456789012345678901234567890123456789"
    numericString.indexes(of: "3456")
    
    0 讨论(0)
  • 2020-12-06 01:08

    You just keep advancing the search range until you can't find any more instances of the substring:

    extension String {
        func indicesOf(string: String) -> [Int] {
            var indices = [Int]()
            var searchStartIndex = self.startIndex
    
            while searchStartIndex < self.endIndex,
                let range = self.range(of: string, range: searchStartIndex..<self.endIndex),
                !range.isEmpty
            {
                let index = distance(from: self.startIndex, to: range.lowerBound)
                indices.append(index)
                searchStartIndex = range.upperBound
            }
    
            return indices
        }
    }
    
    let keyword = "a"
    let html = "aaaa"
    let indicies = html.indicesOf(string: keyword)
    print(indicies) // [0, 1, 2, 3]
    
    0 讨论(0)
  • 2020-12-06 01:14

    There's not really a built-in function to do this, but we can implement a modified Knuth-Morris-Pratt algorithm to get all the indices of the string we want to match. It should also be very performant as we don't need to repeatedly call range on the string.

    extension String {
        func indicesOf(string: String) -> [Int] {
            // Converting to an array of utf8 characters makes indicing and comparing a lot easier
            let search = self.utf8.map { $0 }
            let word = string.utf8.map { $0 }
    
            var indices = [Int]()
    
            // m - the beginning of the current match in the search string
            // i - the position of the current character in the string we're trying to match
            var m = 0, i = 0
            while m + i < search.count {
                if word[i] == search[m+i] {
                    if i == word.count - 1 {
                        indices.append(m)
                        m += i + 1
                        i = 0
                    } else {
                        i += 1
                    }
                } else {
                    m += 1
                    i = 0
                }
            }
    
            return indices
        }
    }
    
    0 讨论(0)
  • 2020-12-06 01:15

    Here are 2 functions. One returns [Range<String.Index>], the other returns [Range<Int>]. If you don't need the former, you can make it private. I've designed it to mimic the range(of:options:range:locale:) method, so it supports all the same features.

    import Foundation
    
    extension String {
        public func allRanges(
            of aString: String,
            options: String.CompareOptions = [],
            range: Range<String.Index>? = nil,
            locale: Locale? = nil
        ) -> [Range<String.Index>] {
    
            // the slice within which to search
            let slice = (range == nil) ? self[...] : self[range!]
    
            var previousEnd = s.startIndex
            var ranges = [Range<String.Index>]()
    
            while let r = slice.range(
                of: aString, options: options,
                range: previousEnd ..< s.endIndex,
                locale: locale
            ) {
                if previousEnd != self.endIndex { // don't increment past the end
                        previousEnd = self.index(after: r.lowerBound)
                }
                ranges.append(r)
            }
    
            return ranges
        }
    
        public func allRanges(
            of aString: String,
            options: String.CompareOptions = [],
            range: Range<String.Index>? = nil,
            locale: Locale? = nil
        ) -> [Range<Int>] {
            return allRanges(of: aString, options: options, range: range, locale: locale)
                .map(indexRangeToIntRange)
        }
    
    
        private func indexRangeToIntRange(_ range: Range<String.Index>) -> Range<Int> {
            return indexToInt(range.lowerBound) ..< indexToInt(range.upperBound)
        }
    
        private func indexToInt(_ index: String.Index) -> Int {
            return self.distance(from: self.startIndex, to: index)
        }
    }
    
    let s = "abc abc  abc   abc    abc"
    print(s.allRanges(of: "abc") as [Range<String.Index>])
    print()
    print(s.allRanges(of: "abc") as [Range<Int>])
    
    0 讨论(0)
提交回复
热议问题