-
-
Save dfrib/d7419038f7e680d3f268750d63f0dfae to your computer and use it in GitHub Desktop.
// For details, see | |
// http://stackoverflow.com/questions/40261857/remove-nested-key-from-dictionary | |
import Foundation | |
extension Dictionary { | |
subscript(keyPath keyPath: String) -> Any? { | |
get { | |
guard let keyPath = Dictionary.keyPathKeys(forKeyPath: keyPath) | |
else { return nil } | |
return getValue(forKeyPath: keyPath) | |
} | |
set { | |
guard let keyPath = Dictionary.keyPathKeys(forKeyPath: keyPath), | |
let newValue = newValue else { return } | |
self.setValue(newValue, forKeyPath: keyPath) | |
} | |
} | |
static private func keyPathKeys(forKeyPath: String) -> [Key]? { | |
let keys = forKeyPath.components(separatedBy: ".") | |
.reversed().flatMap({ $0 as? Key }) | |
return keys.isEmpty ? nil : keys | |
} | |
// recursively (attempt to) access queried subdictionaries | |
// (keyPath will never be empty here; the explicit unwrapping is safe) | |
private func getValue(forKeyPath keyPath: [Key]) -> Any? { | |
guard let value = self[keyPath.last!] else { return nil } | |
return keyPath.count == 1 ? value : (value as? [Key: Any]) | |
.flatMap { $0.getValue(forKeyPath: Array(keyPath.dropLast())) } | |
} | |
// recursively (attempt to) access the queried subdictionaries to | |
// finally replace the "inner value", given that the key path is valid | |
private mutating func setValue(_ value: Any, forKeyPath keyPath: [Key]) { | |
guard self[keyPath.last!] != nil else { return } | |
if keyPath.count == 1 { | |
(value as? Value).map { self[keyPath.last!] = $0 } | |
} | |
else if var subDict = self[keyPath.last!] as? [Key: Value] { | |
subDict.setValue(value, forKeyPath: Array(keyPath.dropLast())) | |
(subDict as? Value).map { self[keyPath.last!] = $0 } | |
} | |
} | |
} | |
/* ------------------------------------------------------------------ */ | |
// example usage | |
var dict: [String: Any] = [ | |
"countries": [ | |
"japan": [ | |
"capital": [ | |
"name": "tokyo", | |
"lat": "35.6895", | |
"lon": "139.6917" | |
], | |
"language": "japanese" | |
] | |
], | |
"airports": [ | |
"germany": ["FRA", "MUC", "HAM", "TXL"] | |
] | |
] | |
// read value for a given key path | |
let isNil: Any = "nil" | |
print(dict[keyPath: "countries.japan.capital.name"] ?? isNil) // tokyo | |
print(dict[keyPath: "airports"] ?? isNil) // ["germany": ["FRA", "MUC", "HAM", "TXL"]] | |
print(dict[keyPath: "this.is.not.a.valid.key.path"] ?? isNil) // nil | |
// write value for a given key path | |
dict[keyPath: "countries.japan.language"] = "nihongo" | |
print(dict[keyPath: "countries.japan.language"] ?? isNil) // nihongo | |
dict[keyPath: "airports.germany"] = | |
(dict[keyPath: "airports.germany"] as? [Any] ?? []) + ["FOO"] | |
dict[keyPath: "this.is.not.a.valid.key.path"] = "notAdded" | |
print(dict) | |
/* [ | |
"countries": [ | |
"japan": [ | |
"capital": [ | |
"name": "tokyo", | |
"lon": "139.6917", | |
"lat": "35.6895" | |
], | |
"language": "nihongo" | |
] | |
], | |
"airports": [ | |
"germany": ["FRA", "MUC", "HAM", "TXL", "FOO"] | |
] | |
] */ |
is it possible to create a dictionary from keypath ?
For instance in your last example you have dict[keyPath: "this.is.not.a.valid.key.path"] = "notAdded"
Would it be possible to update the result to
/*
[
"countries": [
"japan": [
"capital": [
"name": "tokyo",
"lon": "139.6917",
"lat": "35.6895"
],
"language": "nihongo"
]
],
"airports": [
"germany": ["FRA", "MUC", "HAM", "TXL", "FOO"]
],
"this": [
"is" : [
"not" : [
"a" : [
"valid" : [
"path" : "notAdded"
]
]
]
]
]
]
*/
Not support this kind of way with array index dict.valueForKeyPath("airports.germany[2]")
I don't think setting a value via a keyPath actually works?
It doesn't if the key doesn't exist before. I fixed it here https://gist.github.com/yspreen/19b0264472af2a5739fcd048dd71c34c
I adapted the code so that the setter
can also write into complex and simple arrays. The getter is untouched because I just needed to write something but maybe someone who needs it can it take it from there :)
dictionary[keyPath: "aKey.0"] = ""
dictionary[keyPath: "aKey.0.otherKey"] = ""
import Foundation
extension Dictionary {
subscript(keyPath keyPath: String) -> Any? {
get {
guard let keyPath = Dictionary.keyPathKeys(forKeyPath: keyPath) else { return nil }
return getValue(forKeyPath: keyPath)
}
set {
guard let keyPath = Dictionary.keyPathKeys(forKeyPath: keyPath), let newValue = newValue else { return }
self.setValue(newValue, forKeyPath: keyPath)
}
}
static private func keyPathKeys(forKeyPath: String) -> [Key]? {
let keys = forKeyPath.components(separatedBy: ".").compactMap({ $0 as? Key })
return keys.isEmpty ? nil : keys
}
private func getValue(forKeyPath keyPath: [Key]) -> Any? {
guard let value = self[keyPath.first!] else { return nil }
return keyPath.count == 1 ? value : (value as? [Key: Any]).flatMap { $0.getValue(forKeyPath: Array(keyPath.dropFirst())) }
}
private mutating func setValue(_ value: Any, forKeyPath keyPath: [Key]) {
if keyPath.count == 1 {
self[keyPath.first!] = value as? Value
} else {
if self[keyPath.first!] == nil {
self[keyPath.first!] = ([Key: Value]() as? Value)
return
}
if var subDict = self[keyPath.first!] as? [Key: Value] {
subDict.setValue(value, forKeyPath: Array(keyPath.dropFirst()))
self[keyPath.first!] = subDict as? Value
return
}
if var array = self[keyPath.first!] as? [[Key: Value]] {
if let key = keyPath.dropFirst().first as? String, key.isNumber, let index = Int(key) {
array[index].setValue(value, forKeyPath: Array(keyPath.dropFirst().dropFirst()))
self[keyPath.first!] = array as? Value
}
return
}
if var array = self[keyPath.first!] as? [Value] {
if let key = keyPath.dropFirst().first as? String, key.isNumber, let index = Int(key) {
array[index] = value as! Value
self[keyPath.first!] = array as? Value
}
return
}
}
}
}
extension String {
var isNumber: Bool {
CharacterSet(charactersIn: self).isSubset(of: CharacterSet(charactersIn: "0123456789"))
}
}
@armintelker You can update this:
extension String { var isNumber: Bool { CharacterSet(charactersIn: self).isSubset(of: CharacterSet(charactersIn: "0123456789")) } }
to var isNumber: Bool { allSatisfy(\.isNumber) }
Even more background information (past SO solution): https://oleb.net/blog/2017/01/dictionary-key-paths/