Index of a Substring in a String With Swift

Index of a substring in a string with 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"]

How does String substring work in Swift

Sample Image

All of the following examples use

var str = "Hello, playground"

Swift 4

Strings got a pretty big overhaul in Swift 4. When you get some substring from a String now, you get a Substring type back rather than a String. Why is this? Strings are value types in Swift. That means if you use one String to make a new one, then it has to be copied over. This is good for stability (no one else is going to change it without your knowledge) but bad for efficiency.

A Substring, on the other hand, is a reference back to the original String from which it came. Here is an image from the documentation illustrating that.

No copying is needed so it is much more efficient to use. However, imagine you got a ten character Substring from a million character String. Because the Substring is referencing the String, the system would have to hold on to the entire String for as long as the Substring is around. Thus, whenever you are done manipulating your Substring, convert it to a String.

let myString = String(mySubstring)

This will copy just the substring over and the memory holding old String can be reclaimed. Substrings (as a type) are meant to be short lived.

Another big improvement in Swift 4 is that Strings are Collections (again). That means that whatever you can do to a Collection, you can do to a String (use subscripts, iterate over the characters, filter, etc).

The following examples show how to get a substring in Swift.

Getting substrings

You can get a substring from a string by using subscripts or a number of other methods (for example, prefix, suffix, split). You still need to use String.Index and not an Int index for the range, though. (See my other answer if you need help with that.)

Beginning of a string

You can use a subscript (note the Swift 4 one-sided range):

let index = str.index(str.startIndex, offsetBy: 5)
let mySubstring = str[..<index] // Hello

or prefix:

let index = str.index(str.startIndex, offsetBy: 5)
let mySubstring = str.prefix(upTo: index) // Hello

or even easier:

let mySubstring = str.prefix(5) // Hello

End of a string

Using subscripts:

let index = str.index(str.endIndex, offsetBy: -10)
let mySubstring = str[index...] // playground

or suffix:

let index = str.index(str.endIndex, offsetBy: -10)
let mySubstring = str.suffix(from: index) // playground

or even easier:

let mySubstring = str.suffix(10) // playground

Note that when using the suffix(from: index) I had to count back from the end by using -10. That is not necessary when just using suffix(x), which just takes the last x characters of a String.

Range in a string

Again we simply use subscripts here.

let start = str.index(str.startIndex, offsetBy: 7)
let end = str.index(str.endIndex, offsetBy: -6)
let range = start..<end

let mySubstring = str[range] // play

Converting Substring to String

Don't forget, when you are ready to save your substring, you should convert it to a String so that the old string's memory can be cleaned up.

let myString = String(mySubstring)

Using an Int index extension?

I'm hesitant to use an Int based index extension after reading the article Strings in Swift 3 by Airspeed Velocity and Ole Begemann. Although in Swift 4, Strings are collections, the Swift team purposely hasn't used Int indexes. It is still String.Index. This has to do with Swift Characters being composed of varying numbers of Unicode codepoints. The actual index has to be uniquely calculated for every string.

I have to say, I hope the Swift team finds a way to abstract away String.Index in the future. But until then, I am choosing to use their API. It helps me to remember that String manipulations are not just simple Int index lookups.

Swift: get index of start of a substring in a string

You can reach using the rangeOfString function that finds and returns the range of the first occurrence of a given string within a given range like in the following way:

var text = "Hello Victor Sigler"

if let indexOf = text.rangeOfString("Victor")?.startIndex {
println(indexOf) //6
}

I hope this help you.

Swift 4: return Substring index from a String

You can use a regex to match your string, assuming that all substrings end with either a ; or the end of string.

This is the regex you should use:

Substring #(\d+): (.+?)(?:;|$)

It captures the substring number into group 1 and the substring into group 2.

You can use it like this:

extension String {
func substring(withNSRange range: NSRange) -> String {
return String(string[Range(range, in: self)!])
}
}

let string = "Substring #1: Hello World!; Substring #2: My name is Tom"
let regex = try! NSRegularExpression(pattern: "Substring #(\\d+): (.+?)(?:;|$)", options: [])
let matches = regex.matches(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count))
let tuples = matches.map { (Int(string.substring(withNSRange: $0.range(at: 1))), string.substring(withNSRange: $0.range(at: 2))) }
let dict = Dictionary(uniqueKeysWithValues: tuples)

// dict will contain something like [1: Hello World!, 2: My name is Tom]

Edit:

Assuming that the custom end of substring is stored in a variable called customStringEnd, you can create the regex like this:

let regex = try! NSRegularExpression(pattern: "Substring #(\\d+): (.+?)(?:\(NSRegularExpression.escapedPattern(for: customStringEnd))|$)", options: [])

Finding index of character in Swift String

You are not the only one who couldn't find the solution.

String doesn't implement RandomAccessIndexType. Probably because they enable characters with different byte lengths. That's why we have to use string.characters.count (count or countElements in Swift 1.x) to get the number of characters. That also applies to positions. The _position is probably an index into the raw array of bytes and they don't want to expose that. The String.Index is meant to protect us from accessing bytes in the middle of characters.

That means that any index you get must be created from String.startIndex or String.endIndex (String.Index implements BidirectionalIndexType). Any other indices can be created using successor or predecessor methods.

Now to help us with indices, there is a set of methods (functions in Swift 1.x):

Swift 4.x

let text = "abc"
let index2 = text.index(text.startIndex, offsetBy: 2) //will call succ 2 times
let lastChar: Character = text[index2] //now we can index!

let characterIndex2 = text.index(text.startIndex, offsetBy: 2)
let lastChar2 = text[characterIndex2] //will do the same as above

let range: Range<String.Index> = text.range(of: "b")!
let index: Int = text.distance(from: text.startIndex, to: range.lowerBound)

Swift 3.0

let text = "abc"
let index2 = text.index(text.startIndex, offsetBy: 2) //will call succ 2 times
let lastChar: Character = text[index2] //now we can index!

let characterIndex2 = text.characters.index(text.characters.startIndex, offsetBy: 2)
let lastChar2 = text.characters[characterIndex2] //will do the same as above

let range: Range<String.Index> = text.range(of: "b")!
let index: Int = text.distance(from: text.startIndex, to: range.lowerBound)

Swift 2.x

let text = "abc"
let index2 = text.startIndex.advancedBy(2) //will call succ 2 times
let lastChar: Character = text[index2] //now we can index!
let lastChar2 = text.characters[index2] //will do the same as above

let range: Range<String.Index> = text.rangeOfString("b")!
let index: Int = text.startIndex.distanceTo(range.startIndex) //will call successor/predecessor several times until the indices match

Swift 1.x

let text = "abc"
let index2 = advance(text.startIndex, 2) //will call succ 2 times
let lastChar: Character = text[index2] //now we can index!

let range = text.rangeOfString("b")
let index: Int = distance(text.startIndex, range.startIndex) //will call succ/pred several times

Working with String.Index is cumbersome but using a wrapper to index by integers (see https://stackoverflow.com/a/25152652/669586) is dangerous because it hides the inefficiency of real indexing.

Note that Swift indexing implementation has the problem that indices/ranges created for one string cannot be reliably used for a different string, for example:

Swift 2.x

let text: String = "abc"
let text2: String = "br>
let range = text.rangeOfString("b")!

//can randomly return a bad substring or throw an exception
let substring: String = text2[range]

//the correct solution
let intIndex: Int = text.startIndex.distanceTo(range.startIndex)
let startIndex2 = text2.startIndex.advancedBy(intIndex)
let range2 = startIndex2...startIndex2

let substring: String = text2[range2]

Swift 1.x

let text: String = "abc"
let text2: String = "br>
let range = text.rangeOfString("b")

//can randomly return nil or a bad substring
let substring: String = text2[range]

//the correct solution
let intIndex: Int = distance(text.startIndex, range.startIndex)
let startIndex2 = advance(text2.startIndex, intIndex)
let range2 = startIndex2...startIndex2

let substring: String = text2[range2]

swift 3 get start index (as int) of substring

The distance(from:to:) method of String computes the difference
between two String.Index values:

let mystring = "hi this is my name"
if let range = mystring.range(of: "this") {
let startPos = mystring.distance(from: mystring.startIndex, to: range.lowerBound)
let endPos = mystring.distance(from: mystring.startIndex, to: range.upperBound)
print(startPos, endPos) // 3 7
}

Actually it just forwards the call to the string's CharacterView, so
the above gives the same result as

let mystring = "hi this is my name"
if let range = mystring.range(of: "this") {
let startPos = mystring.characters.distance(from: mystring.characters.startIndex, to: range.lowerBound)
let endPos = mystring.characters.distance(from: mystring.characters.startIndex, to: range.upperBound)
print(startPos, endPos) // 3 7
}

If you need all occurrences of the string:

let mystring = "this is this and that is that"
var searchPosition = mystring.startIndex
while let range = mystring.range(of: "this", range: searchPosition..<mystring.endIndex) {
let startPos = mystring.distance(from: mystring.startIndex, to: range.lowerBound)
let endPos = mystring.distance(from: mystring.startIndex, to: range.upperBound)
print(startPos, endPos)

searchPosition = range.upperBound
}

How to get index of a text in a string in swift 4?

You can use NSDataDetector which makes easy to detect URLs inside a string

let inputString = "Please review the work schedule for personnel associated with this device\nhttps://test.abcdxyz.com/media?q=kp97k73a9omm"

let urlDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let getMatches = urlDetector.matches(in: inputString, options: [], range: NSRange(location: 0, length: inputString.utf16.count))

for match in getMatches {
guard let range = Range(match.range, in: inputString) else { continue }
let url = inputString[range]
print(url)
}

More detail: https://www.hackingwithswift.com/example-code/strings/how-to-detect-a-url-in-a-string-using-nsdatadetector

How can I use String substring in Swift 4? 'substring(to:)' is deprecated: Please use String slicing subscript with a 'partial range from' operator

You should leave one side empty, hence the name "partial range".

let newStr = str[..<index]

The same stands for partial range from operators, just leave the other side empty:

let newStr = str[index...]

Keep in mind that these range operators return a Substring. If you want to convert it to a string, use String's initialization function:

let newStr = String(str[..<index])

You can read more about the new substrings here.

How to get a substring in swift 4?

String.Index is not an integer, and you cannot simply subtract
s.endIndex - 3, as "Collections move their index", see
A New Model for Collections and Indices on Swift evolution.

Care must be taken not to move the index not beyond the valid bounds.
Example:

let s = "aString"

if let upperBound = s.index(s.endIndex, offsetBy: -3, limitedBy: s.startIndex) {
let subS = String(s[..<upperBound])
} else {
print("too short")
}

Alternatively,

let upperBound = s.index(s.endIndex, offsetBy: -3, limitedBy: s.startIndex) ?? s.startIndex
let subS = String(s[..<upperBound])

which would print an empty string if s has less then 3 characters.

If you want the initial portion of a string then you can simply do

let subS = String(s.dropLast(3))

or as a mutating method:

var s = "aString"
s.removeLast(min(s.count, 3))
print(s) // "aStr"


Related Topics



Leave a reply



Submit