Apply a number to each letter in text swift2 - numbers

I want to compare two entries in a UITextField giving a number to each letter and then compare the results of the addition of the letters from both fields.
Example:
a=1 b=2 c=3 d=4 e=5 f=6
textfield1= cae
textfield2= fca
Result is:
textfield1=9 and
textfield2=10

You could use the unicode scalar representation of each character (look up ASCII tables) and sum these shifted by -96 (such that a -> 1, b -> 2 and so on). In the following, upper case letters will generate the same number value as lower case ones.
let foo = "cae"
let pattern = UnicodeScalar("a")..."z"
let charsAsNumbers = foo.lowercaseString.unicodeScalars
.filter { pattern ~= $0 }
let sumOfNumbers = charsAsNumbers
.reduce(0) { $0 + $1.value - 96 }
print(sumOfNumbers) // 9
Or, to simplify usage, create a function or String extension
/* as a function */
func getNumberSum(foo: String) -> UInt32 {
let pattern = UnicodeScalar("a")..."z"
return foo.lowercaseString.unicodeScalars
.filter { pattern ~= $0 }
.reduce(0) { $0 + $1.value - 96 }
}
/* or an extension */
extension String {
var numberSum: UInt32 {
let pattern = UnicodeScalar("a")..."z"
return self.lowercaseString.unicodeScalars
.filter { pattern ~= $0 }
.reduce(0) { $0 + $1.value - 96 }
}
}
Example usage for your case:
/* example test case (using extension) */
let textField1 = UITextField()
let textField2 = UITextField()
textField1.text = "cAe"
textField2.text = "FCa"
/* example usage */
if let textFieldText1 = textField1.text,
let textFieldText2 = textField2.text {
print(textFieldText1.numberSum) // 9
print(textFieldText2.numberSum) // 10
print(textFieldText1.numberSum
== textFieldText2.numberSum) // false
}

Related

Find the repeated sequence in the line that go in a row

Given a string of arbitrary length. I need to find 1 subsequences of identical characters that go in a row.
My function (there are two of them, but these are two parts of the same function) turned out to be complex and cumbersome and did not fit because of this. The function I need should be simple and not too long.
Example:
Input : str = "abcabc"
Output : abc
Input : str = "aa"
Output : a
Input : str = "abcbabcb"
Output : abcb
Input : str = "abcbca"
Output : bcbc
Input : str = "cbabc"
Output :
Input : str = "acbabc"
Output :
My unsuccessful function:
func findRepetition(_ p: String) -> [String:Int] {
var repDict: [String:Int] = [:]
var p = p
while p.count != 0 {
for i in 0...p.count-1 {
repDict[String(Array(p)[0..<i]), default: 0] += 1
}
p = String(p.dropFirst())
}
return repDict
}
var correctWords = [String]()
var wrongWords = [String]()
func getRepeats(_ p: String) -> Bool {
let p = p
var a = findRepetition(p)
for i in a {
var substring = String(Array(repeating: i.key, count: 2).joined())
if p.contains(substring) {
wrongWords.append(p)
return false
}
}
correctWords.append(p)
return true
}
I will be very grateful for your help!
Here's a solution using regular expression. I used a capture group that tries to match as many characters as possible such that the whole group repeats at least once.
import Foundation
func findRepetition(_ s: String) -> String? {
if s.isEmpty { return nil }
let pattern = "([a-z]+)\\1+"
let regex = try? NSRegularExpression(pattern: pattern, options: [])
if let match = regex?.firstMatch(in: s, options: [], range:
NSRange(location: 0, length: s.utf16.count)) {
let unitRange = match.range(at: 1)
return (s as NSString).substring(with: unitRange)
}
return nil
}
print(findRepetition("abcabc")) //prints abc
print(findRepetition("aa")) //prints a
print(findRepetition("abcbabcb")) //prints abcb
print(findRepetition("abcbca")) //prints bc
print(findRepetition("cbabc")) //prints nil
print(findRepetition("acbabc")) //prints nil
func findRepetitions(_ p : String) -> [String: Int]{
let half = p.count / 2 + 1
var result : [String : Int] = [:]
for i in 1..<half {
for j in 0...(p.count-i) {
let sub = (p as! NSString).substring(with: NSRange.init(location: j, length: i))
if let val = result[sub] {
result[sub] = val + 1
}else {
result[sub] = 1
}
}
}
return result
}
This is for finding repetitions of possible substrings in your string. Hope it can help
Here is a solution that is based on the Suffix Array Algorithm, that finds the longest substring that is repeated (contiguously):
func longestRepeatedSubstring(_ str: String) -> String {
let sortedSuffixIndices = str.indices.sorted { str[$0...] < str[$1...] }
let lcsArray = [0]
+
sortedSuffixIndices.indices.dropFirst().map { index in
let suffix1 = str[sortedSuffixIndices[index]...]
let suffix2 = str[sortedSuffixIndices[index - 1]...]
let commonPrefix = suffix1.commonPrefix(with: suffix2)
let count = commonPrefix.count
let repeated = suffix1.dropFirst(count).commonPrefix(with: commonPrefix)
return count == repeated.count ? count : 0
}
let maxRepeated = zip(sortedSuffixIndices.indices,lcsArray).max(by: { $0.1 < $1.1 })
if let tuple = maxRepeated, tuple.1 != 0 {
let suffix1 = str[sortedSuffixIndices[tuple.0 - 1]...]
let suffix2 = str[sortedSuffixIndices[tuple.0]...]
let longestRepeatedSubstring = suffix1.commonPrefix(with: suffix2)
return longestRepeatedSubstring
} else {
return ""
}
}
Here is an easy to understand tutorial about such an algorithm.
It works for these examples:
longestRepeatedSubstring("abcabc") //"abc"
longestRepeatedSubstring("aa") //"a"
longestRepeatedSubstring("abcbabcb") //"abcd"
longestRepeatedSubstring("abcbca") //"bcbc"
longestRepeatedSubstring("cbabc") //""
longestRepeatedSubstring("acbabc") //""
As well as these:
longestRepeatedSubstring("a😍ca😍c") //"a😍c"
longestRepeatedSubstring("Ab cdAb cd") //"Ab cd"
longestRepeatedSubstring("aabcbc") //"bc"
Benchmarks
Here is a benchmark that clearly shows that the Suffix Array algorithm is much faster than using a regular expression.
The result is:
Regular expression: 7.2 ms
Suffix Array : 0.1 ms

Need to find consecutive sequence like "6789" or "abcd" in Swift

I need help to find consecutive sequence for example more than 3 characters in ascending order. I've already implemented one solution but It's not universal.
Examples what should be found - "1234", "abcd", "5678".
And what shouldn't be found - "123", "adced", "123abc", "89:;"
Particularly the case "89:;", symbol ":" - is 58 in uniCode and "9" - is 57, that's why my approach does not work in the case.
Implementation should be in swift.
Additional clarification
For now it would be enough to find the sequences only in English letters and numbers.
private func findSequence(sequenceLength: Int, in string: String) -> Bool {
let scalars = string.unicodeScalars
var unicodeArray: [Int] = scalars.map({ Int($0.value) })
var currentLength: Int = 1
var i = 0
for number in unicodeArray {
if i+1 >= unicodeArray.count {
break
}
let nextNumber = unicodeArray[i+1]
if number+1 == nextNumber {
currentLength += 1
} else {
currentLength = 1
}
if currentLength >= sequenceLength {
return true
}
i += 1
}
return false
}
var data = [1,2,5,4,56,6,7,9,6,5,4,5,1,2,5,4,56,6,7,9,8,1,1,2,5,4,56,6,7,9,8,1,1,2,5,4,56,6,7,9,8,1,1,2,5,4,56,6,7,9,8,1,1,2,5,4,56,6,7,9,8,11,2,5,4,56,6,7,9,8,1,2,3]
for i in 0...data.count{
if i+2 < data.count{
if Int(data[i] + data[i+2]) / 2 == data[i+1] && Int(data[i] + data[i+2]) % data[i+1] == 0 && data[i+1] != 1 && data[i] < data[i+1]{
print(data[i] ,data[i+1], data[i+2])
}
}
}
You can check for sequence with CharacterSet
func findSequence(sequenceLength: Int, in string: String) -> Bool {
// It would be better to extract this out of func
let digits = CharacterSet.decimalDigits
let lowercase = CharacterSet(charactersIn: "a"..."z")
let uppercase = CharacterSet(charactersIn: "A"..."Z")
let controlSet = digits.union(lowercase).union(uppercase)
// ---
let scalars = string.unicodeScalars
let unicodeArray = scalars.map({ $0 })
var currentLength: Int = 1
var i = 0
for number in unicodeArray where controlSet.contains(number) {
if i+1 >= unicodeArray.count {
break
}
let nextNumber = unicodeArray[i+1]
if UnicodeScalar(number.value+1) == nextNumber {
currentLength += 1
} else {
currentLength = 1
}
if currentLength >= sequenceLength {
return true
}
i += 1
}
return false
}
I did assumed that "a" ... "z" and "A"..."Z" are consecutive here, to make it in range, but it may be better do explicitly list all the symbols you want.
Or use CharacterSet.alphanumerics, but is not limited to basic latin alphabet.

In Swift, how to modify a character in string with subscript?

Like in C, we can simply do
str[i] = str[j]
But how to write the similar logic in swift?
Here is my code, but got error:
Cannot assign through subscript: subscript is get-only
let indexI = targetString.index(targetString.startIndex, offsetBy: i)
let indexJ = targetString.index(targetString.startIndex, offsetBy: j)
targetString[indexI] = targetString[indexJ]
I know it may work by using this method, but it's too inconvenient
replaceSubrange(, with: )
In C, a string (char *) can be treated as an array of characters. In Swift, you can convert the String to an [Character], do the modifications you want, and then convert the [Character] back to String.
For example:
let str = "hello"
var strchars = Array(str)
strchars[0] = strchars[4]
let str2 = String(strchars)
print(str2) // "oello"
This might seem like a lot of work for a single modification, but if you are moving many characters this way, you only have to convert once each direction.
Reverse a String
Here's an example of a simple algorithm to reverse a string. By converting to an array of characters first, this algorithm is similar to the way you might do it in C:
let str = "abcdefg"
var strchars = Array(str)
var start = 0
var end = strchars.count - 1
while start < end {
let temp = strchars[start]
strchars[start] = strchars[end]
strchars[end] = temp
start += 1
end -= 1
}
let str2 = String(strchars)
print(str2) // "gfedcba"
Dealing with String with Swift is major pain in the a**. Unlike most languages I know that treat string as an array of characters, Swift treats strings as collection of extended grapheme clusters and the APIs to access them is really clumsy. Changes are coming in Swift 4 but that manifesto lost me about 10 paragraphs in.
Back to your question... you can replace the character like this:
var targetString = "Hello world"
let i = 0
let j = 1
let indexI = targetString.index(targetString.startIndex, offsetBy: i)
let indexJ = targetString.index(targetString.startIndex, offsetBy: j)
targetString.replaceSubrange(indexI...indexI, with: targetString[indexJ...indexJ])
print(targetString) // eello world
I was quite shocked as well by the fact that swift makes string indexing so damn complicated. For that reason, I have built some string extensions that enable you to retrieve and change parts of strings based on indices, closed ranges, and open ranges, PartialRangeFrom, PartialRangeThrough, and PartialRangeUpTo. You can download the repository I created here
You can also pass in negative numbers in order to access characters from the end backwards.
public extension String {
/**
Enables passing in negative indices to access characters
starting from the end and going backwards.
if num is negative, then it is added to the
length of the string to retrieve the true index.
*/
func negativeIndex(_ num: Int) -> Int {
return num < 0 ? num + self.count : num
}
func strOpenRange(index i: Int) -> Range<String.Index> {
let j = negativeIndex(i)
return strOpenRange(j..<(j + 1), checkNegative: false)
}
func strOpenRange(
_ range: Range<Int>, checkNegative: Bool = true
) -> Range<String.Index> {
var lower = range.lowerBound
var upper = range.upperBound
if checkNegative {
lower = negativeIndex(lower)
upper = negativeIndex(upper)
}
let idx1 = index(self.startIndex, offsetBy: lower)
let idx2 = index(self.startIndex, offsetBy: upper)
return idx1..<idx2
}
func strClosedRange(
_ range: CountableClosedRange<Int>, checkNegative: Bool = true
) -> ClosedRange<String.Index> {
var lower = range.lowerBound
var upper = range.upperBound
if checkNegative {
lower = negativeIndex(lower)
upper = negativeIndex(upper)
}
let start = self.index(self.startIndex, offsetBy: lower)
let end = self.index(start, offsetBy: upper - lower)
return start...end
}
// MARK: - Subscripts
/**
Gets and sets a character at a given index.
Negative indices are added to the length so that
characters can be accessed from the end backwards
Usage: `string[n]`
*/
subscript(_ i: Int) -> String {
get {
return String(self[strOpenRange(index: i)])
}
set {
let range = strOpenRange(index: i)
replaceSubrange(range, with: newValue)
}
}
/**
Gets and sets characters in an open range.
Supports negative indexing.
Usage: `string[n..<n]`
*/
subscript(_ r: Range<Int>) -> String {
get {
return String(self[strOpenRange(r)])
}
set {
replaceSubrange(strOpenRange(r), with: newValue)
}
}
/**
Gets and sets characters in a closed range.
Supports negative indexing
Usage: `string[n...n]`
*/
subscript(_ r: CountableClosedRange<Int>) -> String {
get {
return String(self[strClosedRange(r)])
}
set {
replaceSubrange(strClosedRange(r), with: newValue)
}
}
/// `string[n...]`. See PartialRangeFrom
subscript(r: PartialRangeFrom<Int>) -> String {
get {
return String(self[strOpenRange(r.lowerBound..<self.count)])
}
set {
replaceSubrange(strOpenRange(r.lowerBound..<self.count), with: newValue)
}
}
/// `string[...n]`. See PartialRangeThrough
subscript(r: PartialRangeThrough<Int>) -> String {
get {
let upper = negativeIndex(r.upperBound)
return String(self[strClosedRange(0...upper, checkNegative: false)])
}
set {
let upper = negativeIndex(r.upperBound)
replaceSubrange(
strClosedRange(0...upper, checkNegative: false), with: newValue
)
}
}
/// `string[...<n]`. See PartialRangeUpTo
subscript(r: PartialRangeUpTo<Int>) -> String {
get {
let upper = negativeIndex(r.upperBound)
return String(self[strOpenRange(0..<upper, checkNegative: false)])
}
set {
let upper = negativeIndex(r.upperBound)
replaceSubrange(
strOpenRange(0..<upper, checkNegative: false), with: newValue
)
}
}
}
Usage:
let text = "012345"
print(text[2]) // "2"
print(text[-1] // "5"
print(text[1...3]) // "123"
print(text[2..<3]) // "2"
print(text[3...]) // "345"
print(text[...3]) // "0123"
print(text[..<3]) // "012"
print(text[(-3)...] // "345"
print(text[...(-2)] // "01234"
All of the above works with assignment as well. All subscripts have getters and setters.
a new extension added,
since String conforms to BidirectionalCollection Protocol
extension String{
subscript(at i: Int) -> String? {
get {
if i < count{
let idx = index(startIndex, offsetBy: i)
return String(self[idx])
}
else{
return nil
}
}
set {
if i < count{
let idx = index(startIndex, offsetBy: i)
remove(at: idx)
if let new = newValue, let first = new.first{
insert(first, at: idx)
}
}
}
}
}
call like this:
var str = "fighter"
str[at: 2] = "6"

How to check last 3 character in string contain numbers in 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
}

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
}
}