How to create range in Swift?
Asked Answered
D

10

129

In Objective-c we create range by using NSRange

NSRange range;

So how to create range in Swift?

Dunbarton answered 7/5, 2015 at 6:41 Comment(1)
What do you need the range for? Swift strings use Range<String.Index>, but sometimes it is necessary to work with NSString and NSRange, so some more context would be useful. – But have a look at #24093384.Agueweed
C
302

Updated for Swift 4

Swift ranges are more complex than NSRange, and they didn't get any easier in Swift 3. If you want to try to understand the reasoning behind some of this complexity, read this and this. I'll just show you how to create them and when you might use them.

Closed Ranges: a...b

This range operator creates a Swift range which includes both element a and element b, even if b is the maximum possible value for a type (like Int.max). There are two different types of closed ranges: ClosedRange and CountableClosedRange.

1. ClosedRange

The elements of all ranges in Swift are comparable (ie, they conform to the Comparable protocol). That allows you to access the elements in the range from a collection. Here is an example:

let myRange: ClosedRange = 1...3

let myArray = ["a", "b", "c", "d", "e"]
myArray[myRange] // ["b", "c", "d"]

However, a ClosedRange is not countable (ie, it does not conform to the Sequence protocol). That means you can't iterate over the elements with a for loop. For that you need the CountableClosedRange.

2. CountableClosedRange

This is similar to the last one except now the range can also be iterated over.

let myRange: CountableClosedRange = 1...3

let myArray = ["a", "b", "c", "d", "e"]
myArray[myRange] // ["b", "c", "d"]

for index in myRange {
    print(myArray[index])
}

Half-Open Ranges: a..<b

This range operator includes element a but not element b. Like above, there are two different types of half-open ranges: Range and CountableRange.

1. Range

As with ClosedRange, you can access the elements of a collection with a Range. Example:

let myRange: Range = 1..<3

let myArray = ["a", "b", "c", "d", "e"]
myArray[myRange] // ["b", "c"]

Again, though, you cannot iterate over a Range because it is only comparable, not stridable.

2. CountableRange

A CountableRange allows iteration.

let myRange: CountableRange = 1..<3

let myArray = ["a", "b", "c", "d", "e"]
myArray[myRange] // ["b", "c"]

for index in myRange {
    print(myArray[index])
}

NSRange

You can (must) still use NSRange at times in Swift (when making attributed strings, for example), so it is helpful to know how to make one.

let myNSRange = NSRange(location: 3, length: 2)

Note that this is location and length, not start index and end index. The example here is similar in meaning to the Swift range 3..<5. However, since the types are different, they are not interchangeable.

Ranges with Strings

The ... and ..< range operators are a shorthand way of creating ranges. For example:

let myRange = 1..<3

The long hand way to create the same range would be

let myRange = CountableRange<Int>(uncheckedBounds: (lower: 1, upper: 3)) // 1..<3

You can see that the index type here is Int. That doesn't work for String, though, because Strings are made of Characters and not all characters are the same size. (Read this for more info.) An emoji like 😀, for example, takes more space than the letter "b".

Problem with NSRange

Try experimenting with NSRange and an NSString with emoji and you'll see what I mean. Headache.

let myNSRange = NSRange(location: 1, length: 3)

let myNSString: NSString = "abcde"
myNSString.substring(with: myNSRange) // "bcd"

let myNSString2: NSString = "a😀cde"
myNSString2.substring(with: myNSRange) // "😀c"    Where is the "d"!?

The smiley face takes two UTF-16 code units to store, so it gives the unexpected result of not including the "d".

Swift Solution

Because of this, with Swift Strings you use Range<String.Index>, not Range<Int>. The String Index is calculated based on a particular string so that it knows if there are any emoji or extended grapheme clusters.

Example

var myString = "abcde"
let start = myString.index(myString.startIndex, offsetBy: 1)
let end = myString.index(myString.startIndex, offsetBy: 4)
let myRange = start..<end
myString[myRange] // "bcd"

myString = "a😀cde"
let start2 = myString.index(myString.startIndex, offsetBy: 1)
let end2 = myString.index(myString.startIndex, offsetBy: 4)
let myRange2 = start2..<end2
myString[myRange2] // "😀cd"

One-sided Ranges: a... and ...b and ..<b

In Swift 4 things were simplified a bit. Whenever the starting or ending point of a range can be inferred, you can leave it off.

Int

You can use one-sided integer ranges to iterate over collections. Here are some examples from the documentation.

// iterate from index 2 to the end of the array
for name in names[2...] {
    print(name)
}

// iterate from the beginning of the array to index 2
for name in names[...2] {
    print(name)
}

// iterate from the beginning of the array up to but not including index 2
for name in names[..<2] {
    print(name)
}

// the range from negative infinity to 5. You can't iterate forward
// over this because the starting point in unknown.
let range = ...5
range.contains(7)   // false
range.contains(4)   // true
range.contains(-1)  // true

// You can iterate over this but it will be an infinate loop 
// so you have to break out at some point.
let range = 5...

String

This also works with String ranges. If you are making a range with str.startIndex or str.endIndex at one end, you can leave it off. The compiler will infer it.

Given

var str = "Hello, playground"
let index = str.index(str.startIndex, offsetBy: 5)

let myRange = ..<index    // Hello

You can go from the index to str.endIndex by using ...

var str = "Hello, playground"
let index = str.index(str.endIndex, offsetBy: -10)
let myRange = index...        // playground

See also:

Notes

  • You can't use a range you created with one string on a different string.
  • As you can see, String ranges are a pain in Swift, but they do make it possibly to deal better with emoji and other Unicode scalars.

Further Study

Craze answered 4/2, 2016 at 5:52 Comment(1)
My comment pertains to the "Problem with NSRange" subsection. NSString internally stores its characters in UTF-16 encoding. A full unicode scalar is 21-bit. The grinning face character (U+1F600) cannot be stored in a single 16-bit code unit, so it's spread over 2. NSRange counts based on 16-bit code units. In this example, 3 code units happen to represent only 2 charactersCupule
U
25

Xcode 8 beta 2 • Swift 3

let myString = "Hello World"
let myRange = myString.startIndex..<myString.index(myString.startIndex, offsetBy: 5)
let mySubString = myString.substring(with: myRange)   // Hello

Xcode 7 • Swift 2.0

let myString = "Hello World"
let myRange = Range<String.Index>(start: myString.startIndex, end: myString.startIndex.advancedBy(5))

let mySubString = myString.substringWithRange(myRange)   // Hello

or simply

let myString = "Hello World"
let myRange = myString.startIndex..<myString.startIndex.advancedBy(5)
let mySubString = myString.substringWithRange(myRange)   // Hello
Undistinguished answered 7/5, 2015 at 7:5 Comment(0)
U
5

I find it surprising that, even in Swift 4, there's still no simple native way to express a String range using Int. The only String methods that let you supply an Int as a way of obtaining a substring by range are prefix and suffix.

It is useful to have on hand some conversion utilities, so that we can talk like NSRange when speaking to a String. Here's a utility that takes a location and length, just like NSRange, and returns a Range<String.Index>:

func range(_ start:Int, _ length:Int) -> Range<String.Index> {
    let i = self.index(start >= 0 ? self.startIndex : self.endIndex,
        offsetBy: start)
    let j = self.index(i, offsetBy: length)
    return i..<j
}

For example, "hello".range(0,1)" is the Range<String.Index> embracing the first character of "hello". As a bonus, I've allowed negative locations: "hello".range(-1,1)" is the Range<String.Index> embracing the last character of "hello".

It is useful also to convert a Range<String.Index> to an NSRange, for those moments when you have to talk to Cocoa (for example, in dealing with NSAttributedString attribute ranges). Swift 4 provides a native way to do that:

let nsrange = NSRange(range, in:s) // where s is the string

We can thus write another utility where we go directly from a String location and length to an NSRange:

extension String {
    func nsRange(_ start:Int, _ length:Int) -> NSRange {
        return NSRange(self.range(start,length), in:self)
    }
}
Uncover answered 29/9, 2017 at 23:59 Comment(0)
B
2

Use like this

var start = str.startIndex // Start at the string's start index
var end = advance(str.startIndex, 5) // Take start index and advance 5 characters forward
var range: Range<String.Index> = Range<String.Index>(start: start,end: end)

let firstFiveDigit =  str.substringWithRange(range)

print(firstFiveDigit)

Output : Hello

Briefs answered 7/5, 2015 at 6:46 Comment(3)
I see they have "Range" Datatype. So how can I use it?Dunbarton
@Dunbarton Can you elaborate more where you want to use ?Briefs
Like in objective c: NSRange range;Dunbarton
S
2

(1..<10)

returns...

Range = 1..<10

Spoils answered 22/10, 2015 at 13:13 Comment(1)
How does this answer the question?Photoflash
C
1

If anyone want to create NSRange object can create as:

let range: NSRange = NSRange.init(location: 0, length: 5)

this will create range with position 0 and length 5

Chromic answered 18/12, 2017 at 6:21 Comment(0)
B
0

You can use like this

let nsRange = NSRange(location: someInt, length: someInt)

as in

let myNSString = bigTOTPCode as NSString //12345678
let firstDigit = myNSString.substringWithRange(NSRange(location: 0, length: 1)) //1
let secondDigit = myNSString.substringWithRange(NSRange(location: 1, length: 1)) //2
let thirdDigit = myNSString.substringWithRange(NSRange(location: 2, length: 4)) //3456
Blodgett answered 7/5, 2015 at 7:5 Comment(0)
D
0

I created the following extension:

extension String {
    func substring(from from:Int, to:Int) -> String? {
        if from<to && from>=0 && to<self.characters.count {
            let rng = self.startIndex.advancedBy(from)..<self.startIndex.advancedBy(to)
            return self.substringWithRange(rng)
        } else {
            return nil
        }
    }
}

example of use:

print("abcde".substring(from: 1, to: 10)) //nil
print("abcde".substring(from: 2, to: 4))  //Optional("cd")
print("abcde".substring(from: 1, to: 0))  //nil
print("abcde".substring(from: 1, to: 1))  //nil
print("abcde".substring(from: -1, to: 1)) //nil
Dyaus answered 5/8, 2016 at 8:6 Comment(0)
S
0
func replace(input: String, start: Int,lenght: Int, newChar: Character) -> String {
    var chars = Array(input.characters)

    for i in start...lenght {
        guard i < input.characters.count else{
            break
        }
        chars[i] = newChar
    }
    return String(chars)
}
Spurling answered 7/8, 2016 at 4:0 Comment(0)
H
0

I want to do this:

print("Hello"[1...3])
// out: Error

But unfortunately, I can't write a subscript of my own because the loathed one takes up the name space.

We can do this however:

print("Hello"[range: 1...3])
// out: ell 

Just add this to your project:

extension String {
    subscript(range: ClosedRange<Int>) -> String {
        get {
            let start = String.Index(utf16Offset: range.lowerBound, in: self)
            let end = String.Index(utf16Offset: range.upperBound, in: self)
            return String(self[start...end])
        }
    }
}
Harmonica answered 25/4, 2020 at 1:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.