Last active
April 14, 2023 12:45
-
-
Save dfrib/d7419038f7e680d3f268750d63f0dfae to your computer and use it in GitHub Desktop.
Swift: Reading and writing to (possible) nested dictionaries for a given key path, using a recursive approach
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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"] | |
] | |
] */ |
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) }
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
It doesn't if the key doesn't exist before. I fixed it here https://gist.github.com/yspreen/19b0264472af2a5739fcd048dd71c34c