How to check last 3 character in string contain numbers in Swift? - swift

For a given String instance, I want to check whether the last three characters are numeric characters (0, 1, 2, ..., 9) or not.
For example, the string
let str1 = "SACH092"
should return true for such a query, whereas e.g.
let str2 = "SACHA92"
should return false for the query.
I am using Xcode 7.3.1.

(As pointed out by #NiravD, for pre Swift 3, use where to join parts of multi-clause conditions. For Swift 3, parts of multi-clause conditions are simply joined by ,. For both methods below, both Swift 2.2 and 3 versions are included)
Use pattern matching for numeric characters "0"..."9"
Swift 2.2
extension String {
var lastThreeLettersAreNumbers: Bool {
if case let chars = characters.suffix(3) where chars.count > 2 {
let numbersPattern = Character("0")..."9"
return chars.reduce(true) { $0 && (numbersPattern ~= $1) }
}
return false
}
}
Swift 3
extension String {
var lastThreeLettersAreNumbers: Bool {
if case let chars = characters.suffix(3), chars.count > 2 {
let numbersPattern = Character("0")..."9"
return chars.reduce(true) { $0 && (numbersPattern ~= $1) }
}
return false
}
}
/* example usage, common for both Swift 2.2/3 version */
let str1 = "SACH092"
let str2 = "SACH0B2"
print(str1.lastThreeLettersAreNumbers) // true
print(str2.lastThreeLettersAreNumbers) // false
Make use of nil-return Int by String initializer, with flatMap
You can make use of the fact that the Int by String initializer returns nil for strings that cannot be represented as integers.
Swift 2.2
extension String {
var lastThreeLettersAreNumbers: Bool {
if case let chars = characters.suffix(3) where chars.count > 2 {
return chars.flatMap{Int(String($0))}.count == 3
}
return false
}
}
Swift 3
extension String {
var lastThreeLettersAreNumbers: Bool {
if case let chars = characters.suffix(3), chars.count > 2 {
return chars.flatMap{Int(String($0))}.count == 3
}
return false
}
}
/* example usage, common for both Swift 2.2/3 version */
let str1 = "SACH092"
let str2 = "SACH0B2"
print(str1.lastThreeLettersAreNumbers) // true
print(str2.lastThreeLettersAreNumbers) // false

For getting last 3 characters,
let exampleString = "SACH092"
let last3Char = exampleString.substringFromIndex(exampleString.endIndex.advancedBy(-3))
Check if last3Char contains all Digits,
let badCharacters = NSCharacterSet.decimalDigitCharacterSet().invertedSet
if last3Char.rangeOfCharacterFromSet(badCharacters) == nil {
print("String contains all digits")
} else {
print("String contains non-digit characters")
}

The best way to achieve string control is to use Regular Expressions.
For you :
var str = "SACH092"
let pattern = "^.*[0-9]{3,3}$"
let regexp = try! NSRegularExpression(pattern: pattern, options: [])
let matches = regexp.matches(in: str, options: [], range: NSMakeRange(0, str.characters.count))
print("End with 3 numbers : \(matches.count > 0)")

You can use like this
let s : NSString = "SACH092"
let trimmedString: String = (s as NSString).substringFromIndex(max(s.length-3,0))
print(trimmedString.isNumeric) // return true or false
//Make Extension of String
extension String {
var isNumeric: Bool {
let nums: Set<Character> = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
return Set(self.characters).isSubsetOf(nums)
}
}

To get the last three letters of a string
let oldString = "yourString"
let newString = a.substringFromIndex(a.endIndex.advancedBy(-3))
To check those characters are numbers,
func isNumber(num: String) -> Bool {
let numberCharacters = NSCharacterSet.decimalDigitCharacterSet().invertedSet
return !num.isEmpty && num.rangeOfCharacterFromSet(numberCharacters) == nil
}

Related

How to remove duplicate characters from a string in Swift

ruby has the function string.squeeze, but I can't seem to find a swift equivalent.
For example I want to turn bookkeeper -> bokepr
Is my only option to create a set of the characters and then pull the characters from the set back to a string?
Is there a better way to do this?
Edit/update: Swift 4.2 or later
You can use a set to filter your duplicated characters:
let str = "bookkeeper"
var set = Set<Character>()
let squeezed = str.filter{ set.insert($0).inserted }
print(squeezed) // "bokepr"
Or as an extension on RangeReplaceableCollection which will also extend String and Substrings as well:
extension RangeReplaceableCollection where Element: Hashable {
var squeezed: Self {
var set = Set<Element>()
return filter{ set.insert($0).inserted }
}
}
let str = "bookkeeper"
print(str.squeezed) // "bokepr"
print(str[...].squeezed) // "bokepr"
I would use this piece of code from another answer of mine, which removes all duplicates of a sequence (keeping only the first occurrence of each), while maintaining order.
extension Sequence where Iterator.Element: Hashable {
func unique() -> [Iterator.Element] {
var alreadyAdded = Set<Iterator.Element>()
return self.filter { alreadyAdded.insert($0).inserted }
}
}
I would then wrap it with some logic which turns a String into a sequence (by getting its characters), unqiue's it, and then restores that result back into a string:
extension String {
func uniqueCharacters() -> String {
return String(self.characters.unique())
}
}
print("bookkeeper".uniqueCharacters()) // => "bokepr"
Here is a solution I found online, however I don't think it is optimal.
func removeDuplicateLetters(_ s: String) -> String {
if s.characters.count == 0 {
return ""
}
let aNum = Int("a".unicodeScalars.filter{$0.isASCII}.map{$0.value}.first!)
let characters = Array(s.lowercased().characters)
var counts = [Int](repeatElement(0, count: 26))
var visited = [Bool](repeatElement(false, count: 26))
var stack = [Character]()
var i = 0
for character in characters {
if let num = asciiValueOfCharacter(character) {
counts[num - aNum] += 1
}
}
for character in characters {
if let num = asciiValueOfCharacter(character) {
i = num - aNum
counts[i] -= 1
if visited[i] {
continue
}
while !stack.isEmpty, let peekNum = asciiValueOfCharacter(stack.last!), num < peekNum && counts[peekNum - aNum] != 0 {
visited[peekNum - aNum] = false
stack.removeLast()
}
stack.append(character)
visited[i] = true
}
}
return String(stack)
}
func asciiValueOfCharacter(_ character: Character) -> Int? {
let value = String(character).unicodeScalars.filter{$0.isASCII}.first?.value ?? 0
return Int(value)
}
Here is one way to do this using reduce(),
let newChar = str.characters.reduce("") { partial, char in
guard let _ = partial.range(of: String(char)) else {
return partial.appending(String(char))
}
return partial
}
As suggested by Leo, here is a bit shorter version of the same approach,
let newChar = str.characters.reduce("") { $0.range(of: String($1)) == nil ? $0.appending(String($1)) : $0 }
Just Another solution
let str = "Bookeeper"
let newChar = str.reduce("" , {
if $0.contains($1) {
return "\($0)"
} else {
return "\($0)\($1)"
}
})
print(str.replacingOccurrences(of: " ", with: ""))
Use filter and contains to remove duplicate values
let str = "bookkeeper"
let result = str.filter{!result.contains($0)}
print(result) //bokepr

Converting string to Bool and testing for IF statement in Swift

So I am trying to take thee first index of a string (fullName) and test it to see if it matches all vowels lower and uppercased... for some reason when I use the .startIndex I test one letter at a time with a If statement.
Ia there a way to test all vowels at once ()I tried || in between each and it gave me the error "cannot convert string to Bool". Appreciate any help.
func lyricsForName(lyricsTemplate: String, fullName: String) -> String {
let shortName = shortNameFromName(name: fullName)
let index = fullName[fullName.startIndex]
if index== ("a","A"){
let lyrics = lyricsTemplate
.replacingOccurrences(of:"<FULL_NAME>", with: fullName)
.replacingOccurrences (of:"<SHORT_NAME>", with: fullName )
return lyrics
}else{
let lyrics = lyricsTemplate
.replacingOccurrences(of:"<FULL_NAME>", with: fullName)
.replacingOccurrences (of:"<SHORT_NAME>", with: shortName )
return lyrics
You can do it as previously answered, but this is a more Swifty way using the available APIs:
extension String {
var firstThreeLettersAreVowels: Bool {
guard characters.count >= 3 else {
return false
}
let firstThreeLetters = substring(to: index(startIndex, offsetBy: 3))
let isAllVowels = CharacterSet(charactersIn: firstThreeLetters).isSubset(of: CharacterSet.vowels)
return isAllVowels
}
var isLower: Bool {
return CharacterSet(charactersIn: self).isSubset(of: CharacterSet.lowercaseLetters)
}
var isUpper: Bool {
return CharacterSet(charactersIn: self).isDisjoint(with: CharacterSet.lowercaseLetters)
}
}
extension CharacterSet {
static var vowels: CharacterSet {
return CharacterSet(charactersIn: "AEIOUYaeiouy")
}
}
Or the one liner (without length check) is
extension String {
var firstThreeLettersAreVowels: Bool {
return CharacterSet(charactersIn: substring(to: index(startIndex, offsetBy: 3)))
.isSubset(of: CharacterSet(charactersIn: "AEIOUYaeiouy"))
}
}
This allows you rely on existing APIs rather than trying to do the work yourself, and it should be pretty fast.
Here is one way to do it. You can use an array of vowels and then test if the array contains the first character:
let fullName = "Albert"
let vowels = "aeiouAEIOU".characters
if let first = fullName.characters.first, vowels.contains(first) {
print("\(fullName) starts with a vowel")
}
Albert starts with a vowel
Note: Using fullName.characters.first is safer than fullName[fullName.startIndex] because the latter will crash for an empty String.
all kudos please to PEEJWEEJ answer
the characterset was his idea :)
i made it only more general
import Foundation
extension String {
func isFirstLetters(count: Int, of characterSet: CharacterSet) -> Bool {
guard characters.count >= count else {
return false
}
let firstLetters = substring(to: index(startIndex, offsetBy: count))
let isInSubset = CharacterSet(charactersIn: firstLetters).isSubset(of: characterSet
)
return isInSubset
}
}
extension CharacterSet {
static var vowels: CharacterSet {
return CharacterSet(charactersIn: "AEIOUYaeiouy")
}
}
some Test cases:
"abcDEf".isFirstLetters(count: 3, of: .vowels) // false
"aioDEf".isFirstLetters(count: 3, of: .vowels) // true
"abcDEf".isFirstLetters(count: 1, of: .vowels) // true
"bbcDEf".isFirstLetters(count: 1, of: .vowels) // false
"ibcDEf".isFirstLetters(count: 1, of: .vowels) // true
"ABcdef".isFirstLetters(count: 3, of: .uppercaseLetters) // false
"ABDdef".isFirstLetters(count: 3, of: .uppercaseLetters) // true
"abBABcdef".isFirstLetters(count: 3, of: .lowercaseLetters) // false
"abbABDdef".isFirstLetters(count: 3, of: .lowercaseLetters) // true
"ABD".isFirstLetters(count: 3, of: CharacterSet(charactersIn: "A"..."C")) // false
"ABD".isFirstLetters(count: 3, of: CharacterSet(charactersIn: "A"..."D")) // true
debug info:
extension CharacterSet {
var characters: [Character] {
var result: [Character] = []
for plane: UInt8 in 0...16 where self.hasMember(inPlane: plane) {
for unicode in UInt32(plane) << 16 ..< UInt32(plane + 1) << 16 {
if let uniChar = UnicodeScalar(unicode), self.contains(uniChar) {
result.append(Character(uniChar))
}
}
}
return result
}
}
print(CharacterSet.uppercaseLetters.description)
//<Foundation._SwiftNSCharacterSet: 0x6000000271a0>
print(String(CharacterSet.vowels.characters))
//AEIOUYaeiouy
print(CharacterSet.vowels.characters)
// ["A", "E", "I", "O", "U", "Y", "a", "e", "i", "o", "u", "y"]

Split string by components and keep components in place

Unlike string.components(separatedBy: ...) I want to keep the separators in place in the resulting array. Code is more explanatory
let input = "foo&bar|hello"
let output = string.tokenize(splitMarks: ["&", "|"])
let desiredResult = ["foo", "&", "bar", "|", "hello"]
Is there any function in the standard library which does this? If not how can I implement such a function?
For that you need to loop through the String and check its each characters that is it tokens or not. You can make extension of String for that like this.
extension String {
func stringTokens(splitMarks: Set<String>) -> [String] {
var string = ""
var desiredOutput = [String]()
for ch in self.characters {
if splitMarks.contains(String(ch)) {
if !string.isEmpty {
desiredOutput.append(string)
}
desiredOutput.append(String(ch))
string = ""
}
else {
string += String(ch)
}
}
if !string.isEmpty {
desiredOutput.append(string)
}
return desiredOutput
}
}
Now you can call this function like this way.
let input = "foo&bar|hello"
print(input.stringTokens(splitMarks: ["&", "|"]))
Output
["foo", "&", "bar", "|", "hello"]
You can use rangeOfCharacter(from: CharacterSet, ...) in a loop to
find the next occurrence of a split mark in the string, and then
append both the preceding part and the separator to an array:
extension String {
func tokenize(splitMarks: String) -> [Substring] {
let cs = CharacterSet(charactersIn: splitMarks)
var result = [Substring]()
var pos = startIndex
while let range = rangeOfCharacter(from: cs, range: pos..<endIndex) {
// Append string preceding the split mark:
if range.lowerBound != pos {
result.append(self[pos..<range.lowerBound])
}
// Append split mark:
result.append(self[range])
// Update position for next search:
pos = range.upperBound
}
// Append string following the last split mark:
if pos != endIndex {
result.append(self[pos..<endIndex])
}
return result
}
}
Example:
let input = "foo&bar|hello"
let output = input.tokenize(splitMarks: "&|")
print(output)
// ["foo", "&", "bar", "|", "hello"]

Reverse Strings without using predefined functions

Pardon me as I am a newbie on this language.
Edit: Is there a way to reverse the position of a array element?
I am trying to create a function that test the given input if its a palindrome or not. I'm trying to avoid using functions using reversed()
let word = ["T","E","S","T"]
var temp = [String]()
let index_count = 3
for words in word{
var text:String = words
print(text)
temp.insert(text, atIndex:index_count)
index_count = index_count - 1
}
Your approach can be used to reverse an array. But you have to
insert each element of the original array at the start position
of the destination array (moving the other elements to the end):
// Swift 2.2:
let word = ["T", "E", "S", "T"]
var reversed = [String]()
for char in word {
reversed.insert(char, atIndex: 0)
}
print(reversed) // ["T", "S", "E", "T"]
// Swift 3:
let word = ["T", "E", "S", "T"]
var reversed = [String]()
for char in word {
reversed.insert(char, at: 0)
}
print(reversed) // ["T", "S", "E", "T"]
The same can be done on the characters of a string directly:
// Swift 2.2:
let word = "TEST"
var reversed = ""
for char in word.characters {
reversed.insert(char, atIndex: reversed.startIndex)
}
print(reversed) // "TSET"
// Swift 3:
let word = "TEST"
var reversed = ""
for char in word.characters {
reversed.insert(char, at: reversed.startIndex)
}
print(reversed)
Swift 5
extension String {
func invert() -> String {
var word = [Character]()
for char in self {
word.insert(char, at: 0)
}
return String(word)
}
}
var anadrome = "god"
anadrome.invert()
// "dog"
Here's my solution:
extension String {
func customReverse() -> String {
var chars = Array(self)
let count = chars.count
for i in 0 ..< count/2 {
chars.swapAt(i, count - 1 - i)
}
return String(chars)
}
}
let input = "abcdef"
let output = input.customReverse()
print(output)
You can try it here.
func reverse(_ str: String) -> String {
let arr = Array(str) // turn the string into an array of all of the letters
let reversed = ""
for char in arr {
reversed.insert(char, at: reversed.startIndex)
}
return reversed
}
To use it:
let word = "hola"
let wordReversed = reverse(word)
print(wordReversed) // prints aloh
Another solution for reversing:
var original : String = "Test"
var reversed : String = ""
var c = original.characters
for _ in 0..<c.count{
reversed.append(c.popLast()!)
}
It simply appends each element of the old string that is popped, starting at the last element and working towards the first
Solution 1
let word = "aabbaa"
let chars = word.characters
let half = chars.count / 2
let leftSide = Array(chars)[0..<half]
let rightSide = Array(chars.reverse())[0..<half]
let palindrome = leftSide == rightSide
Solution 2
var palindrome = true
let chars = Array(word.characters)
for i in 0 ..< (chars.count / 2) {
if chars[i] != chars[chars.count - 1 - i] {
palindrome = false
break
}
}
print(palindrome)
static func reverseString(str : String) {
var data = Array(str)
var i = 0// initial
var j = data.count // final
//either j or i for while , data.count/2 buz only half we need check
while j != data.count/2 {
print("befor i:\(i) j:\(j)" )
j = j-1
data.swapAt(i, j) // swapAt API avalible only for array in swift
i = i+1
}
print(String(data))
}
//Reverse String
let str = "Hello World"
var reverseStr = ""
for char in Array(str) {
print(char)
reverseStr.insert(char, at: str.startIndex)
}
print(reverseStr)

Find the Range of the Nth word in a String

What I want is something like
"word1 word2 word3".rangeOfWord(2) => 6 to 10
The result could come as a Range or a tuple or whatever.
I'd rather not do the brute force of iterating over the characters and using a state machine. Why reinvent the lexer? Is there a better way?
In your example, your words are unique, and you can use the following method:
let myString = "word1 word2 word3"
let wordNum = 2
let myRange = myString.rangeOfString(myString.componentsSeparatedByString(" ")[wordNum-1])
// 6..<11
As pointed out by Andrew Duncan in the comments below, the above is only valid if your words are unique. If you have non-unique words, you can use this somewhat less neater method:
let myString = "word1 word2 word3 word2 word1 word3 word1"
let wordNum = 7 // 2nd instance (out of 3) of "word1"
let arr = myString.componentsSeparatedByString(" ")
var fromIndex = arr[0..<wordNum-1].map { $0.characters.count }.reduce(0, combine: +) + wordNum - 1
let myRange = Range<String.Index>(start: myString.startIndex.advancedBy(fromIndex), end: myString.startIndex.advancedBy(fromIndex+arr[wordNum-1].characters.count))
let myWord = myString.substringWithRange(myRange)
// string "word1" (from range 36..<41)
Finally, lets use the latter to construct an extension of String as you have wished for in your question example:
extension String {
private func rangeOfNthWord(wordNum: Int, wordSeparator: String) -> Range<String.Index>? {
let arr = myString.componentsSeparatedByString(wordSeparator)
if arr.count < wordNum {
return nil
}
else {
let fromIndex = arr[0..<wordNum-1].map { $0.characters.count }.reduce(0, combine: +) + (wordNum - 1)*wordSeparator.characters.count
return Range<String.Index>(start: myString.startIndex.advancedBy(fromIndex), end: myString.startIndex.advancedBy(fromIndex+arr[wordNum-1].characters.count))
}
}
}
let myString = "word1 word2 word3 word2 word1 word3 word1"
let wordNum = 7 // 2nd instance (out of 3) of "word1"
if let myRange = myString.rangeOfNthWord(wordNum, wordSeparator: " ") {
// myRange: 36..<41
print(myString.substringWithRange(myRange)) // prints "word1"
}
You can tweak the .rangeOfNthWord(...) method if word separation is not unique (say some words are separated by two blankspaces " ").
Also pointed out in the comments below, the use of .rangeOfString(...) is not, per se, pure Swift. It is, however, by no means bad practice. From Swift Language Guide - Strings and Characters:
Swift’s String type is bridged with Foundation’s NSString class. If
you are working with the Foundation framework in Cocoa, the entire
NSString API is available to call on any String value you create when
type cast to NSString, as described in AnyObject. You can also use a
String value with any API that requires an NSString instance.
See also the NSString class reference for rangeOfString method:
// Swift Declaration:
func rangeOfString(_ searchString: String) -> NSRange
I went ahead and wrote the state machine. (Grumble..) FWIW, here it is:
extension String {
private func halfOpenIntervalOfBlock(n:Int, separator sep:Character? = nil) -> (Int, Int)? {
enum State {
case InSeparator
case InPrecedingSeparator
case InWord
case InTarget
case Done
}
guard n > 0 else {
return nil
}
var state:State
if n == 1 {
state = .InPrecedingSeparator
} else {
state = .InSeparator
}
var separatorNum = 0
var startIndex:Int = 0
var endIndex:Int = 0
for (i, c) in self.characters.enumerate() {
let inSeparator:Bool
// A bit inefficient to keep doing this test.
if let s = sep {
inSeparator = c == s
} else {
inSeparator = c == " " || c == "\n"
}
endIndex = i
switch state {
case .InPrecedingSeparator:
if !inSeparator {
state = .InTarget
startIndex = i
}
case .InTarget:
if inSeparator {
state = .Done
}
case .InWord:
if inSeparator {
separatorNum += 1
if separatorNum == n - 1 {
state = .InPrecedingSeparator
} else {
state = .InSeparator
}
}
case .InSeparator:
if !inSeparator {
state = .InWord
}
case .Done:
break
}
if state == .Done {
break
}
}
if state == .Done {
return (startIndex, endIndex)
} else if state == .InTarget {
return (startIndex, endIndex + 1) // We ran off end.
} else {
return nil
}
}
func rangeOfWord(n:Int) -> Range<Index>? {
guard let (s, e) = self.halfOpenIntervalOfBlock(n) else {
return nil
}
let ss = self.startIndex.advancedBy(s)
let ee = self.startIndex.advancedBy(e)
return Range(start:ss, end:ee)
}
}
It's not really clear whether the string has to be considered divided in words by separators it may contains, or if you're just looking for a specific substring occurrence.
Anyway both cases could be addressed in this way in my opinion:
extension String {
func enumerateOccurencies(of pattern: String, _ body: (Range<String.Index>, inout Bool) throws -> Void) rethrows {
guard
!pattern.isEmpty,
count >= pattern.count
else { return }
var stop = false
var lo = startIndex
while !stop && lo < endIndex {
guard
let r = self[lo..<endIndex].range(of: pattern)
else { break }
try body(r, &stop)
lo = r.upperBound
}
}
}
You'll then set stop to true in the body closure once reached the desired occurrence number and capture the range passed to it:
let words = "word1, word1, word2, word3, word1, word3"
var matches = 0
var rangeOfThirdOccurencyOfWord1: Range<String.Index>? = nil
words.enumerateOccurencies(of: "word1") { range, stop in
matches +=1
stop = matches == 3
if stop {
rangeOfThirdOccurencyOfWord1 = range
}
}
Regarding the DFA: recently I've wrote one leveraging on Hashable and using a an Array of Dictionaries as its state nodes, but I've found that the method above is faster, cause maybe range(of:) uses finger-printing.
UPDATE
Otherwise you could also achieve that API you've mentioned in this way:
import Foundation
extension String {
func rangeOfWord(order: Int, separator: String) -> Range<String.Index>? {
precondition(order > 0)
guard
!isEmpty,
!separator.isEmpty,
separator.count < count
else { return nil }
var wordsSoFar = 0
var lo = startIndex
while let r = self[lo..<endIndex].range(of: separator) {
guard
r.lowerBound != lo
else {
lo = r.upperBound
continue
}
wordsSoFar += 1
guard
wordsSoFar < order
else { return lo..<r.lowerBound }
lo = r.upperBound
}
if
lo < endIndex,
wordsSoFar + 1 == order
{
return lo..<endIndex
}
return nil
}
}
let words = "word anotherWord oneMore lastOne"
if let r = words.rangeOfWord(order: 4, separator: " ") {
print(words[r])
} else {
print("not found")
}
Here order parameter refers to the nth order of the word in the string, starting from 1. I've also added the separator parameter to specify a string token to use for finding words in the string (it can also be defaulted to " " to be able to call the function without having to specify it).
Here's my attempt at an updated answer in Swift 5.5:
import Foundation
extension String {
func rangeOfWord(atPosition wordAt: Int) -> Range<String.Index>? {
let fullrange = self.startIndex..<self.endIndex
var count = 0
var foundAt: Range<String.Index>? = nil
self.enumerateSubstrings(in: fullrange, options: .byWords) { _, substringRange, _, stop in
count += 1
if count == wordAt {
foundAt = substringRange
stop = true // Stop the enumeration after the word range is found.
}
}
return foundAt
}
}
let lorem = "Morbi leo risus, porta ac consectetur ac, vestibulum at eros."
if let found = lorem.rangeOfWord(atPosition: 8) {
print("found: \(lorem[found])")
} else {
print("not found.")
}
This solution doesn't make a new array to contain the words so uses less memory (I have not tested but in theory it should use less memory). As much as possible, the build in method is used therefore less chance of bugs.
Swift 5 solution, which allows you to specify the word separator
extension String {
func rangeOfWord(atIndex wordIndex: Int) -> Range<String.Index>? {
let wordComponents = self.components(separatedBy: " ")
guard wordIndex < wordComponents.count else {
return nil
}
let characterEndCount = wordComponents[0...wordIndex].map { $0.count }.reduce(0, +)
let start = String.Index(utf16Offset: wordIndex + characterEndCount - wordComponents[wordIndex].count, in: self)
let end = String.Index(utf16Offset: wordIndex + characterEndCount, in: self)
return start..<end
}
}