Calculate range of string from word to end of string in swift
You should generate the resulting string first:
let string = String(format: "Due in %@ (%@) $%@.\nOverdue! Please pay now %@", "some date", "something", "15", "some date")
Then use .disTanceTo
to get the distance between indices;
if let range = string.rangeOfString("Overdue") {
let start = string.startIndex.distanceTo(range.startIndex)
let length = range.startIndex.distanceTo(string.endIndex)
let wordToEndRange = NSRange(location: start, length: length)
// This is the range you need
attributedText.addAttribute(NSForegroundColorAttributeName,
value: UIColor.blueColor(), range: wordToEndRange)
}
Please do note that NSRange
does not work correctly if the string contains Emojis or other Unicode characters so that the above solution may not work properly for that case.
Please look at the following SO answers for a better solution which cover that case as well:
- https://stackoverflow.com/a/27041376/793428
- https://stackoverflow.com/a/27880748/793428
How do you use String.substringWithRange? (or, how do Ranges work in Swift?)
You can use the substringWithRange method. It takes a start and end String.Index.
var str = "Hello, playground"
str.substringWithRange(Range<String.Index>(start: str.startIndex, end: str.endIndex)) //"Hello, playground"
To change the start and end index, use advancedBy(n).
var str = "Hello, playground"
str.substringWithRange(Range<String.Index>(start: str.startIndex.advancedBy(2), end: str.endIndex.advancedBy(-1))) //"llo, playgroun"
You can also still use the NSString method with NSRange, but you have to make sure you are using an NSString like this:
let myNSString = str as NSString
myNSString.substringWithRange(NSRange(location: 0, length: 3))
Note: as JanX2 mentioned, this second method is not safe with unicode strings.
How does String substring work in Swift
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.
NSRange from Swift Range?
Swift String
ranges and NSString
ranges are not "compatible".
For example, an emoji like counts as one Swift character, but as two NSString
characters (a so-called UTF-16 surrogate pair).
Therefore your suggested solution will produce unexpected results if the string
contains such characters. Example:
let text = "Long paragraph saying!"
let textRange = text.startIndex..<text.endIndex
let attributedString = NSMutableAttributedString(string: text)
text.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in
let start = distance(text.startIndex, substringRange.startIndex)
let length = distance(substringRange.startIndex, substringRange.endIndex)
let range = NSMakeRange(start, length)
if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: range)
}
})
println(attributedString)
Output:
Long paragra{
}ph say{
NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}ing!{
}
As you see, "ph say" has been marked with the attribute, not "saying".
Since NS(Mutable)AttributedString
ultimately requires an NSString
and an NSRange
, it is actually
better to convert the given string to NSString
first. Then the substringRange
is an NSRange
and you don't have to convert the ranges anymore:
let text = "Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: nsText)
nsText.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in
if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
}
})
println(attributedString)
Output:
Long paragraph {
}saying{
NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}!{
}
Update for Swift 2:
let text = "Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: text)
nsText.enumerateSubstringsInRange(textRange, options: .ByWords, usingBlock: {
(substring, substringRange, _, _) in
if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
}
})
print(attributedString)
Update for Swift 3:
let text = "Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: text)
nsText.enumerateSubstrings(in: textRange, options: .byWords, using: {
(substring, substringRange, _, _) in
if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.red, range: substringRange)
}
})
print(attributedString)
Update for Swift 4:
As of Swift 4 (Xcode 9), the Swift standard library
provides method to convert between Range<String.Index>
and NSRange
.
Converting to NSString
is no longer necessary:
let text = "Long paragraph saying!"
let attributedString = NSMutableAttributedString(string: text)
text.enumerateSubstrings(in: text.startIndex..<text.endIndex, options: .byWords) {
(substring, substringRange, _, _) in
if substring == "saying" {
attributedString.addAttribute(.foregroundColor, value: NSColor.red,
range: NSRange(substringRange, in: text))
}
}
print(attributedString)
Here substringRange
is a Range<String.Index>
, and that is converted to the
corresponding NSRange
with
NSRange(substringRange, in: text)
Extract Last Word in String with Swift
You can use String method enumerateSubstringsInRange. First parameter just pass your string Range<Index>
, and the option .byWords
. Just append each substring to the resulting collection and return it.
Swift 5 or later (for older Swift syntax check edit history)
import Foundation
extension StringProtocol { // for Swift 4 you need to add the constrain `where Index == String.Index`
var byWords: [SubSequence] {
var byWords: [SubSequence] = []
enumerateSubstrings(in: startIndex..., options: .byWords) { _, range, _, _ in
byWords.append(self[range])
}
return byWords
}
}
Usage:
let sentence = "Out of this world!!!"
let words = sentence.byWords // ["Out", "of", "this", "world"]
let firstWord = words.first // "Out"
let lastWord = words.last // world"
let first2Words = words.prefix(2) // ["Out", "of"]
let last2Words = words.suffix(2) // ["this", "world"]
Without import Foundation
Cleaning punctuation characters filtering the letters and spaces of the string
let clean = sentence.filter{ $0.isLetter || $0.isWhitespace }
find the index after the index of the last space in a string
if let lastIndex = clean.lastIndex(of: " "), let index = clean.index(lastIndex, offsetBy: 1, limitedBy: clean.index(before: clean.endIndex)) {
let lastWord = clean[index...]
print(lastWord) // "world"
}
find the index of the first space in a string
if let index = clean.firstIndex(of: " ") {
let firstWord = clean[...index]
print(firstWord) // "Out""
}
How to shift a string's Range?
This can get kinda tricky, depending on precisely what you need to do.
NSRange
Firstly, as you may have noticed, Range<String.Index>
and NSRange
are different.
Range<String.Index>
is how Swift represent ranges of indices in native Swift.String
s. It's an opaque type, that's only usable by the String APIs that consume it. It understands Swift strings as collections of Swift.Character
s, which represent what Unicode calls "extended grapheme clusters".
NSRange
is the older range representation, used by Objective C to represent ranges in Foundation.NSStrings
. It's an open container, containing a "start" location
and a length
. Importantly, these NSRange
and NSString
understand collections of utf16 encoded unicode scalars.
Because NSRange
and NSString
expose so many of their internals, they haven't undergone the same migration from utf16 to utf8 that Swift.String
underwent. A migration that most people probably didn't even notice, since Swift.String
guarded its implementation details much more than NSString
did.
NSRange
is more amenable to the kinds of simple operations you might be looking for. You can offset the start location
just like you describe. However, you need to be careful that the resulting range doesn't start/end in the middle of an extended grapheme cluster. In that case, slicing could lead to a substring with invalid unicode characters (for example, you might accidentally cut an e
away from its accent. the accent modifier isn't valid on its own without the e
.)
Bridging back and forth between NSRange
and Range<String.Index>
is possible, but can be error prone if you're not careful. For that reason, I suggest you try to minimize conversions, by trying to either exclusively use NSRange
, or Range<String.Index>
, but not mix the two too much.
replacingCharacters(in:with:)
I suspect you're only using this as example way of consuming wordInSentence
, but it's still worth noting that:
Foundation.NSString.replacingCharacters(in:with:)
](https://developer.apple.com/documentation/foundation/nsstring/1412937-replacingoccurrences) is an NSString
API that's imported onto Swift.String
when Foundation
is imported. It accept an NSString
. If you're dealing with Range<String.Index>
, you should use its Swift-native counterpart, Swift.String.replaceSubrange(_:with:)
.
Substring
is your friend
Don't fight it; unless you absolutely need sentence
to be a String
, keep it as a Substring
for the duration of these short-lived processing actions. Not only does this save you a copy of the string's contents, but it also makes it so that the indices can be shared between the slice and the parent string. This is valid:
let sentence = bigLongString[sentenceInString]
print(sentence[wordInString])
or even just: bigLongString[sentenceInString][wordInString]
or bigLongString[wordInString]
Shifting around
I couldn't find a native solution for this, so I rolled my own. I could definitely be missing something simpler, but here's what I came up with:
import Foundation
struct SubstringOffset {
let offset: String.IndexDistance
let parent: String
init(of substring: Substring, in parent: String) {
self.offset = parent.distance(from: parent.startIndex, to: substring.startIndex)
self.parent = parent
}
func convert(indexInParent: String.Index, toIndexIn newString: String) -> String.Index {
let distance = parent.distance(from: parent.startIndex, to: indexInParent)
let distanceInNewString = distance - offset
return newString.index(newString.startIndex, offsetBy: distanceInNewString)
}
func convert(rangeInParent: Range<String.Index>, toRangeIn newString: String) -> Range<String.Index> {
let newLowerBound = self.convert(indexInParent: rangeInParent.lowerBound, toIndexIn: newString)
let span = self.parent.distance(from: rangeInParent.lowerBound, to: rangeInParent.upperBound)
let newUpperBound = newString.index(newLowerBound, offsetBy: span)
return newLowerBound ..< newUpperBound
}
}
// The Setup (this is just to get easy testing values, no need for feedback on this part)
let bigLongString = "Really…? Thought I had it."
let sentenceInString = bigLongString.range(of: "Thought I had it.")!
let wordInString = bigLongString.range(of: "I")!
var sentence: String = String(bigLongString[sentenceInString])
let offset = SubstringOffset(of: bigLongString[sentenceInString], in: bigLongString)
// The Code In Question
let wordInSentence: Range<String.Index> = offset.convert(rangeInParent: wordInString, toRangeIn: sentence)
sentence.replaceSubrange(wordInSentence, with: "*\(sentence[wordInSentence])*")
print(sentence)
How to get substring with specific ranges in Swift 4?
You can search for substrings using range(of:)
.
import Foundation
let greeting = "Hello there world!"
if let endIndex = greeting.range(of: "world!")?.lowerBound {
print(greeting[..<endIndex])
}
outputs:
Hello there
EDIT:
If you want to separate out the words, there's a quick-and-dirty way and a good way. The quick-and-dirty way:
import Foundation
let greeting = "Hello there world!"
let words = greeting.split(separator: " ")
print(words[1])
And here's the thorough way, which will enumerate all the words in the string no matter how they're separated:
import Foundation
let greeting = "Hello there world!"
var words: [String] = []
greeting.enumerateSubstrings(in: greeting.startIndex..<greeting.endIndex, options: .byWords) { substring, _, _, _ in
if let substring = substring {
words.append(substring)
}
}
print(words[1])
EDIT 2: And if you're just trying to get the 7th through the 11th character, you can do this:
import Foundation
let greeting = "Hello there world!"
let startIndex = greeting.index(greeting.startIndex, offsetBy: 6)
let endIndex = greeting.index(startIndex, offsetBy: 5)
print(greeting[startIndex..<endIndex])
Swift 3.1 Get a range between two char?
It's much easier:
- The start index is the
upperBound
of the range of(
in the string The end index is the
lowerBound
of the range of)
in the stringlet aString = "piz(123)zazz"
if let openParenthesisRange = aString.range(of: "("),
let closeParenthesisRange = aString.range(of: ")", range: openParenthesisRange.upperBound..<aString.endIndex) {
let range = openParenthesisRange.upperBound..<closeParenthesisRange.lowerBound
let result = aString.substring(with: range)
print(result)
}
else {
print("Not found")
}
Alternatively regular expression, it's more code but it's much more versatile
let string = "piz(123)zazz"
let pattern = "\\((\\d+)\\)" // searches for 0 ore more digits between parentheses
do {
let regex = try NSRegularExpression(pattern: pattern)
if let match = regex.firstMatch(in: string, range: NSRange(location: 0, length: string.utf16.count)) {
let range = match.rangeAt(1)
let start = string.index(string.startIndex, offsetBy: range.location)
let end = string.index(start, offsetBy: range.length)
print(string.substring(with: start..<end))
} else {
print("Not Found")
}
} catch {
print("Regex Error:", error)
}
Swift Get string between 2 strings in a string
I'd use a regular expression to extract substrings from complex input like this.
Swift 3.1:
let test = "javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
if let match = test.range(of: "(?<=')[^']+", options: .regularExpression) {
print(test.substring(with: match))
}
// Prints: Info/99/something
Swift 2.0:
let test = "javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
if let match = test.rangeOfString("(?<=')[^']+", options: .RegularExpressionSearch) {
print(test.substringWithRange(match))
}
// Prints: Info/99/something
How do I find the last occurrence of a substring in a Swift string?
Cocoa frameworks should be accessible in Swift, but you need to import them. Try importing Foundation
to access the NSString API. From "Working with Cocoa Data Types–Strings" of the "Using Swift with Cocoa and Objective-C" guide:
Swift automatically bridges between the String type and the NSString class. [...] To enable string bridging, just import Foundation.
Additionally, NSBackwardsSearch
is an enum value (marked & imported as an option), so you have to use Swift's enum/option syntax to access it (as part of the NSStringCompareOptions
option type). Prefixes are stripped from C enumeration values, so drop the NS
from the value name.
Taken all together, we have:
import Foundation
"abc def ghi abc def ghi".rangeOfString("c", options:NSStringCompareOptions.BackwardsSearch)
Note that you might have to use the distance
and advance
functions to properly make use of the range from rangeOfString
.
Related Topics
Actions Assigned to Nsmenuitem Dont Seem to Work
List Is Not Conforming to Encodable
Secidentity + Force Cast Violation: Force Casts Should Be Avoided. (Force_Cast)
Swiftui Go Back Programmatically from Representable to View
Using a Timer in The Background Thread to Update UI
Cannot Use Mutating Member ... Because Append
Can't Override UItableviewdatasource and UItableviewdelegate
Why Does a Function Have Long-Term Write Access to All of Its In-Out Parameters
Swiftui Map Overlays Without UIviewrepresentable
Swift Bug? Calling Super Class Method When Subclass with Generic Type
Cropping Cgrect from Avcapturephotooutput (Resizeaspectfill)
Swift Auto Completion Not Working in Xcode 6 Beta
Way to Check If Up or Down Button Is Pressed with Nsstepper
Idiomatic Way to Test Swift Optionals
Swift Compile Error, Subclassing Nsvalue, Using Super.Init(Nonretainedobject:)
Swift Google Maps Smoothly Rounded Polylines
Swift Package: Ld: Warning: Dylib Was Built for Newer Macos Version (11.0) Than Being Linked (10.15)