Custom comparator for Swift - 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
})

Related

Sum value of an array of structs in Swift

I'm looking for some help on how to sum a value within an array of structs.
If I have a struct defined like this:
struct Item {
let value : Float
let name : String
let planDate : String
}
And then an array of these structs like this:
let dataArray = [Item(value:100, name:"apple", planDate:"2020-10-01"),
Item(value:200, name:"lemon", planDate:"2020-10-04"),
Item(value:300, name:"apple", planDate:"2020-10-04"),
Item(value:400, name:"apple", planDate:"2020-10-01")
]
How can I sum the value while grouping by the name and planDate as well as sorting by name and planDate?
Here's what I'd like to return:
let resultArray = [Item(value:500, name:"apple", planDate:"2020-10-01"),
Item(value:300, name:"apple", planDate:"2020-10-04"),
Item(value:200, name:"lemon", planDate:"2020-10-04")
]
The easiest way (well, easy is in the eye of the beholder) is to make a dictionary that groups by a composite of your criteria, name and planDate. Now for each entry in the dictionary you've got an array of all the Items that go together! So just sum their values. Now make the dictionary back into an array and sort it.
let dataArray = [Item(value:100, name:"apple", planDate:"2020-10-01"),
Item(value:200, name:"lemon", planDate:"2020-10-04"),
Item(value:300, name:"apple", planDate:"2020-10-04"),
Item(value:400, name:"apple", planDate:"2020-10-01")
]
let dict = Dictionary(grouping: dataArray) { $0.name + $0.planDate }
let dict2 = dict.mapValues { (arr:[Item]) -> Item in
let sum = arr.reduce(0) {
$0 + $1.value
}
return Item(value:sum, name:arr[0].name, planDate:arr[0].planDate)
}
let dataArray2 = dict2.values.sorted { ($0.name, $0.planDate) < ($1.name, $1.planDate) }
print(dataArray2)
I would take a different approach as well. First make your Item conform to Equatable and Comparable. Then you can reduce your sorted items, check if each item is equal to the last item of the result. If true increase the value otherwise append a new item to the result:
extension Item: Equatable, Comparable {
static func ==(lhs: Self, rhs: Self) -> Bool {
(lhs.name, lhs.planDate) == (rhs.name, rhs.planDate)
}
static func < (lhs: Self, rhs: Self) -> Bool {
(lhs.name, lhs.planDate) < (rhs.name, rhs.planDate)
}
}
let result: [Item] = items.sorted().reduce(into: []) { partial, item in
if item == partial.last {
partial[partial.endIndex-1].value += item.value
} else {
partial.append(item)
}
}
I'll offer another variant using Dictionary(_:uniquingKeysWith:):
let dict = Dictionary(dataArray.map { ($0.name + $0.planDate, $0) },
uniquingKeysWith: {
Item(value: $0.value + $1.value, name: $0.name, planDate: $0.planDate)
})
let result = dict.values.sorted {($0.name, $0.planDate) < ($1.name, $1.planDate)}
For completeness, here's a solution that explicitly makes use of the fact that you want to use name&planDate as both group identifier, and sort key.
You can make use of the Identifiable protocol, and build a struct with name&planDate (structs are almost free in Swift):
extension Item: Identifiable {
struct ID: Hashable, Comparable {
let name: String
let planDate: String
static func < (lhs: Item.ID, rhs: Item.ID) -> Bool {
(lhs.name, lhs.planDate) < (rhs.name, rhs.planDate)
}
}
var id: ID { ID(name: name, planDate: planDate) }
// this will come in handly later
init(id: ID, value: Float) {
self.init(value: value, name: id.name, planDate: id.planDate)
}
}
Then you can destructure the Item struct by its identifier, accumulate the values, and restructure it back:
let valueGroups = dataArray
.reduce(into: [:]) { groups, item in
// here we destructure the Item into id and value, and accumulate the value
groups[item.id, default: 0] += item.value
}
.sorted { $0.key < $1.key } // the key of the dictionary is the id, we sort by that
// and were we restructure it back
let result = valueGroups.map(Item.init(id:value:))
We can take it even further and refine the operations we need by extensing Sequence:
extension Sequence {
/// returns an array made of the sorted elements by the given key path
func sorted<T>(by keyPath: KeyPath<Element, T>) -> [Element] where T: Comparable {
sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
}
/// Accumulates the values by the specified key
func accumulated<K, T>(values: KeyPath<Element, T>,
by key: KeyPath<Element, K>) -> [K:T]
where K: Hashable, T: AdditiveArithmetic {
reduce(into: [:]) { $0[$1[keyPath: key], default: T.zero] += $1[keyPath: values] }
}
}
The two new additions, sort by a key path, and accumulate key paths by using another key, are independent enough to deserve function for they own, as they are generic enough to be reusable in other contexts.
The actual business logic becomes simple as
let result = dataArray
.accumulated(values: \.value, by: \.id)
.map(Item.init(id:value:))
.sorted(by: \.id)
Even if this solution is more verbose than the other one, it has the following advantages:
clear separation of concerns
breaking the code into smaller units, which can be independently unit tested
code reusability
simple caller code, easy to understand and review

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

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

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

Change the value that is being set in variable's willSet block

I'm trying to sort the array that is being set before setting it but the argument of willSet is immutable and sort mutates the value. How can I overcome this limit?
var files:[File]! = [File]() {
willSet(newFiles) {
newFiles.sort { (a:File, b:File) -> Bool in
return a.created_at > b.created_at
}
}
}
To put this question out of my own project context, I made this gist:
class Person {
var name:String!
var age:Int!
init(name:String, age:Int) {
self.name = name
self.age = age
}
}
let scott = Person(name: "Scott", age: 28)
let will = Person(name: "Will", age: 27)
let john = Person(name: "John", age: 32)
let noah = Person(name: "Noah", age: 15)
var sample = [scott,will,john,noah]
var people:[Person] = [Person]() {
willSet(newPeople) {
newPeople.sort({ (a:Person, b:Person) -> Bool in
return a.age > b.age
})
}
}
people = sample
people[0]
I get the error stating that newPeople is not mutable and sort is trying to mutate it.
It's not possible to mutate the value inside willSet. If you implement a willSet observer, it is passed the new property value as a constant parameter.
What about modifying it to use didSet?
var people:[Person] = [Person]()
{
didSet
{
people.sort({ (a:Person, b:Person) -> Bool in
return a.age > b.age
})
}
}
willSet is called just before the value is stored.
didSet is called immediately after the new value is stored.
You can read more about property observers here
https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Properties.html
You can also write a custom getter and setter like below. But didSet seems more convenient.
var _people = [Person]()
var people: [Person] {
get {
return _people
}
set(newPeople) {
_people = newPeople.sorted({ (a:Person, b:Person) -> Bool in
return a.age > b.age
})
}
}
It is not possible to change value types (including arrays) before they are set inside of willSet. You will need to instead use a computed property and backing storage like so:
var _people = [Person]()
var people: [Person] {
get {
return _people
}
set(newPeople) {
_people = newPeople.sorted { $0.age > $1.age }
}
}
Another solution for people who like abstracting away behavior like this (especially those who are used to features like C#'s custom attributes) is to use a Property Wrapper, available since Swift 5.1 (Xcode 11.0).
First, create a new property wrapper struct that can sort Comparable elements:
#propertyWrapper
public struct Sorting<V : MutableCollection & RandomAccessCollection>
where V.Element : Comparable
{
var value: V
public init(wrappedValue: V) {
value = wrappedValue
value.sort()
}
public var wrappedValue: V {
get { value }
set {
value = newValue
value.sort()
}
}
}
and then assuming you implement Comparable-conformance for Person:
extension Person : Comparable {
static func < (lhs: Person, rhs: Person) -> Bool {
lhs.age < lhs.age
}
static func == (lhs: Person, rhs: Person) -> Bool {
lhs.age == lhs.age
}
}
you can declare your property like this and it will be auto-sorted on init or set:
struct SomeStructOrClass
{
#Sorting var people: [Person]
}
// … (given `someStructOrClass` is an instance of `SomeStructOrClass`)
someStructOrClass.people = sample
let oldestPerson = someStructOrClass.people.last
Caveat: Property wrappers are not allowed (as of time of writing, Swift 5.7.1) in top-level code— they need to be applied to a property var in a struct, class, or enum.
To more literally follow your sample code, you could easily also create a ReverseSorting property wrapper:
#propertyWrapper
public struct ReverseSorting<V : MutableCollection & RandomAccessCollection & BidirectionalCollection>
where V.Element : Comparable
{
// Implementation is almost the same, except you'll want to also call `value.reverse()`:
// value = …
// value.sort()
// value.reverse()
}
and then the oldest person will be at the first element:
// …
#Sorting var people: [Person]
// …
someStructOrClass.people = sample
let oldestPerson = someStructOrClass.people[0]
And even more directly, if your use-case demands using a comparison closure via sort(by:…) instead of implementing Comparable conformance, you can do that to:
#propertyWrapper
public struct SortingBy<V : MutableCollection & RandomAccessCollection>
{
var value: V
private var _areInIncreasingOrder: (V.Element, V.Element) -> Bool
public init(wrappedValue: V, by areInIncreasingOrder: #escaping (V.Element, V.Element) -> Bool) {
_areInIncreasingOrder = areInIncreasingOrder
value = wrappedValue
value.sort(by: _areInIncreasingOrder)
}
public var wrappedValue: V {
get { value }
set {
value = newValue
value.sort(by: _areInIncreasingOrder)
}
}
}
// …
#SortingBy(by: { a, b in a.age > b.age }) var people: [Person] = []
// …
someStructOrClass.people = sample
let oldestPerson = someStructOrClass.people[0]
Caveat: The way SortingBy's init currently works, you'll need to specify an initial value ([]). You can remove this requirement with an additional init (see Swift docs), but that approach is much less complicated when your property wrapper works on a concrete type (e.g. if you wrote a non-generic PersonArraySortingBy property wrapper), as opposed to a generic-on-protocols property wrapper.