19

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 return an array of all occurrences instead of just the first one?

Thank you.

extension String {
    func indexOf(string : String) -> Int {
        var index = -1
        if let range = self.range(of : string) {
            if !range.isEmpty {
                index = distance(from : self.startIndex, to : range.lowerBound)
            }
        }
        return index
    }
}

For example instead of a return value of 50 I would like something like [50, 74, 91, 103]

8 Answers8

39

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]
Code Different
  • 90,614
  • 16
  • 144
  • 163
  • 1
    `while let`. brilliant. – Alexander Nov 04 '16 at 00:54
  • Hmm. This seems to fail. `let keyword = "hello" let html = "My Websitehellohellohello" let indicies = html.indicesOf(string: keyword) print(indicies)` `fatal error: cannot increment beyond endIndex` –  Nov 04 '16 at 01:02
  • It seems like it doesn't like things at the end of the string. It looks really good so far though. –  Nov 04 '16 at 01:04
  • 1
    A more simplified example: `let keyword = "a" let html = "a" let indicies = html.indicesOf(string: keyword) print(indicies)` `fatal error: cannot increment beyond endIndex` –  Nov 04 '16 at 01:05
  • 1
    If you put two of the same keyword in a row it will only find one. It finds two if you put three in a row though. Looking much better though. –  Nov 04 '16 at 01:23
  • It's another bug. The perils of insufficient testing. Updated again – Code Different Nov 04 '16 at 01:29
  • It doesn't work always if the length of the keyword is > 1, e.g. keyword="aba", html="ababa", result=[0] but it should be [0, 2] instead. That happens because of: searchStartIndex = range.upperBound – Eugenio Aug 25 '18 at 15:52
  • 2
    @Eugenio if you allow substrings to overlap, advance the search start index one character at a time: `searchStartIndex = string.index(after: searchStartIndex)` – Code Different Aug 25 '18 at 16:01
10

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 }
    }
}
Daniel Hall
  • 13,457
  • 4
  • 41
  • 37
8

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>])
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • Works great for ranges. I was looking for integers for my purpose but it is definitely helpful to most. –  Nov 04 '16 at 01:35
  • 2
    @Alexander you have `s.startIndex` and `s.endIndex`, i believe they're meant to be `slice.startIndex` and `slice.endIndex`? – Workshed Jul 27 '17 at 10:09
  • +1 On this answer. Int indexes are easier to understand, but they generally result in less performant code. eg: a range of `String.Index` allows you to take a substring like `let r = s[range]` which is more convenient as well as faster than indexing the string using Int's. – Berik Nov 06 '19 at 10:16
  • This line gives an error: let slice = (range == nil) ? self : self[range!] .. and the error is: Subscript 'subscript(_:)' requires the types 'String.Index' and 'Int' be equivalent – Starwave Dec 23 '19 at 12:13
  • @Starwave This is really old code, I haven't been maintaining it. – Alexander Dec 23 '19 at 14:38
7

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
    }
}
Kevin Bui
  • 223
  • 1
  • 7
  • 2
    Oooo very nice, I appreciate that you implemented this yourself. `we don't need to repeatedly call range on the string` that's probably fine, most likely `range` just does KMP search, as well. – Alexander Nov 04 '16 at 01:30
  • This is great! One little shortcoming I noticed: if you pass in a string of 0 length, you hit a crash - want me to suggest a fix? – karlbecker_com Dec 03 '17 at 20:17
  • Some advice. Just want to point out that Swift has terrible optimisation of array collections, and this method is far better of using C/ObjC for performant reasons. As of yet, I have not seen anything that Swift does natively be performant. – Peter Suwara Jan 22 '19 at 02:30
  • This needs a bit of rewriting as C writing is not supported anymore – thibaut noah Mar 04 '19 at 12:23
  • How to make case insestive search ? – pankaj nigam Jul 31 '20 at 08:11
4

Please check the following answer for finding multiple items in multiple locations

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
}

func attributedStringWithColor(_ strings: [String], color: UIColor, characterSpacing: UInt? = nil) -> NSAttributedString {
    let attributedString = NSMutableAttributedString(string: self)
    for string in strings {
        let indexes = self.indicesOf(string: string)
        for index in indexes {
            let range = NSRange(location: index, length: string.count)
            attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: color, range: range)
        }
    }
    
    guard let characterSpacing = characterSpacing else {return attributedString}
    
    attributedString.addAttribute(NSAttributedString.Key.kern, value: characterSpacing, range: NSRange(location: 0, length: attributedString.length))
    
    return attributedString
}

can be used as follows :

let message = "Item 1 + Item 2 + Item 3"
message.attributedStringWithColor(["Item", "+"], color: UIColor.red)

and gets the result enter image description here

Unnikrishnan
  • 158
  • 7
1

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")
Adam
  • 26,549
  • 8
  • 62
  • 79
1

Swift 5.7 - iOS 16.0+ - macOS 13.0+

Foundation has now func ranges<C>(of other: C) -> [Range<Self.Index>] where C : Collection, Self.Element == C.Element

so it is much simplified:

extension String {
    func allIndices(of subString: String, caseSensitive: Bool = true) -> [Int] {
        if caseSensitive {
            return self.ranges(of: subString).map { distance(from: self.startIndex, to: $0.lowerBound) }
        } else {
            return self.lowercased().ranges(of: subString.lowercased()).map { distance(from: self.startIndex, to: $0.lowerBound) }
        }
    }
}

soundflix
  • 928
  • 9
  • 22
0

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
    }
}
Kaunteya
  • 3,107
  • 1
  • 35
  • 66