22

Is there a standard swift class that is a Dictionary, but keeps keys in insertion-order like Java's LinkedHashMap? If not, how would one be implemented?

Makyen
  • 31,849
  • 12
  • 86
  • 121
Heath Borders
  • 30,998
  • 16
  • 147
  • 256

2 Answers2

25

Didn't know of one and it was an interesting problem to solve (already put it in my standard library of stuff) Mostly it's just a matter of maintaining a dictionary and an array of the keys side-by-side. But standard operations like for (key, value) in od and for key in od.keys will iterate in insertion order rather than a semi random fashion.

// OrderedDictionary behaves like a Dictionary except that it maintains
//  the insertion order of the keys, so iteration order matches insertion
//  order.
struct OrderedDictionary<KeyType:Hashable, ValueType> {
    private var _dictionary:Dictionary<KeyType, ValueType>
    private var _keys:Array<KeyType>

    init() {
        _dictionary = [:]
        _keys = []
    }

    init(minimumCapacity:Int) {
        _dictionary = Dictionary<KeyType, ValueType>(minimumCapacity:minimumCapacity)
        _keys = Array<KeyType>()
    }

    init(_ dictionary:Dictionary<KeyType, ValueType>) {
        _dictionary = dictionary
        _keys = map(dictionary.keys) { $0 }
    }

    subscript(key:KeyType) -> ValueType? {
        get {
            return _dictionary[key]
        }
        set {
            if newValue == nil {
                self.removeValueForKey(key)
            }
            else {
                self.updateValue(newValue!, forKey: key)
            }
        }
    }

    mutating func updateValue(value:ValueType, forKey key:KeyType) -> ValueType? {
        let oldValue = _dictionary.updateValue(value, forKey: key)
        if oldValue == nil {
            _keys.append(key)
        }
        return oldValue
    }

    mutating func removeValueForKey(key:KeyType) {
        _keys = _keys.filter { $0 != key }
        _dictionary.removeValueForKey(key)
    }

    mutating func removeAll(keepCapacity:Int) {
        _keys = []
        _dictionary = Dictionary<KeyType,ValueType>(minimumCapacity: keepCapacity)
    }

    var count: Int { get { return _dictionary.count } }

    // keys isn't lazy evaluated because it's just an array anyway
    var keys:[KeyType] { get { return _keys } }

    // values is lazy evaluated because of the dictionary lookup and creating a new array
    var values:GeneratorOf<ValueType> {
        get {
            var index = 0
            return GeneratorOf<ValueType> {
                if index >= self._keys.count {
                    return nil
                }
                else {
                    let key = self._keys[index]
                    index++
                    return self._dictionary[key]
                }
            }
        }
    }
}

extension OrderedDictionary : SequenceType {
    func generate() -> GeneratorOf<(KeyType, ValueType)> {
        var index = 0
        return GeneratorOf<(KeyType, ValueType)> {
            if index >= self._keys.count {
                return nil
            }
            else {
                let key = self._keys[index]
                index++
                return (key, self._dictionary[key]!)
            }
        }
    }
}

func ==<Key: Equatable, Value: Equatable>(lhs: OrderedDictionary<Key, Value>, rhs: OrderedDictionary<Key, Value>) -> Bool {
    return lhs._keys == rhs._keys && lhs._dictionary == rhs._dictionary
}

func !=<Key: Equatable, Value: Equatable>(lhs: OrderedDictionary<Key, Value>, rhs: OrderedDictionary<Key, Value>) -> Bool {
    return lhs._keys != rhs._keys || lhs._dictionary != rhs._dictionary
}
David Berry
  • 40,941
  • 12
  • 84
  • 95
  • Looks great! Your `updateValue` method has an error—if a user calls that with a new key, `_keys` won't be updated. – Nate Cook Feb 20 '15 at 19:06
  • 2
    Upvoted, but I'd like to see one that is ideally interchangeable with regular `Dictionary` syntax and that extends `Dictionary` so that I can use it interchangeably with other code that requires a `Dictionary` (like `Alamofire`'s `Parameter` `enum`) – Heath Borders Feb 20 '15 at 20:11
  • I'm not sure that will be possible Heath, you can't subclass generics (and keep them generic) As far as interchangeable with regular `Dictionary` syntax, this should be a drop-in replacement except for typing, and of course, since it's not a sub-class, you can't pass it around as a Dictionary. – David Berry Feb 20 '15 at 20:56
  • @David wouldn't be simplier if in the init method, you just assign the keys instead of using a map function? – Dániel Nagy Feb 20 '15 at 22:18
  • @DánielNagy The `keys` property of a dictionary isn't an array, it's a lazily mapped collection (mapped from the dictionary's sequence of key/value pairs). So you can't assign directly. – Nate Cook Feb 21 '15 at 01:03
  • @DavidBerry, you're correct. That isn't possible: http://stackoverflow.com/a/28681649/9636 – Heath Borders Feb 23 '15 at 19:51
  • Unfortunately, combining an `Array` with a `Dictionary` as a means of implementing an ordered associative data structure is slower than using a linked structure, like `LinkedHashMap`. For one, deletions are `O(n)` on average, whereas a `LinkedHashMap` has `O(1)` deletions. – Alexander Jun 28 '17 at 18:28
  • No argument @Alexander, however, writing a full implementation of LinkedHashMap or something similar may very well be a case of premature optimization if all you need is to store a few keys, particularly if you rarely if ever delete any. – David Berry Jun 28 '17 at 22:29
  • @DavidBerry Agreed, which is why I would defer to a library who has already made a fully-fleshed out implementation, rather than rolling my own – Alexander Jun 28 '17 at 23:03
  • You should add this to raywenderlich/swift-algorithm-club https://github.com/raywenderlich/swift-algorithm-club – mretondo Aug 07 '18 at 20:23
1

Swift 5 version:

// OrderedDictionary behaves like a Dictionary except that it maintains
//  the insertion order of the keys, so iteration order matches insertion
//  order.
struct OrderedDictionary<KeyType: Hashable, ValueType> {
    private var _dictionary: Dictionary<KeyType, ValueType>
    private var _keys: Array<KeyType>

    init() {
        _dictionary = [:]
        _keys = []
    }

    init(minimumCapacity: Int) {
        _dictionary = Dictionary<KeyType, ValueType>(minimumCapacity: minimumCapacity)
        _keys = Array<KeyType>()
    }

    init(_ dictionary: Dictionary<KeyType, ValueType>) {
        _dictionary = dictionary
        _keys = dictionary.keys.map { $0 }
    }

    subscript(key: KeyType) -> ValueType? {
        get {
            _dictionary[key]
        }
        set {
            if newValue == nil {
                self.removeValueForKey(key: key)
            } else {
                _ = self.updateValue(value: newValue!, forKey: key)
            }
        }
    }

    mutating func updateValue(value: ValueType, forKey key: KeyType) -> ValueType? {
        let oldValue = _dictionary.updateValue(value, forKey: key)
        if oldValue == nil {
            _keys.append(key)
        }
        return oldValue
    }

    mutating func removeValueForKey(key: KeyType) {
        _keys = _keys.filter {
            $0 != key
        }
        _dictionary.removeValue(forKey: key)
    }

    mutating func removeAll(keepCapacity: Int) {
        _keys = []
        _dictionary = Dictionary<KeyType, ValueType>(minimumCapacity: keepCapacity)
    }

    var count: Int {
        get {
            _dictionary.count
        }
    }

    // keys isn't lazy evaluated because it's just an array anyway
    var keys: [KeyType] {
        get {
            _keys
        }
    }

    var values: Array<ValueType> {
        get {
            _keys.map { _dictionary[$0]! }
        }
    }

    static func ==<Key: Equatable, Value: Equatable>(lhs: OrderedDictionary<Key, Value>, rhs: OrderedDictionary<Key, Value>) -> Bool {
        lhs._keys == rhs._keys && lhs._dictionary == rhs._dictionary
    }

    static func !=<Key: Equatable, Value: Equatable>(lhs: OrderedDictionary<Key, Value>, rhs: OrderedDictionary<Key, Value>) -> Bool {
        lhs._keys != rhs._keys || lhs._dictionary != rhs._dictionary
    }

}

extension OrderedDictionary: Sequence {

    public func makeIterator() -> OrderedDictionaryIterator<KeyType, ValueType> {
        OrderedDictionaryIterator<KeyType, ValueType>(sequence: _dictionary, keys: _keys, current: 0)
    }

}

struct OrderedDictionaryIterator<KeyType: Hashable, ValueType>: IteratorProtocol {
    let sequence: Dictionary<KeyType, ValueType>
    let keys: Array<KeyType>
    var current = 0

    mutating func next() -> (KeyType, ValueType)? {
        defer { current += 1 }
        guard sequence.count > current else {
            return nil
        }

        let key = keys[current]
        guard let value = sequence[key] else {
            return nil
        }
        return (key, value)
    }

}

I didn't found way to make values 'lazy'.. need more research

trickster77777
  • 1,198
  • 2
  • 19
  • 30