141

I want to convert the index of a letter contained within a string to an integer value. Attempted to read the header files but I cannot find the type for Index, although it appears to conform to protocol ForwardIndexType with methods (e.g. distanceTo).

var letters = "abcdefg"
let index = letters.characters.indexOf("c")!

// ERROR: Cannot invoke initializer for type 'Int' with an argument list of type '(String.CharacterView.Index)'
let intValue = Int(index)  // I want the integer value of the index (e.g. 2)

Any help is appreciated.

ndmeiri
  • 4,979
  • 12
  • 37
  • 45
Christopher
  • 5,806
  • 7
  • 31
  • 41

9 Answers9

112

edit/update:

Xcode 11 • Swift 5.1 or later

extension StringProtocol {
    func distance(of element: Element) -> Int? { firstIndex(of: element)?.distance(in: self) }
    func distance<S: StringProtocol>(of string: S) -> Int? { range(of: string)?.lowerBound.distance(in: self) }
}

extension Collection {
    func distance(to index: Index) -> Int { distance(from: startIndex, to: index) }
}

extension String.Index {
    func distance<S: StringProtocol>(in string: S) -> Int { string.distance(to: self) }
}

Playground testing

let letters = "abcdefg"

let char: Character = "c"
if let distance = letters.distance(of: char) {
    print("character \(char) was found at position #\(distance)")   // "character c was found at position #2\n"
} else {
    print("character \(char) was not found")
}

let string = "cde"
if let distance = letters.distance(of: string) {
    print("string \(string) was found at position #\(distance)")   // "string cde was found at position #2\n"
} else {
    print("string \(string) was not found")
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • 86
    I'm so confused why they wouldn't just decide to make a function that returns the integer-value index of an array's element.. smh – MarksCode Apr 16 '17 at 20:21
  • 7
    Not all characters can be stored in a single byte. You should take some time and read the Swift String documentation – Leo Dabus Apr 16 '17 at 20:28
  • 1
    https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/StringsAndCharacters.html – Leo Dabus Apr 16 '17 at 20:29
  • 5
    Tbh I'm trying to get the integer-value index of a regular array's element, not a string. – MarksCode Apr 16 '17 at 20:30
  • 1
    That should be simple if your array elements conform to equatable protocol – Leo Dabus Apr 16 '17 at 20:31
  • 1
    I'd think so, but I keep getting an `Array.index` instead of an int when trying to get the index of a string in an array of strings: http://imgur.com/a/IaaHn – MarksCode Apr 16 '17 at 20:34
  • 49
    Making simple things unnecessarily complicated :( – Iulian Onofrei Nov 30 '17 at 13:49
  • Please see https://github.com/frogcjn/OffsetIndexableCollection-String-Int-Indexable- – frogcjn Dec 12 '17 at 04:29
  • `encodedOffset` is going to be deprecated, compare https://github.com/apple/swift-evolution/blob/master/proposals/0241-string-index-explicit-encoding-offset.md. – Martin R Feb 26 '19 at 06:03
  • It looks kinda complicated to me, but I am a noob. I wrote this https://gist.github.com/psksvp/bb84c28bee8753f4d0af791d79bf281d – psksvp Jan 11 '20 at 13:02
  • This is so complicated (awful) and yet there doesn't seem to be any other simpler answers, it helped me to decide to refactor my code and go a completely different route. So thx. – John Pitts Apr 06 '20 at 17:39
  • @JohnPitts The intent of this post is to add functionality to the language, not to teach you how to code. All you need to do is to add those extensions to your project. If you have a question about the code itself feel free to open a new question. – Leo Dabus Apr 08 '20 at 22:37
  • 1
    If you cast your String to an NSString, you can access the older, simpler APIs for numerical indexing, **but** your code will absolutely have bugs or break if it has to interact with UTF sequences where multiple characters combine into a single symbol. As @LeoDabus suggests, see the Swift String docs for more info on this issue. Or, if you want to pluck your eyes out, try reading the UTF spec... – Zack Aug 30 '20 at 16:54
  • Thanks so much, it's really disappointing that this still isn't possible in vanilla Swift in 2023! For people who want it to work like in Java (-1 instead of nil if the element/substring isn't found), simply do: 1:`let r = range(of: string)?.lowerBound.distance(in: self)`, 2:`if r==nil { return -1 }`, 3:`return r!` and return a regular `Int` (instead of the optional `Int?`). – Neph Apr 20 '23 at 08:49
45

Works for Xcode 13 and Swift 5

let myString = "Hello World"

if let i = myString.firstIndex(of: "o") {
  let index: Int = myString.distance(from: myString.startIndex, to: i)
  print(index) // Prints 4
}

The function func distance(from start: String.Index, to end: String.Index) -> String.IndexDistance returns an IndexDistance which is just a typealias for Int

Eric33187
  • 1,006
  • 7
  • 13
  • 1
    Thank you! Best answer! please replace "number" with "myString" – Hassan Taleb Feb 28 '21 at 13:48
  • Fixed! Forgot to change that when converting. – Eric33187 Mar 02 '21 at 06:17
  • How would you do this with `lastIndex(of: "")`, which also only returns that weird `String.Index`? – Neph Mar 02 '21 at 15:28
  • @Neph It would literally be the exact same thing except you use `lastIndex(of: )` on line 3 in place of `firstIndex(of: )` – Eric33187 Mar 03 '21 at 05:25
  • @Eric33187 Thanks for your reply! Tbh, I'm still not sure what this actually does but it works (don't forget to also count the space while double-checking the result...)! No idea why Apple doesn't just do it that way by default! – Neph Mar 03 '21 at 14:22
  • 1
    @Neph All it does is gets the first index (or last in your case) of the letter and stores it in the constant `i`. Then it calculates the distance from the string's starting index to `i`. Those are all built in methods that I used which Swift provides. – Eric33187 Mar 10 '21 at 02:57
  • Naming the distance `index` is misleading. You can NOT use it to subscript a string. BTW Swift is a type inferred language. No need to explicitly set the resulting type. – Leo Dabus Sep 13 '21 at 15:00
  • 1
    What if you're looking for "Wo" ? – Dan Selig May 16 '22 at 18:40
9

Swift 4

var str = "abcdefg"
let index = str.index(of: "c")?.encodedOffset // Result: 2

Note: If String contains same multiple characters, it will just get the nearest one from left

var str = "abcdefgc"
let index = str.index(of: "c")?.encodedOffset // Result: 2
  • 9
    Don't use encodedOffset. **encodedOffset is deprecated: encodedOffset has been deprecated as most common usage is incorrect.** try `"".index(of: "")?.encodedOffset // 16` – Leo Dabus May 07 '19 at 01:55
  • @LeoDabus you are correctly stating it is deprecated. But you are suggesting using it as an answer anyways. The correct answer is: index_Of_YourStringVariable.utf16Offset(in: yourStringVariable) – TDesign Jun 17 '20 at 01:37
  • No. UTF16 offset it is probably not what you want – Leo Dabus Jun 17 '20 at 01:51
4

encodedOffset has deprecated from Swift 4.2.

Deprecation message: encodedOffset has been deprecated as most common usage is incorrect. Use utf16Offset(in:) to achieve the same behavior.

So we can use utf16Offset(in:) like this:

var str = "abcdefgc"
let index = str.index(of: "c")?.utf16Offset(in: str) // Result: 2
Narongdej Sarnsuwan
  • 5,095
  • 1
  • 12
  • 10
Shohin
  • 519
  • 8
  • 11
3

When searching for index like this

⛔️ guard let index = (positions.firstIndex { position <= $0 }) else {

it is treated as Array.Index. You have to give compiler a clue you want an integer

✅ guard let index: Int = (positions.firstIndex { position <= $0 }) else {
milczi
  • 7,064
  • 2
  • 27
  • 22
2

Swift 5

You can do convert to array of characters and then use advanced(by:) to convert to integer.

let myString = "Hello World"

if let i = Array(myString).firstIndex(of: "o") {
  let index: Int = i.advanced(by: 0)
  print(index) // Prints 4
}
Nick McConnell
  • 821
  • 8
  • 28
0

To perform string operation based on index , you can not do it with traditional index numeric approach. because swift.index is retrieved by the indices function and it is not in the Int type. Even though String is an array of characters, still we can't read element by index.

This is frustrating.

So ,to create new substring of every even character of string , check below code.

let mystr = "abcdefghijklmnopqrstuvwxyz"
let mystrArray = Array(mystr)
let strLength = mystrArray.count
var resultStrArray : [Character] = []
var i = 0
while i < strLength {
    if i % 2 == 0 {
        resultStrArray.append(mystrArray[i])
      }
    i += 1
}
let resultString = String(resultStrArray)
print(resultString)

Output : acegikmoqsuwy

Thanks In advance

Shrikant Phadke
  • 358
  • 2
  • 11
  • this can be easily accomplished with filter `var counter = 0` `let result = mystr.filter { _ in` `defer { counter += 1 }` `return counter.isMultiple(of: 2)` `}` – Leo Dabus Jun 17 '20 at 02:10
  • if you prefer using the String.index `var index = mystr.startIndex` `let result = mystr.filter { _ in` `defer { mystr.formIndex(after: &index) }` `return mystr.distance(from: mystr.startIndex, to: index).isMultiple(of: 2)` `}` – Leo Dabus Jun 17 '20 at 02:13
0

Here is an extension that will let you access the bounds of a substring as Ints instead of String.Index values:

import Foundation

/// This extension is available at
/// https://gist.github.com/zackdotcomputer/9d83f4d48af7127cd0bea427b4d6d61b
extension StringProtocol {
    /// Access the range of the search string as integer indices
    /// in the rendered string.
    /// - NOTE: This is "unsafe" because it may not return what you expect if
    ///     your string contains single symbols formed from multiple scalars.
    /// - Returns: A `CountableRange<Int>` that will align with the Swift String.Index
    ///     from the result of the standard function range(of:).
    func countableRange<SearchType: StringProtocol>(
        of search: SearchType,
        options: String.CompareOptions = [],
        range: Range<String.Index>? = nil,
        locale: Locale? = nil
    ) -> CountableRange<Int>? {
        guard let trueRange = self.range(of: search, options: options, range: range, locale: locale) else {
            return nil
        }

        let intStart = self.distance(from: startIndex, to: trueRange.lowerBound)
        let intEnd = self.distance(from: trueRange.lowerBound, to: trueRange.upperBound) + intStart

        return Range(uncheckedBounds: (lower: intStart, upper: intEnd))
    }
}

Just be aware that this can lead to weirdness, which is why Apple has chosen to make it hard. (Though that's a debatable design decision - hiding a dangerous thing by just making it hard...)

You can read more in the String documentation from Apple, but the tldr is that it stems from the fact that these "indices" are actually implementation-specific. They represent the indices into the string after it has been rendered by the OS, and so can shift from OS-to-OS depending on what version of the Unicode spec is being used. This means that accessing values by index is no longer a constant-time operation, because the UTF spec has to be run over the data to determine the right place in the string. These indices will also not line up with the values generated by NSString, if you bridge to it, or with the indices into the underlying UTF scalars. Caveat developer.

Zack
  • 941
  • 9
  • 14
  • no need to measure the distance form the startIndex again. Just get the distance from the lower to the upper and add the start. – Leo Dabus Aug 30 '20 at 17:43
  • I would also add the options to your method. Something like `func rangeInt(of aString: S, options: String.CompareOptions = []) -> Range? {` `guard let range = range(of: aString, options: options) else { return nil }` `let start = distance(from: startIndex, to: range.lowerBound)` `return start.. – Leo Dabus Aug 30 '20 at 17:45
  • I would probably change the method name to `countableRange` and return `CountableRange` – Leo Dabus Aug 30 '20 at 17:59
  • Good suggestions @LeoDabus all across the board. I added all the range(of...) arguments, actually, so you can now call countableRange(of:, options:, range:,locale:). Have updated the Gist, will update this post too. – Zack Sep 05 '20 at 03:48
  • I don't thing range is needed considering that you can call that method on a substring – Leo Dabus Sep 05 '20 at 03:54
  • I agree that it's possible to get the functionality without using that argument, but I still think it's better to match the Apple-provided API as closely as I can there. – Zack Sep 11 '20 at 20:36
-1

In case you got an "index is out of bounds" error. You may try this approach. Working in Swift 5

extension String{

   func countIndex(_ char:Character) -> Int{
        var count = 0
        var temp = self
  
        for c in self{
            
            if c == char {
               
                //temp.remove(at: temp.index(temp.startIndex,offsetBy:count))
                //temp.insert(".", at: temp.index(temp.startIndex,offsetBy: count))
                
                return count

            }
            count += 1
         }
        return -1
    }
}
William Tong
  • 479
  • 7
  • 14