106

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?

Inder Kumar Rathore
  • 39,458
  • 17
  • 135
  • 184
Armand Grillet
  • 3,229
  • 5
  • 30
  • 60
  • @eric-d This is not a duplicate of the one you've mentioned. The OP is about indexOf() and not substring(). – mdupls Sep 23 '15 at 19:26
  • In Swift 2 there is a String.rangeOfString(String) method that returns a Range. – mdupls Sep 24 '15 at 12:08

11 Answers11

179

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"]
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • 3
    This isn't quite right, as `"ab".indexOf("a")` and `"ab".indexOf("c")` both return `0`. – Graham Jun 07 '16 at 08:33
  • and for those that have upgraded to Swift 3.0: extension String { func indexOf(string: String) -> String.Index? { return range(of: string, options: .literal, range: nil, locale: nil)?.lowerBound } } – gammachill Jul 30 '16 at 13:25
  • 2
    **Make sure you** `import Foundation` **or this will not work.** Because really you're just using NSString at this point. – CommaToast Aug 12 '16 at 16:59
  • `range: nil` and `locale: nil` can be omitted, those parameters have a default value `nil`. – Martin R Dec 07 '16 at 10:32
  • 1
    This is a ton of work - and not the Swift native way. See @Inder Kumar Rathore's answer below - simple use of '.range( of: "text" )' method – Marchy Jun 02 '18 at 14:34
  • @Marchy kkkk his answer it is exactly the same as mine but limited to the first occurrence – Leo Dabus Jun 02 '18 at 14:46
  • Getting this message - String has no member ranges. Anything on this? – Jayprakash Dubey Sep 25 '18 at 12:06
  • @JayprakashDubey are you sure your extension is added correctly to your project? have you tried your code in playground? – Leo Dabus Sep 25 '18 at 12:09
  • With **Swift 4.2** you need to do `str.index(of: "play")?.encodedOffset` to get 7 – lewis Oct 12 '18 at 20:09
  • @lewis https://stackoverflow.com/questions/34540185/how-to-convert-index-to-type-int-in-swift/34540310#comment98673377_52397384 – Leo Dabus Oct 20 '20 at 22:21
  • @LeoDabus in C++, python you got string.find routine, why the f do you have to always extend the language in SWIFT yourself? Almost all the solutions around swift questions relate to writing own extensions. – YuZ Nov 07 '21 at 12:30
  • What routine is that? Does it find only the first occurrence or all of them? – Leo Dabus Nov 07 '21 at 12:31
  • in C++, python you can optionally pass additional position parameter, finding all occurrences ridiculously straightforward: string.find(substring, idx_firstoccurence+1) – YuZ Nov 07 '21 at 12:37
  • @YuZ you can do exactly the same in Swift using `index(after:)`. So you are just complaining that Swift collection is not integer based. – Leo Dabus Nov 07 '21 at 12:39
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/238958/discussion-between-yuz-and-leo-dabus). – YuZ Nov 07 '21 at 12:43
  • How do I get the result as an int I can work with properly? If I print the result of your `str.index(of: "play")` example in Playground (Swift 5, Xcode 14.2), then I get something weird like `Swift.String.Index(_rawBits: 458765)`. – Neph Apr 18 '23 at 14:24
  • String is not indexed by Int. If you need the distance from startIndex you need to use collection’s distance method – Leo Dabus Apr 18 '23 at 16:09
  • 1
    @Neph [How to convert "Index" to type "Int" in Swift?](https://stackoverflow.com/a/34540310/2303865) – Leo Dabus Apr 18 '23 at 17:27
70

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]

Inder Kumar Rathore
  • 39,458
  • 17
  • 135
  • 184
37

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")
}
Viktor
  • 1,155
  • 14
  • 23
8

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
Ashis Laha
  • 2,176
  • 3
  • 16
  • 17
  • What is `_compoundOffset`, the number of bytes in the string until that point? – ArielSD Feb 28 '18 at 23:33
  • This is extremely inefficient. It will offset the string from the start index on every iteration. You should simply keep the index position and get the index(after:) on each iteration. Note also that `string[startIndex...endIndex]` would crash. Btw Swift 5 or later you can use `PartialRangeFrom` subscript `let substring1 = str[str.startIndex...]` – Leo Dabus Oct 16 '21 at 22:16
7

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.

Armand Grillet
  • 3,229
  • 5
  • 30
  • 60
4

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
    }
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Is this copying the original string? What if the original string were lengthy and this were a repeated operation? That can be done with zero data copy in the jvm world. – WestCoastProjects Jun 18 '20 at 23:46
  • @javadba No copying in deriving a Substring, that's the whole point of a Substring. Basically that code just walks a bunch of pointers. – matt Jun 19 '20 at 00:12
  • OK - I see the `dropFirst` and have not looked at how that's implemented. How can we extract the final returning `Substring` into just a `String` ? I'm seeing super lengthy posts *just on that* .. – WestCoastProjects Jun 19 '20 at 00:48
  • Just coerce to a String. I'm not sure whether there's a copy at that point; there might not be, as long as neither this nor the original string is modified, but I'm unclear on the details of how String adopts copy-on-write. – matt Jun 19 '20 at 00:51
  • ok thx - coercion here we go. I'm getting warning "Cast from Substring to String always fails" when doing `as! String` – WestCoastProjects Jun 19 '20 at 00:53
  • Right, that's a cast, not a coercion. A coercion is `String(theSubstring)`. – matt Jun 19 '20 at 00:59
  • oh! thx for that. I did not even recognize the distinction of the `cast` / `coercion` term as such on the `jvm` – WestCoastProjects Jun 19 '20 at 01:00
  • @javadba In Swift, you can only cast a thing to a type that it already is. Coercion is when you transform a thing to a different type (which you do by creating an instance of the other type). – matt Jun 19 '20 at 01:06
  • Yes I get that: for `jvm` I would say "create a `String` out of it" or "do a `toString()`" - vs "cast it to a `String`" e.g. via `asInstanceOf[String]` (scala). But the former lacked a tidy specific term. now we have `coercion` – WestCoastProjects Jun 19 '20 at 01:08
4

Swift 5

   let alphabet = "abcdefghijklmnopqrstuvwxyz"

    var index: Int = 0
    
    if let range: Range<String.Index> = alphabet.range(of: "c") {
         index = alphabet.distance(from: alphabet.startIndex, to: range.lowerBound)
        print("index: ", index) //index: 2
    }
Adrian
  • 16,233
  • 18
  • 112
  • 180
skety777
  • 210
  • 2
  • 6
  • https://stackoverflow.com/a/34540310/2303865 Note that String indices is not integer based. You won't be able to use it to subscript the collection and access the elements (a character or a substring) – Leo Dabus Nov 07 '21 at 12:15
3

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)
    }
Vyacheslav
  • 26,359
  • 19
  • 112
  • 194
  • Adding `String.` prefix inside a String extension is redundant. `SearchDirection` would suffice. Note also that Swift is a type inferred language. No need to explicitly set the resulting type if it is not generic. – Leo Dabus Nov 02 '21 at 03:02
2

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
Abhinav Arora
  • 537
  • 1
  • 7
  • 23
2

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]
Changnam Hong
  • 1,669
  • 18
  • 29
1

Have you considered using NSRange?

if let range = mainString.range(of: mySubString) {
  //...
}
Kirill
  • 738
  • 10
  • 26