Merge objects of the same type - swift

Say I have a struct Coin
struct Coin {
var value: Float?
var country: String?
var color: String?
}
I have two instances of a Coin; we'll call them coinA and coinB.
let coinA = Coin()
coinA.value = nil
coinA.country = "USA"
coinA.color = "silver"
let coinB = Coin()
coinB.value = 50.0
Now, I want to merge the values of coinB into coinA. So the result would be coinA whose values would result in:
country = "USA"
color = "silver"
value = 50.0
I am able to accomplish this with Dictionary objects using the merge() function. However, I am unsure how to accomplish this using custom Swift objects. Is there a way?
Update
Here's how I've gotten it to work with dictionaries:
var originalDict = ["A": 1, "B": 2]
var newDict = ["B": 69, "C": 3]
originalDict.merge(newDict) { (_, new) in new }
//originalDict = ["A": 1, "B": 69, "C": 3]
And I will further clarify, in this function if the newDict does not have keys that the originalDict, the originalDict maintains them.

Ultimately, the most efficient way in the fewest lines of code is probably exactly what you'd expect:
extension Coin {
func merge(with: Coin) -> Coin {
var new = Coin()
new.value = value ?? with.value
new.country = country ?? with.country
new.color = color ?? with.color
return new
}
}
let coinC = coinA.merge(with: coinB)
Note that in the above scenario, the resulting value will always be coinA's, and will only be coinB's if coinA's value for a given key is nil. Whenever you change, add, or delete a property on Coin, you'll have to update this method, too. However, if you care more about future-proofing against property changes and don't care as much about writing more code and juggling data around into different types, you could have some fun with Codable:
struct Coin: Codable {
var value: Float?
var country: String?
var color: String?
func merge(with: Coin, uniquingKeysWith conflictResolver: (Any, Any) throws -> Any) throws -> Coin {
let encoder = JSONEncoder()
let selfData = try encoder.encode(self)
let withData = try encoder.encode(with)
var selfDict = try JSONSerialization.jsonObject(with: selfData) as! [String: Any]
let withDict = try JSONSerialization.jsonObject(with: withData) as! [String: Any]
try selfDict.merge(withDict, uniquingKeysWith: conflictResolver)
let final = try JSONSerialization.data(withJSONObject: selfDict)
return try JSONDecoder().decode(Coin.self, from: final)
}
}
With that solution, you can call merge on your struct like you would any dictionary, though note that it returns a new instance of Coin instead of mutating the current one:
let coinC = try coinA.merge(with: coinB) { (_, b) in b }

I thought it would be interesting to show a solution based on Swift key paths. This allows us to loop somewhat agnostically through the properties — that is, we do not have to hard-code their names in a series of successive statements:
struct Coin {
var value: Float?
var country: String?
var color: String?
}
let c1 = Coin(value:20, country:nil, color:"red")
let c2 = Coin(value:nil, country:"Uganda", color:nil)
var c3 = Coin(value:nil, country:nil, color:nil)
// ok, here we go
let arr = [\Coin.value, \Coin.country, \Coin.color]
for k in arr {
if let kk = k as? WritableKeyPath<Coin, Optional<Float>> {
c3[keyPath:kk] = c1[keyPath:kk] ?? c2[keyPath:kk]
} else if let kk = k as? WritableKeyPath<Coin, Optional<String>> {
c3[keyPath:kk] = c1[keyPath:kk] ?? c2[keyPath:kk]
}
}
print(c3) // Coin(value: Optional(20.0), country: Optional("Uganda"), color: Optional("red"))
There are unfortunate features of key paths that require us to cast down from the array element explicitly to any possible real key path type, but it still has a certain elegance.

If you're willing to make the merge function specific to Coin, you can just use the coalesce operator like so:
struct Coin {
var value: Float?
var country: String?
var color: String?
func merge(_ other: Coin) -> Coin {
return Coin(value: other.value ?? self.value, country: other.country ?? self.country, color: other.color ?? self.color)
}
}
let coinC = coinA.merge(coinB)
This will return a new Coin using the values from coinB, and filling in any nils with those from coinA.

If your goal is to change coin A what you need is a mutating method. Note that structures are not like classes. If you would like to change its properties you need to declare your coin as variable. Note that none of your examples would compile if you declare your coins as constants:
struct Coin {
var value: Float?
var country: String?
var color: String?
mutating func merge(_ coin: Coin) {
value = value ?? coin.value
country = country ?? coin.country
color = color ?? coin.color
}
init(value: Float? = nil, country: String? = nil, color: String? = nil) {
self.value = value
self.country = country
self.color = color
}
}
Playground testing:
var coinA = Coin(country: "USA", color: "silver")
coinA.merge(Coin(value: 50))
print(coinA.country ?? "nil") // "USA"
print(coinA.color ?? "nil") // "silver"
print(coinA.value ?? "nil") // 50.0

This is not a high-level approach like the merge one you shared the link to but as long as you have a struct to implement the merge feature into, it will do the job.
func merge(other: Coin, keepTracksOfCurrentOnConflict: Bool) -> Coin {
var decidedValue = value
if decidedValue == nil && other.value != nil {
decidedValue = other.value
} else if other.value != nil {
//in this case, it's conflict.
if keepTracksOfCurrentOnConflict {
decidedValue = value
} else {
decidedValue = other.value
}
}
var resultCoin = Coin(value: decidedValue, country: nil, color: nil)
return resultCoin
}
}
You can do the same for other properties.

If you want to wrap it around protocol. The idea behind is the same:
you convert object's to dict
merge two dict's
convert merged dict back to your object
import Foundation
protocol Merge: Codable {
}
extension Dictionary where Key == String, Value == Any {
func mergeAndReplaceWith(object: [Key: Value]) -> [Key: Value] {
var origin = self
origin.merge(object) { (_, new) in
new
}
return origin
}
}
extension Merge {
func toJson() -> [String: Any] {
let jsonData = try! JSONEncoder().encode(self)
let json = try! JSONSerialization.jsonObject(with: jsonData, options: []) as! [String: Any]
return json
}
func merge(object: Merge) -> Merge {
let origin = self.toJson()
let objJson = object.toJson()
let decoder = JSONDecoder()
let merge = origin.mergeAndReplaceWith(object: objJson)
var jsonData = try! JSONSerialization.data(withJSONObject: merge, options: .prettyPrinted)
var mergedObject = try! decoder.decode(Self.self, from: jsonData)
return mergedObject
}
}
struct List: Merge {
let a: String
}
struct Detail: Merge {
struct C: Codable {
let c: String
}
let a: String
let c: C?
}
let list = List(a: "a_list")
let detail_without_c = Detail(a: "a_detail_without_c", c: nil)
let detail = Detail(a: "a_detail", c: Detail.C(c: "val_c_0"))
print(detail.merge(object: list))
print(detail_without_c.merge(object: detail))
Detail(a: "a_list", c: Optional(__lldb_expr_5.Detail.C(c: "val_c_0")))
Detail(a: "a_detail", c: Optional(__lldb_expr_5.Detail.C(c: "val_c_0")))
With this solution you can actually merge two representations of your endpoint, in my case it is List and Detail.

Related

How to recursively compare two Codable objects and get differences in Swift?

I have two instances of Lecture which are oldLecture and newLecture. A simplified declaration of Lecture is as follows (the full version has many more fields):
struct Lecture: Codable {
let title: String
let class_times: [ClassTime]
let color: [String: String]
}
struct ClassTime: Codable {
let start: Double
let len: Double
}
Now I need to recursively compare oldLecture and newLecture, and create a new dictionary-like object that contains only the properties that are modified in newLecture. The resulting object will be sent to the server as PUT request.
For example,
var oldLecture = Lecture(title: "lecture1",
class_times: [ClassTime(start: 1, len: 2)],
color: ["fg": "#fff", "bg": "#000"])
var newLecture = Lecture(title: "lecture1",
class_times: [ClassTime(start: 2, len: 3)],
color: ["fg": "#fff", "bg": "#000"])
// the result object should only contain `class_times` key with the value of `[ClassTime(start: 2, len: 3)]`
What I have tried so far
guard let oldDict = oldLecture.asDictionary() else { return nil }
guard let newDict = newLecture.asDictionary() else { return nil }
var dict: [String: Any] = [:]
for (key, newVal) in newDict {
if let oldVal = oldDict[key], oldVal != newVal {
dict[key] = newVal
}
}
The approach above won't work because it doesn't compare oldVal and newVal recursively.
Is there a cleaner way to achieve this, without using third-party libraries such as SwiftyJSON?
I think, you want to look into Sets and Set Arithmetics.
This code is probably not fully what you want, but it should point you in the right direction.
By creating a symetric different set of the times of both lectures we get a list of times that are in either lecture, but not both.
struct Lecture: Codable, Equatable {
let title: String
let times: [LectureTime]
let color: [String: String]
}
struct LectureTime: Codable, Hashable {
let start: Double
let len: Double
}
var oldLecture = Lecture(title: "lecture1",
times: [.init(start: 1, len: 2), .init(start: 5, len: 2)],
color: ["fg": "#fff", "bg": "#000"])
var newLecture = Lecture(title: "lecture1",
times: [.init(start: 2, len: 3), .init(start: 5, len: 2)],
color: ["fg": "#fff", "bg": "#000"])
if oldLecture != newLecture {
let oldTimes = Set(oldLecture.times)
let newTimes = Set(newLecture.times)
let x = oldTimes.symmetricDifference(newTimes)
print(String(describing:x))
}
prints
[LectureTime(start: 1.0, len: 2.0), LectureTime(start: 2.0, len: 3.0)]
Probably you want to extend your time model to be identifiable— than you could identify, what time has been changed.
to get the all the differences between the newLecture and oldLecture (or vice versa),
and send that to your server, try this approach, using Equatable for ClassTime, that will compare the content of ClassTime objects.
The results of getDiff(...) gives you a Lecture, with the title differences in the title field, and similarly for color and class_times fields.
let result = getDiff(old: oldLecture, new: newLecture)
print("\n---> result: \(result)")
do {
let data = try JSONEncoder().encode(result)
print("---> data: \(data)")
// --> send data to your server
if let str = String(data: data, encoding: .utf8) {
print("---> this is the json object you are sending: \(str)")
}
} catch {
print("---> error: \(error)")
}
where:
func getDiff(old: Lecture, new: Lecture) -> Lecture {
let title = old.title == new.title ? "" : new.title
let color: [String: String] = new.color.filter{ dic in
!old.color.contains(where: { $0.key == dic.key && $0.value == dic.value })
}
let cls = new.class_times.filter{ !old.class_times.contains($0)}
return Lecture(title: title, class_times: cls, color: color)
}
with:
struct ClassTime: Codable, Equatable { // <-- here
let start: Double
let len: Double
}

Conditional appending to an array

I load data from Firebase into my custom "CommentModel" (newComment):
// Load the comment with id
func observeComment(commentId: String, completion: #escaping (CommentModel) -> Void) {
let db = Firestore.firestore()
db.collection("comments").document(commentId).getDocument { (snapshot, error) in
guard let dic = snapshot?.data() else { return }
let newComment = CommentModel(dictionary: dic)
completion(newComment)
}
}
My Model:
import UIKit
class CommentModel {
var postId: String?
var userUid: String?
var postText: String?
var postDate: Double?
init(dictionary: [String: Any]) {
postId = dictionary["postId"] as? String
userUid = dictionary["userUid"] as? String
postText = dictionary["postText"] as? String
postDate = dictionary["postDate"] as? Double
}
}
Which gives me the following result:
["user2" : 5], ["user1" : 4], ["user2" : 3], ["user1" : 2], ["user1" : 1]
What I am trying to achieve: Call a function in "observeComments" and append the data if the userID does not already exists and if the number is the lower than the existing number and load it into a new "CommentModel", no matter how much comments or user I have.
The result should look like:
["user1" : 2], ["user1" : 1]
Because I have two userIDs and returning the lowest number.
Dictionary cannot have duplicate keys and the dictionaries are not based on sorting, they are based on key value. This means you can't order a dictionary.
But, on the other hand, remove a value from a dictonary in swift is easy you only needs to do
dic.removeValue(forKey: "user2")
With conditional
var hues = ["Heliotrope": 296, "Coral": 16, "Aquamarine": 156]
if let value = hues.removeValue(forKey: "Coral") {
print("The value \(value) was removed.")
}
// Prints "The value 16 was removed."
it's this simple,
if v < comments["john"] { comments["john'] = v }

how to get single variable name from struct

I have a core data framework to handle everything you can do with coredata to make it more cooperateable with codable protocol. Only thing i have left is to update the data. I store and fetch data by mirroring the models i send as a param in their functions. Hence i need the variable names in the models if i wish to only update 1 specific value in the model that i request.
public func updateObject(entityKey: Entities, primKey: String, newInformation: [String: Any]) {
let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityKey.rawValue)
do {
request.predicate = NSPredicate.init(format: "\(entityKey.getPrimaryKey())==%#", primKey)
let fetchedResult = try delegate.context.fetch(request)
print(fetchedResult)
guard let results = fetchedResult as? [NSManagedObject],
results.count > 0 else {
return
}
let key = newInformation.keys.first!
results[0].setValue(newInformation[key],
forKey: key)
try delegate.context.save()
} catch let error {
print(error.localizedDescription)
}
}
As you can see the newInformation param contains the key and new value for the value that should be updated. However, i dont want to pass ("first": "newValue") i want to pass spots.first : "newValue"
So if i have a struct like this:
struct spots {
let first: String
let second: Int
}
How do i only get 1 name from this?
i've tried:
extension Int {
var name: String {
return String.init(describing: self)
let mirror = Mirror.init(reflecting: self)
return mirror.children.first!.label!
}
}
I wan to be able to say something similar to:
spots.first.name
But can't figure out how
Not sure that I understood question, but...what about this?
class Spots: NSObject {
#objc dynamic var first: String = ""
#objc dynamic var second: Int = 0
}
let object = Spots()
let dictionary: [String: Any] = [
#keyPath(Spots.first): "qwerty",
#keyPath(Spots.second): 123,
]
dictionary.forEach { key, value in
object.setValue(value, forKeyPath: key)
}
print(object.first)
print(object.second)
or you can try swift keypath:
struct Spots {
var first: String = ""
var second: Int = 0
}
var spots = Spots()
let second = \Spots.second
let first = \Spots.first
spots[keyPath: first] = "qwerty"
spots[keyPath: second] = 123
print(spots)
however there will be complex (or impossible) problem to solve if you will use dictionary:
let dictionary: [AnyKeyPath: Any] = [
first: "qwerty",
second: 123
]
you will need to cast AnyKeyPath back to WritableKeyPath<Root, Value> and this seems pretty complex (if possible at all).
for path in dictionary.keys {
print(type(of: path).rootType)
print(type(of: path).valueType)
if let writableKeyPath = path as? WritableKeyPath<Root, Value>, let value = value as? Value { //no idea how to cast this for all cases
spots[keyPath: writableKeyPath] = value
}
}

UTF-8 encoding issue of JSONSerialization

I was trying convert struct to Dictionary in Swift. This was my code:
extension Encodable {
var dictionary: [String: Any]? {
if let data = try? JSONEncoder().encode(self) {
if let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
return dict
}
return nil
}
return nil
}
}
This works in most situation. But when I try to convert a nested structure which contains unicode characters such as Chinese, this happened:
struct PersonModel: Codable {
var job: String?
var contacts: [ContactSimpleModel]
var manager: ManagerSimpleModel?
}
struct ContactSimpleModel: Codable {
var relation: String
var name: String
}
struct ManagerSimpleModel: Codable {
var name: String
var age: Int
}
let contact1 = ContactSimpleModel(relation: "朋友", name: "宙斯")
let contact2 = ContactSimpleModel(relation: "同学", name: "奥丁")
let manager = ManagerSimpleModel(name: "拉斐尔", age: 31)
let job = "火枪手"
let person = PersonModel(job: job, contacts: [contact1, contact2], manager: manager)
if let dict = person.dictionary {
print(dict)
}
The result of this code is this:
["contacts": <__NSArrayI 0x600002471980>(
{
name = "\U5b99\U65af";
relation = "\U670b\U53cb";
},
{
name = "\U5965\U4e01";
relation = "\U540c\U5b66";
}
)
, "manager": {
age = 31;
name = "\U62c9\U6590\U5c14";
}, "job": 火枪手]
You can see the result. The Chinese characters in those nested structures were become a utf-8 encoding string. The top-level property "job": 火枪手 is right. But the values in those nested structures were not the original string.
Is this a bug of JSONSerialization? Or how to make it right?
More information. I used the result like this:
var sortedQuery = ""
if let dict = person.dictionary {
sortedQuery = dict.sorted(by: {$0.0 < $1.0})
.map({ "\($0)\($1)" })
.joined(separator: "")
}
It was used to check whether the query was legal. The result is not the same as Java or other platform.
The result is perfectly fine. That's the internal string representation – a pre-Unicode legacy – of an array or dictionary when you print it.
Assign the values to a label or text view and you will see the expected characters.

Create a Dictionary from optionals

I some optionals: numberOfApples:Int?, numberOfBananas:Int?, numberOfOlives:Int? and I'd like to create a dictionary of just the set values. Is there way do succinctly create this?
The closest I've got is:
// These variables are hard-coded for the example's
// sake. Assume they're not known until runtime.
let numberOfApples: Int? = 2
let numberOfBananas: Int? = nil
let numberOfOlives: Int? = 5
let dict: [String:Int?] = ["Apples" : numberOfApples,
"Bananas" : numberOfBananas,
"Olives" : numberOfOlives]
And I'd like to dict to be of type: [String:Int] like so:
["Apples" : 2,
"Olives" : 5]
But this gives me a dictionary of optionals and accessing a value by subscripting gives my a double-wrapped-optional.
I realise that I could do this with a for-loop, but I was wondering if there's something more elegant.
Many thanks in advance.
Personally I would do it this way (and it's how I personally do this when it comes up):
var dict: [String: Int] = [:]
dict["Apples"] = numberOfApples
dict["Bananas"] = numberOfBananas
dict["Olives"] = numberOfOlives
Simple. Clear. No tricks.
But if you wanted to, you could write Dictionary.flatMapValues (to continue the pattern of Dictionary.mapValues). It's not hard. (EDIT: Added flattenValues() to more closely match original question.)
extension Dictionary {
func flatMapValues<T>(_ transform: (Value) throws -> T?) rethrows -> [Key: T] {
var result: [Key: T] = [:]
for (key, value) in self {
if let transformed = try transform(value) {
result[key] = transformed
}
}
return result
}
func flattenValues<U>() -> [Key: U] where Value == U? {
return flatMapValues { $0 }
}
}
With that, you could do it this way, and that would be fine:
let dict = [
"Apples" : numberOfApples,
"Bananas": numberOfBananas,
"Olives" : numberOfOlives
].flattenValues()
You can use filter and mapValues. You first filter all pairs where the value is not nil and then you can safely force unwrap the value. This will change the dict type to [String: Int].
let dict = [
"Apples": numberOfApples,
"Bananas": numberOfBananas,
"Olives": numberOfOlives
]
.filter({ $0.value != nil })
.mapValues({ $0! })
print(dict) //["Olives": 5, "Apples": 2]
Try this:
let numberOfApples: Int? = 5
let numberOfBananas: Int? = nil
let numberOfOlives: Int? = 5
let dict: [String: Int?] = [
"Apples": numberOfApples,
"Bananas": numberOfBananas,
"Olives": numberOfOlives
]
extension Dictionary {
func flatMapValues<U>() -> [Key: U] where Value == Optional<U> {
return reduce(into: [:]) { $0[$1.key] = $1.value }
// Keeping this line as it provides context for comments to this answer. You should delete it if you copy paste this.
// return filter { $0.value != nil } as! [Key : U]
}
}
let unwrappedDict = dict.flatMapValues()
let foo: Int?? = dict["Apples"]
let bar: Int? = unwrappedDict["Apples"]