4

Swift 4 changed how strings worked. However, seems to have got more complicated and less readable. Can anyone simplify this example (simply gets the third letter of a String as a String)? (Other than splitting out the lines.)

let myString="abc"
let thirdLetter = String(myString[myString.index(myString.startIndex, offsetBy: 2)])
Nick McConnell
  • 821
  • 8
  • 28
  • 2
    What do you mean simplify ? No need to create a new String from it `let thirdLetter = myString[myString.index(myString.startIndex, offsetBy: 2)]`. BTW you can check this answer https://stackoverflow.com/a/38215613/2303865. It will allow you to subscript String using Int `myString[2]` – Leo Dabus Sep 15 '17 at 23:02
  • 2
    By simplify, I mean easier to understand and easier to write. I was definitely shocked at this complexity for a simple task. If you don't wrap it with String(...) you end up with a Character type not a String – Nick McConnell Sep 15 '17 at 23:05
  • Well thats the behavior I would expect if you are getting just a single letter, a Character is exactly what it is supposed to return. The answer linked above it is already updated to Swift 4. It also supports partial range – Leo Dabus Sep 15 '17 at 23:06
  • 2
    Yes it works - but is there a better way in Swift 4? Seems unnecessarily complicated. – Nick McConnell Sep 15 '17 at 23:26
  • All you need is to include a new swift file to your project, add those extensions there and you can use it anywhere in your code. I don’t think you will find an easier way to accomplish it – Leo Dabus Sep 16 '17 at 00:03
  • 1
    @LeoDabus appreciate the link. Still seems crazy to need to create extensions to make the String type useable. – Nick McConnell Sep 16 '17 at 00:30
  • @NickMcConnell, Leo's extensions are fast. See my answer for performance comparisons. – vacawama Sep 16 '17 at 03:29
  • 1
    I am a bit reluctant to recommend a `subscript(offset: Int) -> String` extension. It lures people into doing something like `for i in 0.. – Martin R Sep 16 '17 at 08:52
  • "Swift 4 changed how strings worked" Not true. The way strings work has not changed substantially since Swift 1. And the `myString.index(myString.startIndex...` that you seem to be complaining about what introduced in Swift 4, not Swift 3. What Swift 4 did was to simplify the notation by no longer requiring you to write `.characters` everywhere — that's all. – matt Sep 16 '17 at 14:37
  • @matt: `myString.index(myString.startIndex...` is already valid in Swift 3. – Martin R Sep 16 '17 at 15:01
  • @MartinR Sorry, that was just a typo, I meant "Swift 3, not Swift 4" That was the _point_ of the comment. :( – matt Sep 16 '17 at 15:24

2 Answers2

7

In Swift 4 you can convert a String myString to [Character] with Array(myString). Then you can index that array with Int and then convert that [Character] to String.

let myString = "abc"
let thirdLetter = String(Array(myString)[2])    // "c"
let firstTwo = String(Array(myString)[0..<2])   // "ab"

If you are going to do a lot of operations on a String, it is frequently better to just convert and keep it as [Character].


Note: I have reworked this section to try to avoid any caching optimizations the compiler might do. Each method is now measured just once and a running total is kept for each method.

Converting to Array and indexing with Int is easy to write and read, but how does it perform? To answer this, I tested the following in a release build:

func time1(str: String, n: Int) -> (Double, String) {
    // Method 1: Index string with String.Index, convert to String

    let start = Date()
    let a = String(str[str.index(str.startIndex, offsetBy: n)])
    let interval = Date().timeIntervalSince(start)

    return (interval, a)
}

func time2(str: String, n: Int) -> (Double, String) {
    // Method 2: Convert string to array, index with Int, convert to String

    let start = Date()
    let a = String(Array(str)[n])
    let interval = Date().timeIntervalSince(start)

    return (interval, a)
}


func time3(str: String, n: Int) -> (Double, String) {
    // Method 3: Use prefix() and last(), convert to String

    let start = Date()
    let a = String(str.prefix(n + 1).last!)
    let interval = Date().timeIntervalSince(start)

    return (interval, a)
}

func time4(str: String, n: Int) -> (Double, String) {
    // Method 4: Use Leo Dabus' extensions
    // https://stackoverflow.com/q/24092884/1630618

    let start = Date()
    let a = str[n]
    let interval = Date().timeIntervalSince(start)

    return (interval, a)
}

func time5(str: String, n: Int) -> (Double, String) {
    // Method 5: Same as 2 but don't measure Array conversion time

    let arr = Array(str)

    let start = Date()
    let a = String(arr[n])
    let interval = Date().timeIntervalSince(start)

    return (interval, a)
}

func test() {
    for repetitions in [1, 10, 100, 1000] {
        var input = ""
        for _ in 0 ..< repetitions {
            input.append("abcdefghijklmnopqrstuvwxyz")
        }

        var t = [0.0, 0.0, 0.0, 0.0, 0.0]
        let funcs = [time1, time2, time3, time4, time5]

        for i in 0 ..< input.count {
            for f in funcs.indices {
                let (interval, _) = funcs[f](input, i)
                t[f] += interval
            }
        }

        print("For string length \(input.count):")
        for i in 0 ..< 5 {
            print(String(format: "Method %d time: %.8f", i + 1, t[i]))
        }
        print("")
    }
}

Results:

For string length 26:
Method 1 time: 0.00108612
Method 2 time: 0.00085294
Method 3 time: 0.00005889
Method 4 time: 0.00002104
Method 5 time: 0.00000405

For string length 260:
Method 1 time: 0.00117570
Method 2 time: 0.00670648
Method 3 time: 0.00115579
Method 4 time: 0.00110406
Method 5 time: 0.00007111

For string length 2600:
Method 1 time: 0.09964919
Method 2 time: 0.57621503
Method 3 time: 0.09244329
Method 4 time: 0.09166771
Method 5 time: 0.00087011

For string length 26000:
Method 1 time: 9.78054154
Method 2 time: 56.92994779
Method 3 time: 9.02372885
Method 4 time: 9.01480001
Method 5 time: 0.03442019

Analysis:

  1. The conversion to array is expensive, especially as the array size grows.
  2. If you can keep the converted [Character] around, the indexing operations on it are quite quick. (see Method 5)
  3. Methods 1, 3, and 4 are all about the same speed, so choose based on your own personal preference.
vacawama
  • 150,663
  • 30
  • 266
  • 294
  • 1
    I did it in a real project. – vacawama Sep 16 '17 at 02:52
  • Can you test my extension and see if it is slower or faster than using Array of characters? – Leo Dabus Sep 16 '17 at 02:53
  • 1
    Yes. I will test it. – vacawama Sep 16 '17 at 02:55
  • I think that for really short strings Array of characters it is probably faster. One advantage of using the extension is that you don't need to check the length of it before trying to access the array element at certain index and also the fact that it returns a substring instead of a String or an Array of Characters – Leo Dabus Sep 16 '17 at 02:56
  • 1
    @LeoDabus, your extension was *wicked fast* as we say here in New England, USA. – vacawama Sep 16 '17 at 03:20
  • I wonder why @Leo's subscript method is so much faster. If I understand it correctly, it essentially does the same calculations as `str[str.index(str.startIndex, offsetBy: i)]` in the `time1` functions. – I will do some tests later. – Martin R Sep 16 '17 at 06:11
  • Did you compile in Release mode? You should also repeat the tests `for _ in 1...4 { time1(); ... }` to avoid influences of memory caching. In my tests, method 1, 2, 4 are roughly equally fast (with Leo's methods slightly faster), and method 2 is slower by a factor which depends on the size of the string. – Martin R Sep 16 '17 at 08:55
2

It appears you are trying to extract the third character. I would say

let c = myString.prefix(3).last

Granted, it's an Optional Character, but you can unwrap at leisure and coerce to String when needed.

The prefix method is extremely valuable because it takes an Int rather than forcing you into the wild and wacky world of String.Index.

matt
  • 515,959
  • 87
  • 875
  • 1,141