Skip to content

Instantly share code, notes, and snippets.

@dfrib
Last active April 14, 2023 12:45
Show Gist options
  • Star 31 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save dfrib/d7419038f7e680d3f268750d63f0dfae to your computer and use it in GitHub Desktop.
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
// 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"]
]
] */
@karstengresch
Copy link

Even more background information (past SO solution): https://oleb.net/blog/2017/01/dictionary-key-paths/

@ioskunal
Copy link

ioskunal commented Apr 23, 2019

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"
                  ]
                ]
              ]  
          ]  
     ]
]

*/

Copy link

ghost commented May 27, 2020

Not support this kind of way with array index dict.valueForKeyPath("airports.germany[2]")

@yspreen
Copy link

yspreen commented Jul 23, 2020

I don't think setting a value via a keyPath actually works?

@yspreen
Copy link

yspreen commented Jul 23, 2020

It doesn't if the key doesn't exist before. I fixed it here https://gist.github.com/yspreen/19b0264472af2a5739fcd048dd71c34c

@armintelker
Copy link

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"))
    }
}

@MojtabaHs
Copy link

MojtabaHs commented Mar 7, 2023

@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