12

So I've found issues relating to the case of converting NSRange to Range<String.Index>, but I've actually run into the opposite problem.

Quite simply, I have a String and a Range<String.Index> and need to convert the latter into an NSRange for use with an older function.

So far my only workaround has been to grab a substring instead like so:

func foo(theString: String, inRange: Range<String.Index>?) -> Bool {
    let theSubString = (nil == inRange) ? theString : theString.substringWithRange(inRange!)
    return olderFunction(theSubString, NSMakeRange(0, countElements(theSubString)))
}

This works of course, but it isn't very pretty, I'd much rather avoid having to grab a sub-string and just use the range itself somehow, is this possible?

Haravikk
  • 3,109
  • 1
  • 33
  • 46
  • Where do you want to use the range instead of the substring? As argument to olderFunction? Then what is it's purpose? – qwerty_so Jan 24 '15 at 18:09
  • I suppose my example may be confusing the issue since it only returns a `Bool`, however, some other similar functions return more complex results, which are going to require correction when using this sub-string trick, since the indices they return will be for `theSubString` rather than `theString`. Is there really no way to just convert `String.Index` to an `Int`? – Haravikk Jan 25 '15 at 14:42
  • something that worked for me (in my situation): `if idx.startIndex == url.startIndex { // some code }` – olympia Feb 24 '16 at 10:25

6 Answers6

10

If you look into the definition of String.Index you find:

struct Index : BidirectionalIndexType, Comparable, Reflectable {

    /// Returns the next consecutive value after `self`.
    ///
    /// Requires: the next value is representable.
    func successor() -> String.Index

    /// Returns the previous consecutive value before `self`.
    ///
    /// Requires: the previous value is representable.
    func predecessor() -> String.Index

    /// Returns a mirror that reflects `self`.
    func getMirror() -> MirrorType
}

So actually there is no way to convert it to Int and that for good reason. Depending on the encoding of the string the single characters occupy a different number of bytes. The only way would be to count how many successor operations are needed to reach the desired String.Index.

Edit The definition of String has changed over the various Swift versions but it's basically the same answer. To see the very current definition just CMD-click on a String definition in XCode to get to the root (works for other types as well).

The distanceTo is an extension which goes to a variety of protocols. Just look for it in the String source after the CMD-click.

qwerty_so
  • 35,448
  • 8
  • 62
  • 86
  • 1
    I understand the reasons for it, but it's unfortunate. It wouldn't be so bad if I could at least determine that two String.Index values represented a difference of say, 5 characters, as I could use advance(5) to trivially correct my results as it's making working with older code very tricky =( – Haravikk Jan 26 '15 at 14:38
  • You might abbreviate that with an extension. Character strings are in fact voodoo or black magic. We never should have left the 7-bit ASCII tree ;-) – qwerty_so Jan 26 '15 at 14:47
  • Perhaps not, though actually I've just discovered the `distance()` function (or preprocessor, I'm not sure); anyway, it allows for determining the difference between two `String.Index`'s like so: `distance(theString.startIndex, theRange.startIndex)`. It's still not pretty but I think I can make it work for now, I just hope it doesn't have to handle huge strings in future ;) – Haravikk Jan 27 '15 at 15:42
  • Luckily most strings are relatively short. However, I've seen implementations handling BLOB as string (which of course is not a good idea). See also here: http://practicalswift.com/2014/06/14/the-swift-standard-library-list-of-built-in-functions/ This lists all those predefined functions. Simply a design fault since you have to learn all of them - and I forgot about it :-( – qwerty_so Jan 27 '15 at 15:47
7
let index: Int = string.startIndex.distanceTo(range.startIndex)
Cœur
  • 37,241
  • 25
  • 195
  • 267
Alexander Volkov
  • 7,904
  • 1
  • 47
  • 44
7

I don't know which version introduced it, but in Swift 4.2 you can easily convert between the two.

To convert Range<String.Index> to NSRange:

let range   = s[s.startIndex..<s.endIndex]
let nsRange = NSRange(range, in: s)

To convert NSRange to Range<String.Index>:

let nsRange = NSMakeRange(0, 4)
let range   = Range(nsRange, in: s)

Keep in mind that NSRange is UTF-16 based, while Range<String.Index> is Character based. Hence you can't just use counts and positions to convert between the two!

hnh
  • 13,957
  • 6
  • 30
  • 40
3

In Swift 4, distanceTo() is deprecated. You may have to convert String to NSString to take advantage of its -[NSString rangeOfString:] method, which returns an NSRange.

wzso
  • 3,482
  • 5
  • 27
  • 48
0

Swift 4 Complete Solution:

OffsetIndexableCollection (String using Int Index)

https://github.com/frogcjn/OffsetIndexableCollection-String-Int-Indexable-

let a = "01234"

print(a[0]) // 0
print(a[0...4]) // 01234
print(a[...]) // 01234

print(a[..<2]) // 01
print(a[...2]) // 012
print(a[2...]) // 234
print(a[2...3]) // 23
print(a[2...2]) // 2

if let number = a.index(of: "1") {
    print(number) // 1
    print(a[number...]) // 1234
}

if let number = a.index(where: { $0 > "1" }) {
    print(number) // 2
}
frogcjn
  • 819
  • 5
  • 21
-1

You can use this function and call it when ever you need convertion

extension String
{ 
    func CnvIdxTooIntFnc(IdxPsgVal: Index) -> Int
    {
        return startIndex.distanceTo(IdxPsgVal)
    }
}
Sujay U N
  • 4,974
  • 11
  • 52
  • 88