Remove duplicate struct from an array -- is there a better way?

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.