Index of a substring in a string with Swift

SwiftStringSubstring

Swift Problem Overview


I'm used to do this in JavaScript:

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

Swift doesn't have this function, how to do something similar?

Swift Solutions


Solution 1 - Swift

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"]

Solution 2 - Swift

Using String[Range<String.Index>] subscript you can get the sub string. You need starting index and last index to create the range and you can do it as below

let str = "abcde"
if let range = str.range(of: "cd") {
  let substring = str[..<range.lowerBound] // or str[str.startIndex..<range.lowerBound]
  print(substring)  // Prints ab
}
else {
  print("String not present")
}

If you don't define the start index this operator ..< , it take the starting index. You can also use str[str.startIndex..<range.lowerBound] instead of str[..<range.lowerBound]

Solution 3 - Swift

Swift 5

Find index of substring

let str = "abcdecd"
if let range: Range<String.Index> = str.range(of: "cd") {
    let index: Int = str.distance(from: str.startIndex, to: range.lowerBound)
    print("index: ", index) //index: 2
}
else {
    print("substring not found")
}

Find index of Character

let str = "abcdecd"
if let firstIndex = str.firstIndex(of: "c") {
    let index = str.distance(from: str.startIndex, to: firstIndex)
    print("index: ", index)   //index: 2
}
else {
    print("symbol not found")
}

Solution 4 - Swift

In Swift 4 :

Getting Index of a character in a string :
let str = "abcdefghabcd"
if let index = str.index(of: "b") {
   print(index) // Index(_compoundOffset: 4, _cache: Swift.String.Index._Cache.character(1))
}
Creating SubString (prefix and suffix) from String using Swift 4:
let str : String = "ilike"
for i in 0...str.count {
    let index = str.index(str.startIndex, offsetBy: i) // String.Index
    let prefix = str[..<index] // String.SubSequence
    let suffix = str[index...] // String.SubSequence
    print("prefix \(prefix), suffix : \(suffix)")
}
Output
prefix , suffix : ilike
prefix i, suffix : like
prefix il, suffix : ike
prefix ili, suffix : ke
prefix ilik, suffix : e
prefix ilike, suffix : 
If you want to generate a substring between 2 indices , use :
let substring1 = string[startIndex...endIndex] // including endIndex
let subString2 = string[startIndex..<endIndex] // excluding endIndex

Solution 5 - Swift

Doing this in Swift is possible but it takes more lines, here is a function indexOf() doing what is expected:

func indexOf(source: String, substring: String) -> Int? {
    let maxIndex = source.characters.count - substring.characters.count
    for index in 0...maxIndex {
        let rangeSubstring = source.startIndex.advancedBy(index)..<source.startIndex.advancedBy(index + substring.characters.count)
        if source.substringWithRange(rangeSubstring) == substring {
            return index
        }
    }
    return nil
}

var str = "abcde"
if let indexOfCD = indexOf(str, substring: "cd") {
    let distance = str.startIndex.advancedBy(indexOfCD)
    print(str.substringToIndex(distance)) // Returns "ab"
}

This function is not optimized but it does the job for short strings.

Solution 6 - Swift

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
    }
}

Solution 7 - Swift

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
    }

Solution 8 - Swift

In the Swift version 3, String doesn't have functions like -

str.index(of: String)

If the index is required for a substring, one of the ways to is to get the range. We have the following functions in the string which returns range -

str.range(of: <String>)
str.rangeOfCharacter(from: <CharacterSet>)
str.range(of: <String>, options: <String.CompareOptions>, range: <Range<String.Index>?>, locale: <Locale?>)

For example to find the indexes of first occurrence of play in str

var str = "play play play"
var range = str.range(of: "play")
range?.lowerBound //Result : 0
range?.upperBound //Result : 4

Note : range is an optional. If it is not able to find the String it will make it nil. For example

var str = "play play play"
var range = str.range(of: "zoo") //Result : nil
range?.lowerBound //Result : nil
range?.upperBound //Result : nil

Solution 9 - Swift

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]

Solution 10 - Swift

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)
    }

Solution 11 - Swift

Have you considered using NSRange?

if let range = mainString.range(of: mySubString) {
  //...
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionArmand GrilletView Question on Stackoverflow
Solution 1 - SwiftLeo DabusView Answer on Stackoverflow
Solution 2 - SwiftInder Kumar RathoreView Answer on Stackoverflow
Solution 3 - SwiftViktorView Answer on Stackoverflow
Solution 4 - SwiftAshis LahaView Answer on Stackoverflow
Solution 5 - SwiftArmand GrilletView Answer on Stackoverflow
Solution 6 - SwiftmattView Answer on Stackoverflow
Solution 7 - Swiftskety777View Answer on Stackoverflow
Solution 8 - SwiftAbhinav AroraView Answer on Stackoverflow
Solution 9 - SwiftChangnam HongView Answer on Stackoverflow
Solution 10 - SwiftVyacheslavView Answer on Stackoverflow
Solution 11 - SwiftKirill KudaevView Answer on Stackoverflow