5

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?

Just a coder
  • 15,480
  • 16
  • 85
  • 138
  • The problem with the kind of data structure you're proposing is that you'll need to handle situations where you have overlapping ranges as 'keys' – what value should a value in the intersection of those ranges be mapped to? – Hamish Feb 25 '17 at 20:15
  • Also note that, in the context of a dict., the hashvalue for an instance that conforms to `Hashable` is simply used to place the `keys` in different bins (based on the hashvalue), and only in case one tries access a bin with several keys will the dictionary proceed to test equality for the actual _unique_ key being searched for (all this with the purpose of allowing amortized O(1) access to values by keys in a dictionary). This means that (for `Range` theoretically being extended to `Hashable`), `0...10` is a unique key, and `1...10` is also a unique key, even if their hashvales are the same. – dfrib Feb 25 '17 at 20:18
  • 1
    Not specifically Swift, but [this Q&A](http://stackoverflow.com/q/2147505/2976878) will likely be a good starting place. – Hamish Feb 25 '17 at 20:20

2 Answers2

8

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])
Code Different
  • 90,614
  • 16
  • 144
  • 163
  • A good answer above, but might be good to explicitly point out that the value by key access when using the new subscript implementations above will be `O(n)` and not amortized `O(1)` (as one might expect the latter for dictionaries). Also, if you'd like to go all out functional you can condense the bodies of the `subscript` implementations above to single expressions: `return keys.first(where: { $0.1 ~= rawValue }).flatMap { self[$0.0] }` and `return ranges.enumerated().first(where: { $0.1 ~= value }).flatMap { descriptions[$0.0] }` for the top and bottom implementations, respectively. – dfrib Feb 25 '17 at 20:55
  • ... and comparing this to a binary search approach applied to a sorted list (I believe OP pointed out ranges where non-overlapping / unique) which would have `O(log n)` worst case performance for access. – dfrib Feb 25 '17 at 21:01
  • @dfri `O(log n)` hmm.. meaning the binary search approach would be faster. I'm looking to see how i can work with his solution but making the search binary. – Just a coder Feb 25 '17 at 21:02
  • Indeed, for access. But the binary search approach will need sorting (say, ~ `O(n log n)`) to initially construct the list of ranges (and `O(n)` for new insertions into the list). Also, the binary search approach is applicable only if the ranges are non-overlapping. In your custom approach above (second snippet), you could simply make sure that the `ranges` array is sorted, an apply binary search to it when calling `subscript`. You might want to ascertain, however, in som fashion, that all `ranges` are non-overlapping (`O(n)` to check), possibly for each new element added to `ranges`. – dfrib Feb 25 '17 at 21:05
  • ... since adding a new element to a sorted list (and keeping it sorted) is already `O(n)`, you can easily perform this non-overlapping check when inserting a new (`Range`) element (since you know the list is sorted). – dfrib Feb 25 '17 at 21:09
3

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"]
brimstone
  • 3,370
  • 3
  • 28
  • 49