Is it possible to have a range as a key in a Swift Dictionary? - 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?

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

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

Related

How can I implement a CountedSet (NSCountedSet) in Swift?

Create a generic CountedSet struct that is constrained to Hashable elements. A counted set is an unordered collection of unique elements that may appear more than once in the collection. Use a private dictionary as your backing storage for set members and their counts.
struct CountedSet<Element> {
private(set) var elements: [Element]
mutating func insert(_ element: Element) {
elements.append(element)
}
mutating func remove() -> Element? {
guard elements.isEmpty == false else { return nil}
return elements.removeFirst()
}
subscript(_ member: Element) -> Int {
return 0
}
}
I don't understand what the real objective is here. The instructions are very confusing at least to me.
1) Make your generic struct element conform to Hashable, this is necessary because the dictionary keys are required to conform to Hashable.
struct CountedSet<Element: Hashable>
2) The backing storage you have used is an ordered array, not a dictionary and you need to initialize it with an empty one.
private(set) var elements: [Element: Int] = [:]
3) Your subscript method you need to return the count for the counted set member or zero if it is nil.
return elements[member] ?? 0
4) Your Insert and Remove methods need to first check the count of a member in the backing dictionary before adding or removing an element from it.
So your CountedSet should look like this:
struct CountedSet<Element: Hashable> {
private(set) var elements: [Element: Int] = [:]
mutating func insert(_ member: Element) {
elements[member, default: 0] += 1
}
mutating func remove(_ member: Element) -> Element? {
guard var count = elements[member], count > 0 else { return nil }
count -= 1
elements[member] = count == 0 ? nil : count
return member
}
subscript(_ member: Element) -> Int {
elements[member] ?? 0
}
}
var countedSet = CountedSet<Int>()
countedSet.insert(3)
countedSet.insert(3)
countedSet.insert(4)
countedSet.elements // [4: 1, 3: 2]
countedSet.remove(4)
countedSet.elements // [3: 2]
countedSet.remove(4) // nil
Expanding on that you can also make your CountedSet conform to ExpressibleByArrayLiteral to allow you to initialize your CountedSet with an array and CustomStringConvertible to allow you to print its elements:
extension CountedSet: ExpressibleByArrayLiteral, CustomStringConvertible {
typealias ArrayLiteralElement = Element
init<S: Sequence>(_ sequence: S) where S.Element == Element {
self.elements = sequence.reduce(into: [:]) { $0[$1, default: 0] += 1 }
}
init(arrayLiteral elements: Element...) { self.init(elements) }
var description: String { .init(describing: elements) }
}
var countedSet: CountedSet = [1,2,2,3,3,3,4,4,5,5,5]
print(countedSet) // "[5: 3, 2: 2, 3: 3, 4: 2, 1: 1]\n"

How to filter objects of certain type from [AnyObject] array

Is it possible to filter an array of [AnyObject] to yield all elements of a given type, and none other?
I can do it if the type is known at compile time:
class MyClass1: CustomStringConvertible {
var value: Int
var description: String {
return "MyClass1: \(value)"
}
init(_ value: Int) {
self.value = value
}
}
class MyClass2: CustomStringConvertible {
var value: Int
var description: String {
return "MyClass1: \(value)"
}
init(_ value: Int) {
self.value = value
}
}
class MySubClass1: MyClass1 {
override var description: String {
return "MySubClass1: \(value)"
}
}
let a1 = MySubClass1(1)
let a2 = MySubClass1(2)
let b1 = MyClass1(3)
let b2 = MyClass2(4)
let array: [AnyObject] = [a1, b1, a2, b2]
func getClass1ObjectsFromArray(_ array: [AnyObject]) -> [MyClass1] {
return array.compactMap( { $0 as? MyClass1 })
}
func getSubClass1ObjectsFromArray(_ array: [AnyObject]) -> [MySubClass1] {
return array.compactMap( { $0 as? MySubClass1 })
}
print(getClass1ObjectsFromArray(array))
print(getSubClass1ObjectsFromArray(array))
Prints:
[MySubClass1: 1, MyClass1: 3, MySubClass1: 2]
[MySubClass1: 1, MySubClass1: 2]
For every type I want to filter on, I had to write a separate function. This looks ugly to me, and will not work when the type to be selected for is only known at run time.
Question:
Is there a generic way to write such a function? Preferably something like:
func getObjectsOfType(_ type: TypeExpression, fromArray array: [AnyObject])
-> [TypeExpression] {
...
}
Or any other way to achieve this?
Thanks for any help!
I think you could use something like this...
let filteredArray = array.compactMap { $0 as? RequiredType }
This will filter the array and return a typed array containing only the type you want.
Caveat
Having said that. In Swift you should be avoiding heterogeneous arrays where possible. Arrays should really only contain one type of item.
A bit of code testing...
Tested in Playground...
let array: [Any] = [1, "hello", 3, 3.1415, "world"]
let filteredArray = array.compactMap { $0 as? String }
filteredArray
Output:
filteredArray = ["hello", "world"]
👍🏻
Edit 1
You could also create a generic function something like this...
func filter<T>(array: [Any]) -> [T] {
return array.compactMap { $0 as? T }
}
let filteredArray: [String] = filter(array: array)
This will then filter based on the type of the output array that you want.
I'm not sure what you mean by only knowing the type you want at run time. Can you give a more concrete example of what you mean?
Edit 2
Another possibility is a generic function like this...
func filter<T>(array: [Any], byType typeObject: T) -> [T] {
return array.compactMap { $0 as? T }
}
let filteredArray = filter(array: array, byType: "some string")
This uses the type information of the second parameter to filter the array by that type of item.
Edit 3
If you don't like passing in an instance of the type then you can pass the type itself...
func filter<T>(array: [Any], byType typeObject: T.Type) -> [T] {
return array.compactMap { $0 as? T }
}
let filteredArray = filter(array: array, byType: String.self)
But I'm not sure what more you're getting from this than just filtering by string in the first place?

Custom comparator for Swift

This is my code (simplified code):
struct SomeStruct {
let id: Int
let age: Int
}
extension SomeStruct: Hashable {
var hashValue: Int {
return id.hashValue * age.hashValue
}
static func ==(lhs: SomeStruct, rhs: SomeStruct) -> Bool {
return lhs.id == rhs.id && lhs.age == rhs.age
}
}
struct Calculator {
let struct1: [SomeStruct]
let struct2: [SomeStruct]
func uniqueById() {
let struct3 = Set(struct2).union(Set(struct1))
// I want to union it by property 'id' only.
// If the property 'id' is equal for both objects,
// the object in struct2 should be used (since that can have a different age property)
}
}
SomeStruct is a generated struct which I do not want to edit. I want to create a Set for SomeStruct that is based on 1 property: id. For that, I think I need a custom Comparator, just as Java has. Is there any Swifty way? This is the only thing I can come up with, but I am wondering if there is a better way:
struct SomeStructComparatorById: Hashable {
let someStruct: SomeStruct
var hashValue: Int {
return someStruct.id.hashValue
}
static func ==(lhs: SomeStructComparatorById, rhs: SomeStructComparatorById) -> Bool {
return lhs.someStruct.id == rhs.someStruct.id
}
}
First, I don't think this would work in Java. addAll() doesn't take a Comparator (nor does contains, etc.) Comparators are for sorting, not equality. Conceptually this is breaking how Set works in any language. Two items are not "equal" unless they can be swapped in all cases.
That tells us that we don't want a Set here. What you want here is uniqueness based on some key. That's a Dictionary (as Daniel discusses).
You could either just have a "id -> age" dictionary or "id -> struct-of-other-properties" dictionary as your primary data type (rather than using Array). Or you can turn your Array into a temporary Dictionary like this:
extension Dictionary {
init<S>(_ values: S, uniquelyKeyedBy keyPath: KeyPath<S.Element, Key>)
where S : Sequence, S.Element == Value {
let keys = values.map { $0[keyPath: keyPath] }
self.init(uniqueKeysWithValues: zip(keys, values))
}
}
And merge them like this:
let dict1 = Dictionary(struct1, uniquelyKeyedBy: \.id)
let dict2 = Dictionary(struct2, uniquelyKeyedBy: \.id)
let merged = dict1.merging(dict2, uniquingKeysWith: { old, new in old }).values
This leaves merged as [SomeStruct].
Note that this Dictionary(uniquelyKeyedBy:) has the same preconditions as Dictionary(uniqueKeysWithValues:). If there are duplicate keys, it's a programming error and will raise precondition failure.
You could do something like this:
var setOfIds: Set<Int> = []
var struct3 = struct2.filter { setOfIds.insert($0.id).inserted }
struct3 += struct1.filter { setOfIds.insert($0.id).inserted }
The result would be an array of SomeStruct, with all elements with unique ids.
You could define this as a custom operator :
infix operator *>
func *> (lhs: [SomeStruct], rhs: [SomeStruct]) -> [SomeStruct] {
var setOfIds: Set<Int> = []
var union = lhs.filter { setOfIds.insert($0.id).inserted }
union += rhs.filter { setOfIds.insert($0.id).inserted }
return union
}
Your code would then look like this:
func uniqueById() {
let struct3 = struct2 *> struct1
//use struct3
}
The short answer is no. Swift sets do not have any way to accept a custom comparator and if you absolutely must have a Set, then your wrapper idea is the only way to do it. I question the requirement for a set though.
Instead of using Set in your calculator, I recommend using dictionary.
You can use a Dictionary to produce an array where each item has a unique ID...
let struct3 = Dictionary(grouping: struct1 + struct2, by: { $0.id })
.compactMap { $0.value.max(by: { $0.age < $1.age })}
Or you can keep the elements in a [Int: SomeStruct] dictionary:
let keysAndValues = (struct1 + struct2).map { ($0.id, $0) }
let dictionary = Dictionary(keysAndValues, uniquingKeysWith: { lhs, rhs in
lhs.age > rhs.age ? lhs : rhs
})

Swift sorting on arbitrary types

I have a Set of instances of type Thingie, and I want to provide arrays of Thingies sorted on any property of Thingie. Some of the properties are Int, for instance, while others are String, and there could be others. So I wanted to create a sort routine that accepts a string as the name of the property and compares the two properties of two thingies to determine the order.
It seemed like a job for generics, and I'm getting close, but there's a hole.
Here's where I'm at right now:
func compare<T:Comparable>(lft: T, _ rgt: T) -> Bool {
return lft < rgt
}
func orderBy(sortField: String) -> [Thingie] {
let allArray = (self.thingies as NSSet).allObjects as! [Thingie]
//typealias T = the type of allArray[0][sortField]
// or maybe create an alias that conforms to a protocol:
//typealias T:Comparable = ?
return allArray.sort({(a, b) -> Bool in
return self.compare(a[sortField] as! T, b[sortField] as! T)
})
}
I created a compare function using generics, and invoke it in my sort routine. The catch is that AnyObject! will not work for my generic, so I need to cast the values returned from a[sortField] and b[sortField] to be of the same type. It doesn't even really matter what type as long as the compiler is happy that both values are of the same type and that it implements the Comparable protocol.
I figured a typealias would do the trick, but maybe there's a better way?
Side question: surely there's a better way to create the initial, unsorted array from the set without resorting to NSSet. A little hint would be welcome. [Solved that bit! Thanks, Oliver Atkinson!]
Here's a big 'ol chunk of code you can paste into a playground. It has three attempts at the orderBy implementation, each with a problem.
//: Playground - noun: a place where people can play
import Foundation
class Thingie: Hashable {
var data: [String: AnyObject]
var hashValue: Int
init(data: [String: AnyObject]) {
self.data = data
self.hashValue = (data["id"])!.hashValue
}
subscript(propName: String) -> AnyObject! {
return self.data[propName]
}
}
func ==(lhs: Thingie, rhs: Thingie) -> Bool {
return lhs.hashValue == rhs.hashValue
}
var thingies: Set = Set<Thingie>()
thingies.insert(Thingie(data: ["id": 2, "description": "two"]));
thingies.insert(Thingie(data: ["id": 11, "description": "eleven"]));
// attempt 1
// won't compile because '<' won't work when type is ambiguous e.g., AnyObject
func orderByField1(sortField: String) -> [Thingie] {
return thingies.sort { $0[sortField] < $1[sortField] }
}
// compare function that promises the compiler that the operands for < will be of the same type:
func compare<T:Comparable>(lft: T, _ rgt: T) -> Bool {
return lft < rgt
}
// attempt 2
// This compiles but will bomb at runtime if Thingie[sortField] is not a string
func orderByField2(sortField: String) -> [Thingie] {
return thingies.sort { compare($0[sortField] as! String, $1[sortField] as! String) }
}
// attempt 3
// Something like this would be ideal, but protocol Comparable can't be used like this.
// I suspect the underlying reason that Comparable can't be used as a type is the same thing preventing me from making this work.
func orderByField3(sortField: String) -> [Thingie] {
return thingies.sort { compare($0[sortField] as! Comparable, $1[sortField] as! Comparable) }
}
// tests - can't run until a compiling candidate is written, of course
// should return array with thingie id=2 first:
var thingieList: Array = orderByField2("id");
print(thingieList[0]["id"])
// should return array with thingie id=11 first:
var thingieList2: Array = orderByField2("description");
print(thingieList2[0]["id"])
My previous answer, though it works, does not make the most of the Swift's excellent type checker. It also switches between the types that can be used in one centralised place which limits extensibility to the framework owner.
The following approach solves these issues. (Please forgive me for not having the heart to delete my previous answer; let us say that it's limitations are instructive...)
As before, we'll start with the target API:
struct Thing : ThingType {
let properties: [String:Sortable]
subscript(key: String) -> Sortable? {
return properties[key]
}
}
let data: [[String:Sortable]] = [
["id": 1, "description": "one"],
["id": 2, "description": "two"],
["id": 3, "description": "three"],
["id": 4, "description": "four"],
["id": 4, "description": "four"]
]
var things = data.map(Thing.init)
things.sortInPlaceBy("id")
things
.map{ $0["id"]! } // [1, 2, 3, 4]
things.sortInPlaceBy("description")
things
.map{ $0["description"]! } // ["four", "one", "three", "two"]
To make this possible we must have this ThingType protocol and an extension to mutable collections (which will work for sets as well as arrays):
protocol ThingType {
subscript(_: String) -> Sortable? { get }
}
extension MutableCollectionType
where Index : RandomAccessIndexType, Generator.Element : ThingType
{
mutating func sortInPlaceBy(key: String, ascending: Bool = true) {
sortInPlace {
guard let lhs = $0[key], let rhs = $1[key] else {
return false // TODO: nil handling
}
guard let b = (try? lhs.isOrderedBefore(rhs, ascending: ascending)) else {
return false // TODO: handle SortableError
}
return b
}
}
}
Evidently, the whole idea revolves around this Sortable protocol:
protocol Sortable {
func isOrderedBefore(_: Sortable, ascending: Bool) throws -> Bool
}
... which can be conformed to independently by any type we want to work with:
import Foundation
extension NSNumber : Sortable {
func isOrderedBefore(other: Sortable, ascending: Bool) throws -> Bool {
try throwIfTypeNotEqualTo(other)
let f: (Double, Double) -> Bool = ascending ? (<) : (>)
return f(doubleValue, (other as! NSNumber).doubleValue)
}
}
extension NSString : Sortable {
func isOrderedBefore(other: Sortable, ascending: Bool) throws -> Bool {
try throwIfTypeNotEqualTo(other)
let f: (String, String) -> Bool = ascending ? (<) : (>)
return f(self as String, other as! String)
}
}
// TODO: make more types Sortable (including those that do not conform to NSObject or even AnyObject)!
This throwIfTypeNotEqualTo method is just a convenience extension of Sortable:
enum SortableError : ErrorType {
case TypesNotEqual
}
extension Sortable {
func throwIfTypeNotEqualTo(other: Sortable) throws {
guard other.dynamicType == self.dynamicType else {
throw SortableError.TypesNotEqual
}
}
}
And that's it. Now we can conform new types to Sortable even outside of the framework and the type checker is validating our [[String:Sortable]] source data at compile time. Also, if Thing is extended to conform to Hashable then Set<Thing> will also be sortable by key...
Note that, although Sortable is itself unconstrained (which is awesome), source data and Thing's properties can be constrained to dictionaries with NSObject or AnyObject values if required by making use of a protocol like:
protocol SortableNSObjectType : Sortable, NSObjectProtocol { }
... or more directly by declaring data and Thing's properties as:
let _: [String : protocol<Sortable, NSObjectProtocol>]
I don't know the implementation of Thingie but maybe you could provide more context.
You could however go for something like this
func orderBy(sortField: String) -> [Thingie] {
return thingies.allObjects.map { $0 as! Thingie }.sort { $0[sortField] < $1[sortField] }
}
If you could provide a playground example so I can provide further help.
Also why did you use NSSet rather than a swift Set? would that give you what you want
let thingies: Set = Set<Thingie>()
func orderBy(sortField: String) -> [Thingie] {
return thingies.sort { $0[sortField] < $1[sortField] }
}
edit:
The trouble is with swift's type safety - it requires you to know what types you are dealing with so that it can compile correctly - if you specify the actual type when you want to order the field you can get it to work as expected.
func orderByField<T: Comparable>(sortField: String, type: T.Type) -> [Thingie] {
return thingies.sort { ($0[sortField] as? T) < ($1[sortField] as? T) }
}
var thingieList: Array = orderByField("id", type: Int.self);
print(thingieList[0]["id"])
var thingieList2: Array = orderByField("description", type: String.self);
print(thingieList2[0]["id"])
The above will print 2 then 11 - if you wanted to get around this you could store your objects in a different struct and then you can sort the array of 'Things' on the variable.
e.g.
struct Thing {
let id: Int
let description: String
}
var data: [Thing] = [
Thing(id: 2, description: "two"),
Thing(id: 11, description: "eleven")
]
let first = data.sort { $0.id < $1.id }.first?.id
let second = data.sort { $0.description < $1.description }.first?.id
print(first)
print(second)
Which would achieve the same thing - 2 and 11
I would advise against using AnyObject where possible as its trying to cheat the compiler into telling it you don't care for its help.
Its an interesting problem though and I hope this helps you towards your solution.
I will start with the target API (ignoring conformance to Hashable as its addition wont change anything in what follows). So, let's say we'd like to be able to write the following:
var thingies = [
["id": 1, "description": "one"],
["id": 2, "description": "two"],
["id": 3, "description": "three"],
["id": 4, "description": "four"]
].map(Thingie.init)
thingies.sortInPlace{ $0["id"] < $1["id"] }
... and even:
thingies.sortInPlaceBy("id")
thingies
.map{ $0["id"]!.value } // [1, 2, 3, 4]
thingies.sortInPlaceBy("description")
thingies
.map{ $0["description"]!.value } // ["four", "one", "three", "two"]
Obviously, we'd need an extension of MutableCollectionType protocol along the lines of:
protocol ThingieDatumSubscriptable {
subscript(_: String) -> ThingieDatum? { get }
}
extension Thingie : ThingieDatumSubscriptable {}
extension MutableCollectionType
where Index : RandomAccessIndexType, Generator.Element : ThingieDatumSubscriptable
{
mutating func sortInPlaceBy(datumName: String, ascending: Bool = true) {
let f: (ThingieDatum?, ThingieDatum?) -> Bool = ascending ? (<) : (>)
sortInPlace{ f($0[datumName], $1[datumName]) }
}
}
This ThingieDatum would then be something like:
import Foundation
struct ThingieDatum : Comparable {
let type: AnyObject.Type
let value: AnyObject
let name: String
init(keyValuePair: (String, AnyObject)) {
name = keyValuePair.0
value = keyValuePair.1
type = keyValuePair.1.dynamicType
}
}
... and its conformance to Comparable implemented in some sort of pedestrian way as follows (unless we introduce more protocols):
func == (lhs: ThingieDatum, rhs: ThingieDatum) -> Bool {
guard lhs.name == rhs.name && lhs.type == rhs.type else {
return false
}
switch lhs.type {
// TODO: implement for other types
case is NSNumber.Type: return lhs.value as! NSNumber == rhs.value as! NSNumber
case is NSString.Type: return (lhs.value as! String) == (rhs.value as! String)
default: break
}
return false
}
func < (lhs: ThingieDatum, rhs: ThingieDatum) -> Bool {
assert(lhs.name == rhs.name && lhs.type == rhs.type)
switch lhs.type {
// TODO: implement for other types
case is NSNumber.Type: return (lhs.value as! NSNumber).doubleValue < (rhs.value as! NSNumber).doubleValue
case is NSString.Type: return (lhs.value as! String) < (rhs.value as! String)
default: break
}
return false
}
Armed with such a ThingieDatum we can finally work out the Thingie itself:
struct Thingie {
var data: [ThingieDatum]
init(_ data: [String: AnyObject]) {
self.data = data.map(ThingieDatum.init)
}
subscript(datumName: String) -> ThingieDatum? {
for datum in data where datum.name == datumName {
return datum
}
return nil
}
}
And although this is, of course, all meant as a fun exercise, it does work (copy and paste into the playground if you can work our the correct order of snippets)... To take this idea further, however, we would probably want to constrain ThingiDatum initialiser to a custom protocol (rather than AnyObject), which would guarantee comparability. We would then conform to that protocol with each type we want to work with instead of switching through those types in one centralised place...

Case insensitive Dictionary in Swift

Given a Dictionary whose Key is of type String, is there a way to access the value in a case-insensitive manner? For example:
let dict = [
"name": "John",
"location": "Chicago"
]
Is there a way to call dict["NAME"], dict["nAmE"], etc. and stil get "John"?
A cleaner approach, swift 4:
extension Dictionary where Key == String {
subscript(caseInsensitive key: Key) -> Value? {
get {
if let k = keys.first(where: { $0.caseInsensitiveCompare(key) == .orderedSame }) {
return self[k]
}
return nil
}
set {
if let k = keys.first(where: { $0.caseInsensitiveCompare(key) == .orderedSame }) {
self[k] = newValue
} else {
self[key] = newValue
}
}
}
}
// Usage:
var dict = ["name": "John"]
dict[caseInsensitive: "NAME"] = "David" // overwrites "name" value
print(dict[caseInsensitive: "name"]!) // outputs "David"
Swift support multiple subscripting so you can take advantage of that to define a case-insensitve accessor:
extension Dictionary where Key : StringLiteralConvertible {
subscript(ci key : Key) -> Value? {
get {
let searchKey = String(key).lowercaseString
for k in self.keys {
let lowerK = String(k).lowercaseString
if searchKey == lowerK {
return self[k]
}
}
return nil
}
}
}
// Usage:
let dict = [
"name": "John",
"location": "Chicago",
]
print(dict[ci: "NAME"]) // John
print(dict[ci: "lOcAtIoN"]) // Chicago
This extension is limited to Dictionary whose Key is of type String (as lowercase is meaningless with other data types). However, Swift will complain about constraining a generic type to a struct. The protocol that is closest to String is StringLiteralConvertible.
Note that if you have 2 keys whose lowercase forms are identical, there's no guarantee which one you will get back:
let dict = [
"name": "John",
"NAME": "David",
]
print(dict[ci: "name"]) // no guarantee that you will get David or John.
The existing answers are fine, but the time complexity of lookups/insertions with those strategies deteriorates from O(1) to O(N) (where N is the number of objects in the dictionary).
To retain O(1) you may want to consider the following approach:
/// Wrapper around String which uses case-insensitive implementations for Hashable
public struct CaseInsensitiveString: Hashable, LosslessStringConvertible, ExpressibleByStringLiteral {
public typealias StringLiteralType = String
private let value: String
private let caseInsensitiveValue: String
public init(stringLiteral: String) {
self.value = stringLiteral
self.caseInsensitiveValue = stringLiteral.lowercased()
}
public init?(_ description: String) {
self.init(stringLiteral: description)
}
public var hashValue: Int {
return self.caseInsensitiveValue.hashValue
}
public static func == (lhs: CaseInsensitiveString, rhs: CaseInsensitiveString) -> Bool {
return lhs.caseInsensitiveValue == rhs.caseInsensitiveValue
}
public var description: String {
return value
}
}
var dict = [CaseInsensitiveString: String]()
dict["name"] = "John"
dict["NAME"] = "David" // overwrites "name" value
print(dict["name"]!) // outputs "David"
can use Collection's first(where:) to find first lowercased match from all keys mapped lowercased, then return the value from this result.
extension Dictionary where Key == String {
func valueForKeyInsensitive<T>(key: Key) -> T? {
let foundKey = self.keys.first { $0.compare(key, options: .caseInsensitive) == .orderedSame } ?? key
return self[foundKey] as? T
}
}
first(where:) is a much efficient way to filter or iterate over the large collection
reference:
https://developer.apple.com/documentation/swift/anybidirectionalcollection/2906322-first#
https://github.com/realm/SwiftLint/blob/master/Rules.md#first-where
This should do the job with O(1) while also not allowing to add the same string with different casing (e.g. if you first insert Def it is not replaced by DEF). It also works for Substring if necessary. Note, that this solution is more memory effective, but comes at the cost at recomputing the string transformation and hash on every lookup of a string. If you need to look-up the same value frequently it might be worth to have an implementation which caches the hashValue.
struct CaseInsensitiveString<T: StringProtocol>: Hashable, Equatable, CustomStringConvertible {
var string: T
init(_ string: T) {
self.string = string
}
var description: String { get {
return string.description
}}
var hashValue: Int { get {
string.lowercased().hashValue
} }
func hash(into hasher: inout Hasher) {
hasher.combine(hashValue)
}
static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.string.compare(rhs.string, options: .caseInsensitive) == .orderedSame
}
}
typealias SubstringCI = CaseInsensitiveString<String>
var codeMap = [SubstringCI: Int]()
let test = "Abc Def Ghi"
let testsub = test[test.firstIndex(of: "D")!...test.lastIndex(of: "f")!]
codeMap[SubstringCI(String(testsub))] = 1
print(codeMap.keys, codeMap[SubstringCI("Def")]!, codeMap[SubstringCI("def")]!)
codeMap[SubstringCI("DEF")] = 1
print(codeMap.keys, codeMap[SubstringCI("Def")]!, codeMap[SubstringCI("def")]!)