1

As a learning exercise, I am trying to use a for-in loop to replace a String in an Array (with a dictionary value) if the String is an existing key in a Dictionary. I already know how to do this using .stringByReplacingOccurrencesOfString, but I'd like to learn what I'm doing wrong here, and how to accomplish this with a for-in loop.

let sillyMonkeyString = "A monkey stole my iPhone"
let dictionary = ["monkey": "🐒", "iPhone":"📱"]
var toArray = sillyMonkeyString.componentsSeparatedByString(" ")
// desired result is "A 🐒 stole my 📱"

Here is what doesn't work:

for var str in toArray {
    if let val = dictionary[str] {
        str = val                  
    }
}
let backToString = toArray.joinWithSeparator(" ")

What does work:

var newArray = [String]()
for var str in toArray {
    if let val = dictionary[str] {
        str = val
    }
    newArray.append(str)
}
let backToString = newArray.joinWithSeparator(" ")

This works because I'm creating a new array and appending to the new array. However, my original Array is mutable, so why is the first solution not correctly assigning str to val in the original Array inside the for-in loop? The only other related question I found here had a great one-line answer that did not explain whether or not I can accomplish this using a for-in loop.

UPDATE: I do not recommend implementing a for-in loop for this particular use case. I asked this question to learn how to do this. If a user would like to replace parts of strings with a dictionary, I highly recommend considering using one of the more efficient and Swifty solutions below (which may not be the accepted solution)

3
  • 3
    Since you're making an attempt to learn things for yourself - here's a bonus challenge: Rather than using for-in use the reduce function on the array. Commented Mar 4, 2016 at 18:25
  • I did the course on udacity as well Commented Mar 4, 2016 at 19:15
  • you can use for in loop (see my answer), but i recommend you more 'functional' and swifty solution Commented Mar 4, 2016 at 19:20

8 Answers 8

3

'pure' Swift (no import Foundation) and 'functional' way leads to

let sillyMonkeyString = "A monkey stole my iPhone"
let dictionary = ["monkey": "🐒", "iPhone":"📱"]

let arr = sillyMonkeyString.characters.split(" ").map(String.init)
let result = arr.map { dictionary[$0] ?? $0 }.joinWithSeparator(" ")

print(result)
// A 🐒 stole my 📱

if for in loop is required, than

var resArr:[String] = []
for txt in arr {
    resArr.append(dictionary[txt] ?? txt)
}
let result2 = resArr.joinWithSeparator(" ") // A 🐒 stole my 📱"

if you would like to have mutating solution ...

let sillyMonkeyString = "A monkey stole my iPhone"
let dictionary = ["monkey": "🐒", "iPhone":"📱"]

var arr = sillyMonkeyString.characters.split(" ").map(String.init)

for (idx,value) in arr.enumerate() {
    arr[idx] = dictionary[value] ?? value
}
let result3 = arr.joinWithSeparator(" ") // "A 🐒 stole my 📱"

if you don't like enumerate(), you can use indices instead

let sillyMonkeyString = "A monkey stole my iPhone"
let dictionary = ["monkey": "🐒", "iPhone":"📱"]

var arr = sillyMonkeyString.characters.split(" ").map(String.init)

for i in arr.indices {
    arr[i] = dictionary[arr[i]] ?? arr[i]
}
let result4 = arr.joinWithSeparator(" ") // "A 🐒 stole my 📱"
Sign up to request clarification or add additional context in comments.

2 Comments

Your 2nd answer is what I posted in my question originally, as I knew it worked if I created a new array, but wanted to know how to avoid doing so.
@jungledev OK, i append 'mutating' solution :-)
1

When declaring something as "var" in a for loop or "if var"(deprecated) statement, the value is copied to that variable, not the reference. So when you change "val", you're changing that local variable and not changing the object that exists in toArray

If you want to change it within the array, this should work (not compiled fyi):

for str in toArray {
    if let val = dictionary[str] {
        toArray[toArray.indexOf(str)] = val
    }
}

Alternately, you can track the index instead with

for i in 0 ..< toArray.count {
    if let val = dictionary[toArray[i]] {
        toArray[i] = val
    }
}

To give an comparable example to the link you sent, you could do something like this:

toArray = toArray.map { dictionary[$0] ?? $0 }

If you're unfamiliar with the map function, I would suggest looking it up as it's one of the best features in swift! :)

6 Comments

I understand mapping. I'm curious why the exercise challenge I'm working on is explicitly asking me to solve it with a for-in loop. My solution feels hacky, but if the challenge is asking for a for-in, I'm assuming there's an elegant solution to be discovered. I can't get your first solution to work yet. I feel like there should be another way other than using indexes. pushing on.. (and thanks for the help =) )
Saw a potential mistake. I believe it should be .indexOf(str) (answer updated)
Very close. I just figured it out. Your solution threw an optional error. I've added mine.
Although they're both valid solutions it's good to notice that the second one is more performant (which you might notice on long strings) - every time you call toArray.indexOf(str) the function has to loop through the array to find the index and as shown in the second solution this is unnecessary.
Yeah, if you want to do pure fast enumeration it's limiting. your other option is to track the index independent of your loop.
|
1

str = val will only change the value in your local variable, which is valid only in the scope of the for loop. The value in the array will no be changed. What you want really is:

for i in 0..< toArray.count {
    if let val = dictionary[toArray[i]] {
        toArray[i] = val
    }
}

4 Comments

FYI this traditional C-style for loop is deprecate in Swift 2.2 and will be removed in Swift 3.0
@PEEJWEEJ Thanks. Updated to new swift syntax.
You're starting to loop at index 1, this will cause on indexOutOfBounds error. It should be "for i in 0..< toArray.count".
still will crash with out of bound ( ... should be ..<)
1

You can use forEach to iterate through your dictionary key/value pairs and replace them as follow:

var sillyMonkeyString = "A monkey stole my iPhone"
let dictionary = ["monkey": "🐒", "iPhone":"📱"]

dictionary.forEach{ sillyMonkeyString = sillyMonkeyString.stringByReplacingOccurrencesOfString($0.0, withString: $0.1) }
print(sillyMonkeyString)  // "A 🐒 stole my 📱"

Comments

1

How about this approach:

var sillyMonkeyString = "A monkey stole my iPhone"
let dictionary = ["monkey": "🐒", "iPhone":"📱"]
var toArray = sillyMonkeyString.componentsSeparatedByString(" ")

for (index, string) in toArray.enumerate()
{
  if let val = dictionary[string]
  {
    toArray[index] = val
  }
}
let finalString = toArray.joinWithSeparator(" ")

That code uses a variant of for...in that returns the objects in an array and the index as tuples. Some other answers used indexOf. indexOf is both computationally expensive, and potentially risky. Arrays can contain multiple copies of the same object, so the string in question could occur at multiple places. In this particular example, it works because we loop through the array of words front to back, replacing matching words as we go. So, indexOf will always find the first remaining occurrence of the string. Nevertheless, it is still quite slow on large arrays.

Comments

1

This is the solution I came up with. I'll consider using others since many have recommended that using .indexOf is very expensive. Rather than assigning to an ivar, I'm assigning val directly to the item at the current index in the original array. I'm all for learning and implementing what is least costly. However, you got to admit this is clean and short. (though not the most efficient)

for var str in toArray {
    if let val = dictionary[str] {
        toArray[toArray.indexOf(str)!] = val
    }
}

3 Comments

just a hint: indexOf is potentially expensive (it enumerates each time from the beginning) and can fail if equal objects are in an array (it returns the first index of an equal object).
I avoid using it by using enumeration methods or by maintaining my own index variable.
Please elaborate on how this code answers the question.
1

On the topic of "what was wrong" in the code that didn't work :

 for var str in toArray {
     if let val = dictionary[str] {
         str = val                  
     }
 }
 let backToString = toArray.joinWithSeparator(" ")

The str variable in your for loop is not a reference to the elements of your toArray but a copy of it. So modifying str inside the loop has no effect on the array it came from.

Comments

0

As others have said, instead of

for var str in toArray {

if must be

for str in toArray {

But I want to show another approach, using enumerate() on dictionary

var sillyMonkeyString = "A monkey stole my iPhone"
let dictionary = ["monkey": "🐒", "iPhone":"📱"]

for (_ ,v) in dictionary.enumerate() {
    sillyMonkeyString = sillyMonkeyString.stringByReplacingOccurrencesOfString(v.0, withString: v.1)
}

print(sillyMonkeyString)

prints A 🐒 stole my 📱

enumerate() returns a tuple containing of the index and the tuple with the key and value.

and to make tanzolone happy, though this is a rather stupid approach:

let sillyMonkeyString = "A monkey stole my iPhone"
let dictionary = ["monkey": "🐒", "iPhone":"📱"]
var toArray = sillyMonkeyString.componentsSeparatedByString(" ")

for (i,v) in dictionary.enumerate() {
    for (j,s) in toArray.enumerate() {
        if s == v.0 {
            toArray[j] = v.1
        }
    }
}

print(toArray.joinWithSeparator(" "))

enumeration with for in

var sillyMonkeyString = "A monkey stole my iPhone"
let dictionary = ["monkey": "🐒", "iPhone":"📱"]

for (k,v) in dictionary{
    sillyMonkeyString = sillyMonkeyString.stringByReplacingOccurrencesOfString(k, withString: v)
}

print(sillyMonkeyString)

6 Comments

PO has clearly said: "I already know how to do this using .stringByReplacingOccurrencesOfString, but I'd like to learn what I'm doing wrong here, and how to accomplish this with a for-in loop."
the main focus here is that I use proper enumeration as requested by OP (for in). Your answer violates that main aspect of the question.
So, .enumerate really is less expensive because it converts the Array to a dictionary and only enumerates once? Really enjoying this dialogue =)
Speaking in BigO you can say that indexOf make any algorithm of n^x to n^(x+1), as it has n for each call and is executed n times. enumerate does not convert anything. depending on what you call it, it will return a tuple of an index and some object. as I am enumerating the dictionary the tuple consists of the index and a tuple representing the dictionaries key and value. enumerate is cheaper, as it is linear, no extra look up for the index is needed and it is guaranteed to be the right one.
@vikingosegundo for (_ ,v) in dictionary.enumerate() why are you using enumerate if you are discarding the index ?
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.