Swift 3.0 Iterate Over String.Index Range

Swift 3.0 iterate over String.Index range

You can traverse a string by using indices property of the characters property like this:

let letters = "string"
let middle = letters.index(letters.startIndex, offsetBy: letters.characters.count / 2)

for index in letters.characters.indices {

// to traverse to half the length of string
if index == middle { break } // s, t, r

print(letters[index]) // s, t, r, i, n, g
}

From the documentation in section Strings and Characters - Counting Characters:

Extended grapheme clusters can be composed of one or more Unicode scalars. This means that different characters—and different representations of the same character—can require different amounts of memory to store. Because of this, characters in Swift do not each take up the same amount of memory within a string’s representation. As a result, the number of characters in a string cannot be calculated without iterating through the string to determine its extended grapheme cluster boundaries.

emphasis is my own.

This will not work:

let secondChar = letters[1] 
// error: subscript is unavailable, cannot subscript String with an Int

Getting String.Index while enumerating swift string

There is no built-in functionality for this. You could wrap this in a custom iterator, but then you only encapsulate the same kind of computation in a different place, so that's not an answer :)

Code Complexity

However, you can improve performance of your current code:

greeting.index(greeting.startIndex, offsetBy: intIndex)
  • This will calculate the index from startIndex to the resulting index for every loop iteration.
  • The index calculation with index(_:offsetBy:) is really just another loop itself, where it +1s each index. There's no O(1) way to "compute" the index; it is found out by a loop in O(n)

So your own outer loop is linear with O(n) for n iterations, one for every character.

Then computing the index with an inner loop means there are 1+2+3+4+5+6+...n = (n^2 + n)/2 iterations, where n is the intIndex in this case.

That means the algorithm has a complexity of *handwaiving* roundabout O(n + n^2). The quadratic part is problematic!

Better approach

You can get the complexity down to 2 operations per iteration, or O(2n). Just keep the previously computed index in memory and +1 yourself, avoiding a recomputation from scratch.

Here's the code:

let greeting = "Hello"
var index = greeting.startIndex
for char in greeting {
let indexAfterCurrentIndex = greeting.index(after: index)
print(greeting[indexAfterCurrentIndex...])
index = indexAfterCurrentIndex
}

Still not a simple and built-in solution, but you can just as well wrap this more efficient algorithm and off you go!

extension String {
func forEachCharacterWithIndex(iterator: (String.Index, Character) -> Void) {
var currIndex = self.startIndex
for char in self {
iterator(currIndex, char)
currIndex = self.index(after: currIndex)
}
}
}

let greeting = "Hello"
greeting.forEachCharacterWithIndex { (index, char) in
let indexAfterCurrentIndex = greeting.index(after: index)
print(greeting[indexAfterCurrentIndex...])
}

Range String.Index Versus String.Index

String indices aren't integers. They're opaque objects (of type String.Index) which can be used to subscript into a String to obtain a character.

Ranges aren't limited to only Range<Int>. If you look at the declaration of Range, you can see it's generic over any Bound, so long as the Bound is Comparable (which String.Index is).

So a Range<String.Index> is just that. It's a range of string indices, and just like any other range, it has a lowerBound, and an upperBound.

How to iterate a loop with index and element in Swift

Yes. As of Swift 3.0, if you need the index for each element along with its value, you can use the enumerated() method to iterate over the array. It returns a sequence of pairs composed of the index and the value for each item in the array. For example:

for (index, element) in list.enumerated() {
print("Item \(index): \(element)")
}

Before Swift 3.0 and after Swift 2.0, the function was called enumerate():

for (index, element) in list.enumerate() {
print("Item \(index): \(element)")
}

Prior to Swift 2.0, enumerate was a global function.

for (index, element) in enumerate(list) {
println("Item \(index): \(element)")
}

Iterate over part of String in Swift

string.characters is a collection of characters.
Use reversed() to access the elements in reverse order, anddropFirst() to skip the initial element of the reversed collection:

let string = "abbr>
for ch in string.characters.reversed().dropFirst() {
print(ch)
// `ch` is a Character. Use `String(ch)` if you need a String.
}

Output:


b
br>a

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.Index work in Swift

Sample Image

All of the following examples use

var str = "Hello, playground"

startIndex and endIndex

  • startIndex is the index of the first character
  • endIndex is the index after the last character.

Example

// character
str[str.startIndex] // H
str[str.endIndex] // error: after last character

// range
let range = str.startIndex..<str.endIndex
str[range] // "Hello, playground"

With Swift 4's one-sided ranges, the range can be simplified to one of the following forms.

let range = str.startIndex...
let range = ..<str.endIndex

I will use the full form in the follow examples for the sake of clarity, but for the sake of readability, you will probably want to use the one-sided ranges in your code.

after

As in: index(after: String.Index)

  • after refers to the index of the character directly after the given index.

Examples

// character
let index = str.index(after: str.startIndex)
str[index] // "e"

// range
let range = str.index(after: str.startIndex)..<str.endIndex
str[range] // "ello, playground"

before

As in: index(before: String.Index)

  • before refers to the index of the character directly before the given index.

Examples

// character
let index = str.index(before: str.endIndex)
str[index] // d

// range
let range = str.startIndex..<str.index(before: str.endIndex)
str[range] // Hello, playgroun

offsetBy

As in: index(String.Index, offsetBy: String.IndexDistance)

  • The offsetBy value can be positive or negative and starts from the given index. Although it is of the type String.IndexDistance, you can give it an Int.

Examples

// character
let index = str.index(str.startIndex, offsetBy: 7)
str[index] // p

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

limitedBy

As in: index(String.Index, offsetBy: String.IndexDistance, limitedBy: String.Index)

  • The limitedBy is useful for making sure that the offset does not cause the index to go out of bounds. It is a bounding index. Since it is possible for the offset to exceed the limit, this method returns an Optional. It returns nil if the index is out of bounds.

Example

// character
if let index = str.index(str.startIndex, offsetBy: 7, limitedBy: str.endIndex) {
str[index] // p
}

If the offset had been 77 instead of 7, then the if statement would have been skipped.

Why is String.Index needed?

It would be much easier to use an Int index for Strings. The reason that you have to create a new String.Index for every String is that Characters in Swift are not all the same length under the hood. A single Swift Character might be composed of one, two, or even more Unicode code points. Thus each unique String must calculate the indexes of its Characters.

It is possible to hide this complexity behind an Int index extension, but I am reluctant to do so. It is good to be reminded of what is actually happening.

How to iterate through an array of Strings and get the substring in Swift?

Try this:

var months = [String]()
var days = [String]()
var array = ["January 27, 5:00PM - 10:00PM", "February 28, 11:00AM - 10:00PM", "March 29, 11:00AM - 9:00PM"]

array.forEach() {
let monthAndDay = $0.components(separatedBy: ",")

let dateFormatter = DateFormatter()

dateFormatter.dateFormat = "MMM dd"
let date = dateFormatter.date(from: monthAndDay.first!)

let dateFormatterMonth = DateFormatter()
dateFormatterMonth.dateFormat = "MMM"
months.append(dateFormatterMonth.string(from: date!))

let dateFormatterDay = DateFormatter()
dateFormatterDay.dateFormat = "dd"
days.append(dateFormatterDay.string(from: date!))
}

print(months) // Jan, Fer, Mar
print(days) // 27, 28, 29

Swift: For Loop to iterate through enumerated array by index greater than 1

You have two ways to get your desired output.

  1. Using only stride

    var testArray2: [String] = ["a", "b", "c", "d", "e"]

    for index in stride(from: 0, to: testArray2.count, by: 2) {
    print("position \(index) : \(testArray2[index])")
    }
  2. Using enumerated() with for in and where.

    for (index,item) in testArray2.enumerated() where index % 2 == 0 {
    print("position \(index) : \(item)")
    }


Related Topics



Leave a reply



Submit