I have an array of structs that look like this:
struct Periodical: Codable, Identifiable
{
var id: UUID = UUID()
let name: String // name of the bill or periodical
let dueDate: Date // due date
let price: Double? // amount due
let notes: String // any comments
var timestamp: Date? = Date()
}
A duplicate is defined as having the same id. If there are two items in the array with the same id, the one with the earliest timestamp is removed. A nil timestamp is considered earlier than any other timestamp.
Here’s the function I have to accomplish this:
func removeDuplicates(_ list: [Periodical]) -> [Periodical]
{
var resultList: [Periodical] = []
let originalList = list.sorted(by: { $0.id.uuidString < $1.id.uuidString })
var skipItem = false
if originalList.count < 2 { return originalList }
for index in 0..<originalList.count - 1
{
if skipItem
{
skipItem = false
continue
}
if originalList[index].id != originalList[index + 1].id
{
resultList.append(originalList[index])
if index == originalList.count - 2 // Is this the next-to-last item?
{ resultList.append(originalList[index + 1]) }
}
else
{
if originalList[index].timestamp == nil
{ resultList.append(originalList[index + 1]) }
else if originalList[index + 1].timestamp == nil
{ resultList.append(originalList[index]) }
else if originalList[index].timestamp! > originalList[index + 1].timestamp!
{ resultList.append(originalList[index]) }
else
{ resultList.append(originalList[index + 1]) }
skipItem = true // Skip the earlier item.
}
}
return resultList
}
The function works fine, but I’m wondering if there’s a better way. I’m still relatively new to Swift and thought there might be some magic functions that would make this easier.
Do you have any sample data we can use for testing?
Will this do? I made changes to the struct and function to simplify testing.
struct Periodical
{
let id: Int
let timestamp: Int?
}
let p1 = Periodical(id: 1, timestamp: 5)
let p2 = Periodical(id: 1, timestamp: 4)
let p3 = Periodical(id: 4, timestamp: 1)
let p4 = Periodical(id: 7, timestamp: nil)
let p5 = Periodical(id: 7, timestamp: nil)
let p6 = Periodical(id: 5, timestamp: 4)
let l1 = [p1]
let l2 = [p4]
let l3 = [p1, p2, p3, p4, p5, p6]
func test()
{
removeDuplicates(l1)
removeDuplicates(l2)
removeDuplicates(l3)
}
func removeDuplicates(_ list: [Periodical]) -> [Periodical]
{
var resultList: [Periodical] = []
let originalList = list.sorted(by: { $0.id < $1.id })
var skipItem = false
if originalList.count < 2 { return originalList }
for index in 0..<originalList.count - 1
{
if skipItem
{
skipItem = false
continue
}
if originalList[index].id != originalList[index + 1].id
{
resultList.append(originalList[index])
if index == originalList.count - 2 // Is this the next-to-last item?
{ resultList.append(originalList[index + 1]) }
}
else
{
if originalList[index].timestamp == nil
{ resultList.append(originalList[index + 1]) }
else if originalList[index + 1].timestamp == nil
{ resultList.append(originalList[index]) }
else if originalList[index].timestamp! > originalList[index + 1].timestamp!
{ resultList.append(originalList[index]) }
else
{ resultList.append(originalList[index + 1]) }
skipItem = true // Skip the earlier item.
}
}
return resultList
}
Try this:
func removeDuplicates(_ periodicals: [Periodical]) -> [Periodical] {
let uniques = periodicals.reduce(into: Dictionary<Int, Periodical>()) { result, currentVal in
//do we already have this periodical in our result?
if let existingVal = result[currentVal.id] {
//yes we do, so compare the timestamps
switch (currentVal.timestamp, existingVal.timestamp) {
case (nil, nil):
//both currentVal.timestamp and existingVal.timestamp are nil
//so we do nothing
break
case (nil, .some(_)):
//currentVal.timestamp == nil, so do nothing
break
case (.some(_), nil):
//existingVal.timestamp == nil so replace with currentVal
result[currentVal.id] = currentVal
default:
//both currentVal.timestamp and existingVal.timestamp have values
//so compare them and keep the latest one
//NOTE: it's safe to force unnwrap here since we already
//determined they have values
if currentVal.timestamp! > existingVal.timestamp! {
result[currentVal.id] = currentVal
}
}
} else {
//no we do not, so add it
result[currentVal.id] = currentVal
}
}
return Array(uniques.values)
}
Wow, there’s some new concepts here that I need to study, so I understand what’s happening.
There’s also a new complication: if both timestamps are nil, I want to keep the one with the latest due date. That should be pretty easy to implement in the (nil, nil) case.
Thanks for the help. Give me a couple of days to play with it and I’ll let you know how it turns out.
I have to do some more testing, but so far, it works like a charm. Thanks for the lesson. That’s a really clever way to use a dictionary. I learned something today.