Unique element from array with no repeat using random.element()?

Hello! Beginner here with limited English

I am trying to use randomElement() to pick one dictionary from my multi-dictionary array (data from JSONfile). However, I realised randomElement() can repeat the same element previously picked! :frowning: What do you recommend I do to this code snippet?

func randomWord() {
    
    guard let jsonURL = Bundle.main.url(forResource: "vocabList", withExtension: "json") else {
        return}
    
    guard let jsonString = try? String(contentsOf: jsonURL, encoding: String.Encoding.utf8) else {return}
    
    var vocabData: [VocabData]? //from my Struct file
    do {
        vocabData = try JSONDecoder().decode([VocabData].self, from: Data(jsonString.utf8))
    } catch {
        print(error)
    }
  
    if let randomVocab = vocabData?.randomElement() { // have one dictionary picked randomly from Array and able to access its properties
   
    definitionLabel.text = randomVocab.Definition
    chineseLabel.text = randomVocab.Chinese
    pinyinLabel.text = randomVocab.Pinyin } else {print("no randomVocab")
// this works perfectly, but I don't want to randomElement() to pick an element for next time if previously picked
    }
}

You can shuffle the array, then keep poping the last element.

1 Like

Hi Lantua, thank you so much for taking the time to help ^^

I am not sure what you mean by "poping the last element?" Could you provide a code example please?

After you get the shuffled array. You'd want to get the first or last element since it's easy (and fast) to retrieve. You then also want to remove the retrieved element since you don't want to use it again. So you can pretty much do:

var item: Item?
if !array.isEmpty {
  item = array.removeLast()
}

You can't use removeLast if the array is empty (it'll crash), so you need to check it first.

Is randomElement() still needed? This is how I tried to do it:

vocabData?.shuffle()

        if let randomVocab = vocabData?.randomElement() { // want to have one dictionary picked randomly from Array
                   
        definitionLabel.text = randomVocab.Definition
        chineseLabel.text = randomVocab.Chinese
        pinyinLabel.text = randomVocab.Pinyin } else {print("no randomVocab")
        
        if !vocabData!.isEmpty {
        randomVocab = vocabData.removeLast() //generates error 
        }
        }

There are two important pieces:

  • You need to remember the items you've picked (or the ones you haven't). After all, you can't say you picked 7 twice if you don't remember picking it.
  • Whether picking the first or the fifth item, if it's from the scrambled (shuffled) pile, it doesn't matter. I suggest picking the last item because it's easier and faster.

So, you need two steps:

  • You first load the list of items and shuffle them, then
  • You pick an element from the shuffled pile, and remove the element you picked.
var vocabData: [VocabData] = []

func loadWords() {
  // Get URL
  guard let jsonURL = Bundle.main.url(forResource: "vocabList", withExtension: "json") else {
    return
  }

  do {
    // Get JSON string
    let jsonString = try String(contentsOf: jsonURL, encoding: String.Encoding.utf8) else {
    // Decode JSON String, and put it into `vocabData`
    vocabData = try JSONDecoder().decode([VocabData].self, from: Data(jsonString.utf8))
    // Shuffle data in-place
    vocabData.shuffle()
  } catch {
    print(error)
  }
}

func randomWord() {
  if !vocabData.isEmpty {
    let randomVocab = vocabData.removeLast()
    definitionLabel.text = randomVocab.Definition
    chineseLabel.text = randomVocab.Chinese
    pinyinLabel.text = randomVocab.Pinyin
  } else {
    print("no randomVocab")
  }
}

In this case, you need to call loadWord only once before calling randomWord. Since I put vocabData outside of the function, they are shared between the two functions, and will remember which words are available.

randomElement is not useful here. As you discovered, nothing stops randomElement to pick the same element twice.

1 Like

Wow... thank you so much! Now I undertand why I get all the errors, randomElement is useless here. Two separate functions and using vocabData as global variable makes so much sense now...

thank you !!

If you have a model tracking all your data (which you should outside of trivial tutorial code), you should put your vocabData in there instead of as a global var.

1 Like
if let item = array.removeLast() {
    // process another item
} else {
    // no more item
}

Is this cleaner? No var item: Item? optional to deal with and one place to check availability of data and retrieve the new item.

1 Like

Not quite, removeLast crashes if the array is empty, which is the case after we pop the only item. So it returns Element, not Element?.

Also, in later version, I just declare it inside the if block, removing the optional.

2 Likes

popLast() -> Element? is also a thing.

4 Likes

:face_with_monocle: I thought I checked... ah well. :innocent:

1 Like

Thank you the kindness everyone!

Just two generic questions to gain a better understanding:

1)Is there a way for an array to "restart"? Something like:

func randomWord() {
  if !vocabData.isEmpty {
    //code
  } else {
    //restart array?
  }

Does removeLast actually delete the element from the file or memory?

  1. If the app is updated (in real life) adds more elements to the array, would it then reiterate over the elements it had previously selected for the user or not? I guess it is related to Q1 and memory.

Makes sense now thank you!

The only thing we have is what is available to use, which is stored in vocabData. So if you want to restart it, you just need to fill in the vocabData with all items, then shuffle again. Sounds familiar? Because that's exactly what loadWord does! So you can just call the function.

Let's look at loadWords a little bit:

func loadWords() {
  // Get URL
  guard let jsonURL = Bundle.main.url(forResource: "vocabList", withExtension: "json") else {
    return
  }

  do {
    // Get JSON string
    let jsonString = try String(contentsOf: jsonURL, encoding: String.Encoding.utf8) else {
    // Decode JSON String, and put it into `vocabData`
    vocabData = try JSONDecoder().decode([VocabData].self, from: Data(jsonString.utf8))
    // Shuffle data in-place
    vocabData.shuffle()
  } catch {
    print(error)
  }
}
  1. We read the file into jsonString,
  2. Decode it into vocabData, then
  3. Shuffle vocabData.

When we put the file content into jsonString, we copy the content into jsonString. Mutating jsonString won't change the file content. Likewise, changing the file won't mutate the jsonString. Similarly, when you decode the string and put it in vocabData, the content of vocabData and jsonString are independent of each other. That's why when you shuffle vocabData, jsonString doesn't get jumbled.

So removeLast does mutate the memory of vocabData, but doesn't touch jsonString (not that it still exists), and won't be altering the original file.

It depends on how you update it. As mentioned earlier, the content of the file are separated from the content of the vocabData. So if you update the file, vocabData won't change.

You can change the vocabData as you see fit, it only represent the items that can be selected, in a random order. So long as you maintain that notion, it should be fine.


So, to meet the new requirement to restart the when vocabData is empty, the randomWord becomes:

func randomWord() {
  if vocabData.isEmpty {
    loadWords()
  }
  assert(!vocabData.isEmpty)

  let randomVocab = vocabData.removeLast()
  definitionLabel.text = randomVocab.Definition
  chineseLabel.text = randomVocab.Chinese
  pinyinLabel.text = randomVocab.Pinyin
}

Note that I added a little function call assert(...). This function will crash the program if the condition inside the call is false (when you're in debug mode). I out it there because of what the code before just did:

if vocabData.isEmpty {
  loadWords()
}

What did we just do? "if vocabData is empty, load the words (into vocabData)." So vocabData can't be empty right after.

  • If it was't empty at the beginning of randomWord, it surely isn't right now,
  • If it was empty at the beginning of randomWord, we just reload it.

Note that it can still be empty if, it was empty at the beginning and the file contains no item. If you want to handle it, you can replace assert with appropriate checking.

Now after we assert that vocabData isn't empty, we can just assume it to be so afterward (we haven't mutate vocabData, there's no reason to think otherwise). So we can remove the unnecessary if.

1 Like

This makes sense! Thank you for this clear explanation - you are an excellent teacher!

I wish the Apple documentation was as good as this :sweat_smile: