How to recursively compare two Codable objects and get differences in Swift? - 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
}

Related

Create Value Array From Model into String in Swift

I want to create data like
From model like
but the data that I get is like this
how can i get the data structure like that (top)? clean there is no model name and no optional in data
Note: i use method create value like this
for i in 0 ..< self.dataProduct.count {
let id_sell = "\(self.dataProduct[i].seller_id ?? 0)"
let origin = self.dataProduct[i].origin ?? 0
let product = self.dataProduct[i].product ?? []
var dataItem = [DataCheckoutMitras.ProductItemCheckout]()
var itemMitra : DataCheckoutMitras?
var dataCourierSelected : CourierObject?
for x in 0 ..< product.count {
var item : DataCheckoutMitras.ProductItemCheckout?
item = DataCheckoutMitras.ProductItemCheckout(product_id: product[x].product_id ?? 0,
name: product[x].name ?? "",
price: product[x].price ?? 0,
subTotal: product[x].subTotal ?? 0,
quantity: product[x].quantity ?? 0,
weight: product[x].weight ?? 0,
origin_item: origin,
notes: product[x].notes ?? "")
dataItem.append(item!)
}
for x in 0 ..< self.id_seller.count {
if id_sell == self.id_seller[x] {
dataCourierSelected = self.dataKurir[x]
}
}
itemMitra = DataCheckoutMitras(origin: origin, select_price_courier: dataCourierSelected, items: dataItem)
mitras.append(itemMitra!)
}
The issue you are facing is because you are printing definition of your struct. What you want is the JSON to do so you will need to:
Implement Codable protocol in both of your struct
This also applied to your CourierObject
struct DataCheckoutMitras: Codable {
let origin: Int?
let items: [ProductItemCheckout]?
struct ProductItemCheckout: Codable {
let product_id : Int?
let name : String?
}
}
encode the struct to JSON data using JSONEncoder
let encodedJSONData = try! JSONEncoder().encode(mitras)
convert JSON to string
let jsonString = String(data: encodedJSONData, encoding: .utf8)
print(jsonString)

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 }

decoding an array of objects in swift

I have an array of objects
{"total_rows":5,"offset":0,"rows":[
{"id":"index","key":"index","value":{"rev":"4-8655b9538706fc55e1c52c913908f338"}},
{"id":"newpage","key":"newpage","value":{"rev":"1-7a27fd343ff98672236996b3fe3abe4f"}},
{"id":"privacy","key":"privacy","value":{"rev":"2-534b0021f8ba81d09ad01fc32938ce15"}},
{"id":"secondpage","key":"secondpage","value":{"rev":"2-65847da61d220f8fc128a1a2f1e21e89"}},
{"id":"third page","key":"third page","value":{"rev":"1-d3be434b0d3157d7023fca072e596fd3"}}
]}
that I need too fit in struct and then decode in swift. My current code is:
struct Index: Content {
var total_rows: Int
var offset: Int
// var rows: [String: String] // I don't really know what I am doing here
}
and the router (using vapor)
router.get("/all") { req -> Future<View> in
let docId = "_all_docs"
print(docId)
let couchResponse = couchDBClient.get(dbName: "pages", uri: docId, worker: req)
guard couchResponse != nil else {
throw Abort(.notFound)
}
print("one")
return couchResponse!.flatMap { (response) -> EventLoopFuture<View> in
guard let data = response.body.data else { throw Abort(.notFound) }
print(data)
let decoder = JSONDecoder()
let doc = try decoder.decode(Index.self, from: data)
let allDocs = Index(
total_rows: doc.total_rows,
offset: doc.offset
//rows: doc.rows
)
print("test after allDocs")
return try req.view().render("index", allDocs)
}
}
to summarise all is fine for the first level (total rows and offset are int and properly decoded) but how can I include in my structure the rows: array and assign thee parsed values to it ?
You're on the right road, you just need to keep going.
struct Index: Decodable {
var total_rows: Int
var offset: Int
var rows: [Row]
}
Then you define a Row:
struct Row: Decodable {
var id: String
var key: String
var value: Value
}
It's not really clear what a Value is in this context, but just to keep the structure.
struct Value: Decodable {
var rev: String
}
And that's all.
let index = try JSONDecoder().decode(Index.self, from: jsonData)

How to sum the Value from the same Key using a Dictionary?

I have a dictionary the looks like this:
(value: 21.0, id: "JdknnhshyrY56AXQcAiLcYtbVlz2") ---> This has a second entry
(value: 18.0, id: "nIvb519nNfMtlVNnsQJ5w4bRbFp2")
(value: 14.0, id: "tlKqcxHdPoemwGqJsyLhERnuQ3Z2")
(value: 5.0, id: "JdknnhshyrY56AXQcAiLcYtbVlz2")]---> This is the second entry
I want to add the value to the same id if has a duplicate entry.
Something like this:
(value: 26.0, id: "JdknnhshyrY56AXQcAiLcYtbVlz2") ---> Value has added and key has become only one
(value: 18.0, id: "nIvb519nNfMtlVNnsQJ5w4bRbFp2")
(value: 14.0, id: "tlKqcxHdPoemwGqJsyLhERnuQ3Z2")]
I already tried using a map method but has been working, has added the values but still multiple entries on the id keeping more than one.
How can I achieve the result.
It's always advisable to convert your Dictionary (with heterogeneous
type of values that eventually falls into [AnyHashable : Any]
category) to a Typed object. With typed object, manipulation or
other calculation become handy.
On the premise of the above statement you will do something like:
let objects = [Object]() // this would be you array with actual data
let reduced = objects.reduce(into: [Object]()) { (accumulator, object) in
if let index = accumulator.firstIndex(where: { $0.id == object.id }) {
accumulator[index].value += object.value
} else {
accumulator.append(object)
}
}
Where the Object is:
struct Object {
var value: Double
let id: String
}
Now if you are wondering how would you convert your Dictionary to & from Object type, look at the complete code:
let arrayOfDictionary = [["value": 21.0, "id": "JdknnhshyrY56AXQcAiLcYtbVlz2"],
["value": 18.0, "id": "nIvb519nNfMtlVNnsQJ5w4bRbFp2"],
["value": 14.0, "id": "tlKqcxHdPoemwGqJsyLhERnuQ3Z2"],
["value": 5.0, "id": "JdknnhshyrY56AXQcAiLcYtbVlz2"]]
struct Object: Codable {
var value: Double
let id: String
}
do {
let jsonData = try JSONSerialization.data(withJSONObject: arrayOfDictionary, options: [])
let objects = try JSONDecoder().decode([Object].self, from: jsonData)
let reduced = objects.reduce(into: [Object]()) { (accumulator, object) in
if let index = accumulator.firstIndex(where: { $0.id == object.id }) {
accumulator[index].value += object.value
} else {
accumulator.append(object)
}
}
let encodedObjects = try JSONEncoder().encode(reduced)
let json = try JSONSerialization.jsonObject(with: encodedObjects, options: [])
if let reducedArrayOfDictionary = json as? [[String : Any]] {
print(reducedArrayOfDictionary)
}
} catch {
print(error)
}

Merge objects of the same type

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.