Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to have a range as a key in a Swift Dictionary?

Tags:

generics

swift

For simplification. Lets say i have some unique values -> the numbers from 1 to 10

Now I want 1-5 map to the value "first" and I want 6-10 map to the value "second"

Is there a way I can create or extend a dictionary to work like the following?

let dict: [Range<Int> : String]

The goal is to have the following results:

print(dict[1]) // prints first
print(dict[2]) // prints first
print(dict[3]) // prints first
print(dict[7]) // prints second
print(dict[8]) // prints second
print(dict[9]) // prints second

The way I am currently doing it is to simply have the multiple keys map to the same value. But my dictionary can have sometimes 60k values. So I am wondering if a range can work.

I know I can make the value into a class instead of a struct so that multiple keys can map to the same class object, but I was wondering if simply creating a Dictionary that worked like above was possible?

like image 456
Just a coder Avatar asked Feb 25 '17 19:02

Just a coder


2 Answers

If you insist on using Dictionary, you have to wait until Swift 3.1 (currently in beta):

extension CountableClosedRange : Hashable {
    public var hashValue: Int {
        return "\(lowerBound) to \(upperBound)".hashValue
    }
}

// This feature is called concrete-type extension and requires Swift 3.1
extension Dictionary where Key == CountableClosedRange<Int> {
    subscript(rawValue rawValue: Int) -> Value? {
        for k in self.keys {
            if k ~= rawValue {
                return self[k]
            }
        }

        return nil
    }
}

let dict : [CountableClosedRange<Int>: String] = [
    1...5: "first",
    6...10: "second"
]

print(dict[rawValue: 1])
print(dict[rawValue: 2])
print(dict[rawValue: 3])
print(dict[rawValue: 7])
print(dict[rawValue: 8])
print(dict[rawValue: 9])

However, it's a lot clearer if you implement your own data model:

struct MyRange {
    var ranges = [CountableClosedRange<Int>]()
    var descriptions = [String]()

    mutating func append(range: CountableClosedRange<Int>, description: String) {
        // You can check for overlapping range here if you want
        self.ranges.append(range)
        self.descriptions.append(description)
    }

    subscript(value: Int) -> String? {
        for (i, range) in self.ranges.enumerated() {
            if range ~= value {
                return descriptions[i]
            }
        }

        return nil
    }
}

var range = MyRange()
range.append(range: 1...5, description: "one")
range.append(range: 6...10, description: "second")

print(range[1])
print(range[2])
print(range[6])
print(range[7])
print(range[100])
like image 181
Code Different Avatar answered Oct 29 '22 01:10

Code Different


This is in Swift 3.0, it may not be as nice as Code Different's answer though.

class MyRange: Hashable, Equatable {
    public var hashValue: Int {
        get {
            return (self.range.lowerBound + self.range.upperBound).hashValue
        }
    }

    var range: Range<Int>!

    public static func ==(_ lhs: MyRange, _ rhs: MyRange) -> Bool {
        return lhs.range == rhs.range
    }

    init(range: Range<Int>) {
        self.range = range
    }
}


extension Dictionary where Key: MyRange, Value: ExpressibleByStringLiteral {
    internal subscript(index: Int) -> [String] {
        return self.filter({$0.key.range.contains(index)}).map({$0.value as! String})
    }
}

Now, you can make your dictionary like so:

var dict = Dictionary<MyRange, String>()
dict[MyRange(range: 0..<5)] = "first"
dict[MyRange(range: 5..<10)] = "second"

Getting values works with Integers and Ranges:

print(dict[1]) // ["first"]
print(dict[5]) // ["second"]
print(dict[11]) // []

print(dict[MyRange(range: 0..<5)]) // "first"
print(dict[MyRange(range: 0..<6)]) // nil

The dictionary should look like this:

print(dict)
// [MyRange: "first", MyRange: "second"]
like image 43
brimstone Avatar answered Oct 29 '22 01:10

brimstone