How can I remove a key from a dictionary where the value is X? I need a dictionary.removeKeyForValue(...) function.
I'd like to optimise the following code. I have a text which is associated with a certain category and a dictionary that associates all keywords to categories. Although my text is already categorised I'd like check whether it should fall into a different category.
let text = "he said hello and then ran away" // This is taken from the "activity" category
// Dictionary associating keywords to categories
let categoryRules = ["hi" : "greeting", "hello" : "greeting", "jogging" : "activity", "joy" : "feeling"]
let keywords = Array(categoryRules.keys)
// Make out of text an Array of words.
let textWordArray = text.lowercaseString.characters.split{$0 == " "}.map(String.init)
// I SHOULDN'T HAVE TO GO THROUGH THE KEYS ASSOCIATED WITH "ACTIVITY" BECAUSE TEXT IS ALREADY IN IT.
for keyword in keywords {
// If text contains the rule
if let index = textWordArray.indexOf(keyword) {
// Get the associated category
if let category = categoryRules[keyword] {
print("The text should fall into the category of \(category)")
break
}
}
}
To remove all keys from a dictionary with a certain value, you can use a for loop with where clause to select the keys to remove and then assign nil to remove them:
var categoryRules = ["hi" : "greeting", "hello" : "greeting", "jogging" : "activity", "joy" : "feeling"]
for (key, value) in categoryRules where value == "greeting" {
categoryRules[key] = nil
}
print(categoryRules) // ["jogging": "activity", "joy": "feeling"]
You can add removeKeysForValue by adding an extension to Dictionary that works for values that are Equatable (can be compared with ==):
extension Dictionary where Value: Equatable {
mutating func removeKeysForValue(value: Value) {
for (key, val) in self where val == value {
self[key] = nil
}
}
}
var categoryRules = ["hi" : "greeting", "hello" : "greeting", "jogging" : "activity", "joy" : "feeling"]
categoryRules.removeKeysForValue("greeting")
print(categoryRules) // ["jogging": "activity", "joy": "feeling"]
Here's an alternate solution that I find elegant which uses filters.
var categoryRules = ["hi" : "greeting", "hello" : "greeting", "jogging" : "activity", "joy" : "feeling"]
let keysToRemove = dict.keys.filter { dict[$0]! == "greeting" }
for key in keysToRemove {
dict.removeValueForKey(key)
}
// categoryRules = ["jogging": "activity", "joy": "feeling"]
keysToRemove will have "hi" and "hello" because they matched the given filter of having the value "greeting".
Edit:
OP mentioned that he wanted a dictionary.removeKeyForValue() function. You can create an extension with the above code to increase readability and avoid code duplication if you plan on doing this action often.
Example:
extension Dictionary {
mutating func removeKeysForValue(value: NSObject) {
let keysToRemove = self.keys.filter { self[$0]! as! NSObject == value }
for key in keysToRemove {
self.removeValueForKey(key)
}
}
}
var dict = ["hi" : "greeting", "hello" : "greeting", "jogging" : "activity", "joy" : "feeling"]
dict.removeKeysForValue("greeting") // dict = ["jogging": "activity", "joy": "feeling"]
Since it will take O(n) time to remove keys from the dictionary (where n is the size of the dictionary) it might be a better idea to create a list of dictionaries (instead of just one.) Each dictionary would exclude the necessary elements. So when looking up values out of the dictionary you just have to choose the correct dictionary.
This will have better performance then removing keys from the dictionary at runtime. Especially when doing lookups multiple times in loops.
Related
Swift 5
I want to get array of all values from "title" key
// Create variable
var arrSportsList:[[String:String]] = []
// viewDidLoad code
arrSportsList = [
["title":"Cricket"],
["title":"Soccer"],
["title":"American Football"],
["title":"Ice Hockey"],
["title":"Tennis"],
["title":"Baseball"],
["title":"Basketball"],
]
I want to use this title in picker view.
You can use compactMap:
let titleArr = arrSportsList.compactMap { $0["title"] }
It transforms each dictionary to the value associated with the key title, and removes the dictionaries which don't have a title key.
I also suggest you to create a class/struct to store these sports, instead of dictionaries:
struct Sport {
let title: String
// other properties
}
Use compactMap(_:) method to get the title value from all dictionaries. If any dictionary doesn't contain title key it will be ignored
var arrSportsList:[[String:String]] = []
// viewDidLoad code
arrSportsList = [
["title1":"anothergame"],
["title":"Cricket"],
["title":"Soccer"],
["title":"American Football"],
["title":"Ice Hockey"],
["title":"Tennis"],
["title":"Baseball"],
["title":"Basketball"],
]
let titleArr = arrSportsList.compactMap { $0["title"] }
print(titleArr)//Cricket,Soccer,American Football,Ice Hockey,Tennis,Baseball,Basketball
Easiest way
let titleArr = arrSportsList.compactMap { $0["title"] }
Or you can loop through the dictionary array and can get the titles Like
let titleArr = Array<String>();
for dict in arrSportsList {
if let title = dict["title"] {
titleArr.append(title);
}
}
In case of understanding more, you can use
// Create variable
var arrSportsList:[[String:String]] = []
// viewDidLoad code
arrSportsList = [
["title":"Cricket"],
["title":"Soccer"],
["title":"American Football"],
["title":"Ice Hockey"],
["title":"Tennis"],
["title":"Baseball"],
["title":"Basketball"],
]
var titleArray: [String]
for (key, value) in arrSportsList {
print (key) // "title"
print (value) //Cricket, Soccer, American Football, Ice Hockey, Tennis, Baseball, Basketball
titleArray.append(value)
}
print (titleArray) //Cricket, Soccer, American Football, Ice Hockey, Tennis, Baseball, Basketball
I have an array which gets instantiated in viewDidLoad like var bookingsArray : [[String]] = []
I am adding elements to it in this way:
var configuration: [String] = []
configuration.append(textfieldFacility.text!)
configuration.append((pickSlotTF.text?.components(separatedBy: " ")[0])!)
configuration.append((pickSlotTF.text?.components(separatedBy: " ")[1])!)
bookingsArray.append(configuration as [String])
bookingsArray looks like :
[["A", "20-08-2017", "14:00"], ["B", "20-08-2017", "14:00"]]
While adding new elements to bookingsArray, I want to check if the new element is already present in the array. How do I do it in this case of multi-dimensional array?
First of all, if you want unique objects use a Set.
If this is not possible I highly recommend to use a custom struct which can conform to Equatable rather than a nested array , for example
struct Booking : Equatable {
let facilty : String
let date : String
let time : String
static func ==(lhs : Booking, rhs : Booking) -> Bool {
return lhs.facilty == rhs.facilty && lhs.date == rhs.date && lhs.time == rhs.time
}
}
Then declare the array as
var bookingsArray = [Booking]()
and create an object with
let dateArray = pickSlotTF.text!.components(separatedBy: " ")
let configuration = Booking(facility: textfieldFacility.text!,
date: dateArray[0],
time = dateArray[1])
bookingsArray.append(configuration)
The huge benefit is that you can easily check
if bookingsArray.contains(item)
You can simply search for it with contains().
var configuration: [String] = []
configuration.append(textfieldFacility.text!)
configuration.append((pickSlotTF.text?.components(separatedBy: " ")[0])!)
configuration.append((pickSlotTF.text?.components(separatedBy: " ")[1])!)
if !bookingsArray.contains(where: {$0 == configuration}) {
bookingsArray.append(configuration)
}
I'm really having trouble with determining a specific value returned in SwiftyJson; hopefully, someone can help explain this to me.
I want to see if there is a match between a predetermined word, "apple" to any of the words received from the JSON responses.
If there is a match then a message is displayed, and the user either chooses to progress to the next level or the user returns to the home screen.
If there is no match then a message is displayed, and the user must either continue playing or cancel playing.
I would like to do this for multiple words across different levels of the game.
Level one: match "apple" to any of the received JSON responses.
Level two: match "computer" to any of the received JSON responses.
Level three: match "telephone" or "phone" or "iPhone" or "Android" or any or all of the above to any of the received JSON responses.
So, basically, I can get all of the JSON responses, but I'm having a hard time finding out how to set up to determine if there is a specific, predefined JSON response returned.
I have looked everywhere for weeks with another post but to no avail :(
JSON RESPONSES
{
"responses" : [
{
"labelAnnotations" : [
{
"mid" : "\/m\/01m2v",
"score" : 0.9245476,
"description" : "computer keyboard"
},
{
"mid" : "\/m\/01c648",
"score" : 0.7945268,
"description" : "laptop"
},
{
"mid" : "\/m\/01mfj",
"score" : 0.74227184,
"description" : "computer hardware"
},
{
"mid" : "\/m\/0541p",
"score" : 0.7062791,
"description" : "multimedia"
},
{
"mid" : "\/m\/07c1v",
"score" : 0.7039645,
"description" : "technology"
},
{
"mid" : "\/m\/03gq5hm",
"score" : 0.69323385,
"description" : "font"
},
{
"mid" : "\/m\/0bs7_0t",
"score" : 0.6724673,
"description" : "electronic device"
},
{
"mid" : "\/m\/01vdm0",
"score" : 0.66489816,
"description" : "electronic keyboard"
},
{
"mid" : "\/m\/0121tl",
"score" : 0.60392517,
"description" : "electronic instrument"
},
{
"mid" : "\/m\/0h8n5_7",
"score" : 0.5834592,
"description" : "laptop replacement keyboard"
}
]
}
]
}
CODE TO SHOW ALL JSON RESPONSES
// Use SwiftyJSON to parse results
let json = JSON(data: dataToParse)
let errorObj: JSON = json["error"]
// Parse the response
print(json)
let responses: JSON = json["responses"][0]
// Get label annotations
let labelAnnotations: JSON = responses["labelAnnotations"]
let numLabels: Int = labelAnnotations.count
var labels: Array<String> = []
if numLabels > 0 {
var labelResultsText:String = "Labels found: "
for index in 0..<numLabels {
let label = labelAnnotations[index]["description"].stringValue
labels.append(label)
}
for label in labels {
// if it's not the last item add a comma
if labels[labels.count - 1] != label {
labelResultsText += "\(label), "
} else {
labelResultsText += "\(label)"
}
}
self.labelResults.text = labelResultsText
} else {
self.labelResults.text = "No labels found"
}
EDIT
I'm apparently not able to answer my own question, I'll post an edit since I think it's a better solution but #pierce's was pretty decent for a single word, not many; it just wasn't applicable for a game setting application.
So, I created a new NSObject, created a
static var _words: [[String]] = [
["apple", "computer", "beer"]]
then
func checkAnnotations(annotations: [Annotation]) -> Bool {
var isMatched = false
let searchWords = self.words
for searchWord in searchWords {
for annotation in annotations {
if searchWord == annotation.descriptionString {
isMatched = true
break
}
}
if isMatched {
break
}
}
return isMatched
}
then created a function to handle the level state,
and finally compared that to the JSON response in the View Controller and advanced level if matched
// Get JSON key value
let labelAnnotations = responses["labelAnnotations"].arrayValue
let annotationObjects: [Annotation] = labelAnnotations.flatMap({ annotationDictionary in
if let mid = annotationDictionary["mid"].string,
let score = annotationDictionary["score"].double,
let description = annotationDictionary["description"].string {
let annotation = Annotation(mid: mid, score: score, descriptionString: description)
return annotation
}
return nil
})
//print(annotationObjects)
let searchString = LevelState.shared.words[0]
print("Level \(LevelState.shared.level), looking for: \(searchString)")
var isMatched = LevelState.shared.checkAnnotations(annotations: annotationObjects)
if isMatched {
LevelState.shared.advance()
}
let alertTitle = isMatched ? "Congrats! You got \(searchString)" : "Keep looking for \(searchString)"
//let translationResult = "Translated: \(levelDescription) to \(translatedText)"
let alertController = UIAlertController(title: alertTitle, message: nil, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
self.prepareForNewLevel()
})
}
First thing - I really don't understand why you want to append a comma at the end of each description. I really thing that's unnecessary. Are you confused about separating elements in an array? Because the actual Strings don't require that, it's only if you're manually writing out the elements of an array (i.e. let array = ["a", "b", "c"]).
So then say you setup a property for this labels array, which is an array of Strings.
var labels: Array<String> = []
Once you've gone through and appended all the description values from your JSON, you can then manipulate it.
if numLabels > 0 {
for index in 0..<numLabels {
let label = labelAnnotations[index]["description"].stringValue
labels.append(label)
}
}
Now you could create a method that would return a filtered array of Strings based on some user entered word:
func findMatches(_ userEntry: String) -> [String] {
return labels.filter { $0.contains(userEntry) }
}
Now you could use the above method to handle some sort of user entry, like say you had the text from a UITextField named textField:
// Return the filtered matches based on the textField text (unless nil)
let matches = findMatches(textField.text ?? "")
// Print the number of matches, and also show the matches
print("Found \(matches.count) matches to user input\r\(matches)")
Now if you had labels which held ["a", "aa", "ba", "b", "c", "apple"], and ran the above code where the userEntry was just the letter "a", you'd see this print out in the console window:
Found 4 matches to user input
["a", "aa", "ba", "apple"]
EDIT - You can use the findMatches method above for what you're trying to do with pre-determined words to match. I'm not sure what you're trying to do exactly, but there are a couple different ways. First, say you had an array of pre-determined words you wanted to check as an array:
let words = ["word", "verb", "noun", "adverb"]
Then you could loop through that and check each one
for word in words {
let matches = findMatches(word)
if matches.count > 0 {
print("Found \(matches.count) matches to \(word)\r\(matches)")
} else {
// Do whatever you want when there are no matches
print("No matches found")
}
}
If you want to just check for a specific word, and have a specific response you could setup a method like so:
func checkWord(word: String, noMatchResponse: String) {
let matches = findMatches(word)
if matches.count > 0 {
print("Found \(matches.count) matches to \(word)\r\(matches)")
} else {
// Do whatever with the no match response
print(noMatchResponse)
}
}
There are so many ways you could implement this. You could also use a switch statement, and then use different case statements for each pre-determined word. It's really up to you and how you want to design your game.
I declared and initialized a [[String:[String:String] dictionary. It's empty in the beginning, and I am trying to add multiple values under the parent keys.
var dictionary = [String:[String:String]
// some checks
dictionary[i] = ["name" : name]
dictionary[i] = ["from" : country]
dictionary[i] = ["age" : age]
When I do that, I end up having only age key as a child under [key: [String:String]. So it's overwriting when I use this approach.
What is the appropriate way of doing
Your code is creating a new dictionary on each line and assigning this in the dictionary for key i, so you end up with the last dictionary ["age" : age]
You need to create an inner dictionary, assign the values to this and then assign this to your outer dictionary;
var innerDict = [String:String]()
innerDict["name"] = name
innerDict["from"] = from
innerDict["age"] = age
dictionary[i] = innerDict
I would suggest, however, that you look at creating a Struct and put that in your outer dictionary rather than using a dictionary of dictionaries
func insert(key:String, value:String, at k:String) {
var d = dictionary[k] ?? [String:String]()
d[key] = value
dictionary[k] = d
}
And here's how to test it:
insert("name", value: name, at: i)
insert("from", value: country, at: i)
insert("age", value: age, at: i)
You can use optional chaining to assign to the inner dictionary, but you need to create the inner dictionary first.
// create the dictionary of dictionaries
var dictionary = [String:[String:String]]()
// some example constants to make your code work
let i = "abc"
let name = "Fred"
let country = "USA"
let age = "28"
// Use nil coalescing operator ?? to create
// dictionary[i] if it doesn't already exist
dictionary[i] = dictionary[i] ?? [:]
// Use optional chaining to assign to inner dictionary
dictionary[i]?["name"] = name
dictionary[i]?["from"] = country
dictionary[i]?["age"] = age
print(dictionary)
Output:
["abc": ["age": "28", "from": "USA", "name": "Fred"]]
Using these techniques, here's my version of #matt's insert(_:value:at:) function:
func insert(key:String, value:String, at k:String) {
dictionary[k] = dictionary[k] ?? [:]
dictionary[k]?[key] = value
}
var sourceEntries: [Entry] = [entry1, ..., entry14]
var myDict: Dictionary<String, [Entry]> = [:]
for entry in sourceEntries {
if var array = myDict[entry.attribute1] { theArray.append(entry) }
else { myDict[entry.attribute1] = [entry] }
}
I am intending to create a Dictionary, which matches all the objects of the struct "Eintrag" with the same attribute from the source-Array "alleEinträge" to a String containing the value of the shared attribute. For some reason my final Dictionary just matches Arrays of one element to the Strings, although some Arrays ought to contain up to four elements.
The problem is that the array is passed by value (i.e. "copied"), so the array you are writing to when you say array.append is not the array that is "inside" the dictionary. You have to write back into the dictionary explicitly if you want to change what's in it.
Try it in a simple situation:
var dict = ["entry":[0,1,2]]
// your code
if var array = dict["entry"] { array.append(4) }
// so what happened?
println(dict) // [entry: [0, 1, 2]]
As you can see, the "4" never got into the dictionary.
You have to write back into the dictionary explicitly:
if var array = dict["entry"] { array.append(4); dict["entry"] = array }
FURTHER THOUGHTS: You got me thinking about whether there might be a more elegant way to do what you're trying to do. I'm not sure whether you will think this is "more elegant", but perhaps it has some appeal.
I will start by setting up a struct (like your Entry) with a name attribute:
struct Thing : Printable {
var name : String
var age : Int
var description : String {
return "{\(self.name), \(self.age)}"
}
}
Now I will create an array like your sourceEntries array, where some of the structs share the same name (like your shared attribute attribute1):
let t1 = Thing(name: "Jack", age: 40)
let t2 = Thing(name: "Jill", age: 38)
let t3 = Thing(name: "Jill", age: 37)
let arr = [t1,t2,t3]
And of course I will prepare the empty dictionary, like your myDict, which I call d:
var d = [String : [Thing]]()
Now I will create the dictionary! The idea is to use map and filter together to do all the work of creating key-value pairs, and then we just build the dictionary from those pairs:
let pairs : [(String, [Thing])] = arr.map {
t in (t.name, arr.filter{$0.name == t.name})
}
for pair in pairs { d[pair.0] = pair.1 }