3

I'd like to sort an array of persons considering 3 criteria : this array is made of customers with 3 informations :

  1. name (String)
  2. hasPurchased (Bool)
  3. purchaseDate (that can be nil if hasPurchased = false)

I'm able to sort my customers by name, no problem:

    customersList.sort(by: {$0.name!.compare($1.name!) == .orderedAscending})

I'm able to sort by boolean and name, no problem:

    customersList.sort {$1.hasPurchased == $0.hasPurchased ? ($0.name!.compare($1.name!) == .orderedAscending) : $1.hasPurchased && !$0.hasPurchased}

But what I would like to do is to sort in a single array :

  1. first, by boolean and name (hasPurchased = false, name orderedAscending)
  2. then, if hasPurchased = true, sort by purchaseDate, comparing dates orderedAscending

This way, I would keep in a single array my customers by "ABC" that haven't purchased yet and then those who purchased sorted by date of purchase (non-null criteria in this case)...

tell me what you think about it.

1
  • 1
    Don't do ternary if, use plain if, with else etc, or it will be unreadable. It should be easier then, because you have already the logic.. Commented Aug 9, 2021 at 11:03

3 Answers 3

2

Notice that the argument to sort is called "areInIncreasingOrder". Given two Customer objects, you are supposed to return whether they are in increasing order. So to determine whether two customers are in increasing order with your 2 rules:

  • if they have different hasPurchased values, they are in increasing order if the second one has purchased and the first one has not
  • otherwise, if they have the same hasPurchased value of false, then they are in increasing order if the first one's name is in increasing order with the second one's name
  • otherwise, if they have the same hasPurchased value of true, then they are in increasing order if the first one's purchase date is in increasing order with the second one's purchase date

Translating that to code:

customerList.sort { c1, c2 in
    if c1.hasPurchased != c2.hasPurchased {
        // non-purchased customers are ordered before purchased customers
        return c2.hasPurchased && !c1.hasPurchased
    } else if !c1.hasPurchased { // both are non-purchased customers
        // sort by name
        return c1.name < c2.name
    } else { // both are purchased customers
        // sort by date
        return c1.purchaseDate! < c2.purchaseDate!
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

a good classical "if...else" func in fact... it works fine! thank you. just fixed c1.name < c2.name with (c1.name!.compare(c2.name!) == .orderedAscending) to do comparison
1

If this will be the default and most prevalent way you want want to sort customers by I'd approach this by defining Comparable for the customer to take into account these priorities.

extension Customer: Comparable {
   static func < (lhs: Customer, rhs: Customer) -> Bool {
      switch (lhs.hasPurchased, rhs.hasPurchased) {
         case (true, true): return lhs.purchaseDate! < rhs.purchaseDate!
         case (true, false): return false
         case (false, true): return true
         case (false, false): return lhs.name < rhs.name
      }
   }
}

This will then allow you to just use the .sorted() method from Sequence:

customers.sorted()

As an examples of this:

let customers:[Customer] = [
   Customer(name: "Alan", hasPurchased: false, purchaseDate: nil),
   Customer(name: "Ben", hasPurchased: false, purchaseDate: nil),
   Customer(name: "Carol", hasPurchased: true, purchaseDate: Date()),
   Customer(name: "Dan", hasPurchased: true, purchaseDate: Date().addingTimeInterval(100)),
   Customer(name: "Elsie", hasPurchased: false, purchaseDate: nil),
   Customer(name: "Frank", hasPurchased: true, purchaseDate: Date().addingTimeInterval(200))
]

let s = customers.sorted()
s.map{print($0)}

Which gives you:

Customer(name: "Alan", hasPurchased: false, purchaseDate: nil)
Customer(name: "Ben", hasPurchased: false, purchaseDate: nil)
Customer(name: "Elsie", hasPurchased: false, purchaseDate: nil)
Customer(name: "Carol", hasPurchased: true, purchaseDate: Optional(2021-08-09 11:02:55 +0000))
Customer(name: "Dan", hasPurchased: true, purchaseDate: Optional(2021-08-09 11:04:35 +0000))
Customer(name: "Frank", hasPurchased: true, purchaseDate: Optional(2021-08-09 11:06:15 +0000))

This approach will mean that if you want at some other point, for example, to just sort by name, then you will have to define that sort explicitly via a closure, but as that sort closure will be far simpler to write (and later on to understand) I see this as an acceptable compromise. It depends on the detail of your wider use case, which isn't clear from the question. If this need is the exception rather than the norm, you can adopt the above approach to an explicit sort closure.

Comments

1

The hasPurchased Bool is redundant information. Either the object has a purchased date or it doesn't. As it stands, what are you going to do if a date exists but hasPurchased is false? What if hasPurchased is true but no date exists? I suggest removing the Bool.

I would solve the problem this way:

struct Person {
    let name: String
    let purchaseDate: Date?
}

func example(people: [Person]) -> [Person] {
    people.sorted(by: unpurchasedFirst)
}

func unpurchasedFirst(lhs: Person, rhs: Person) -> Bool {
    switch (lhs.purchaseDate, rhs.purchaseDate) {
    case (.none, .some): // lhs hasn't purchased yet, so needs to go first
        return true
    case (.some, .none): // rhs hasn't purchased yet, so needs to go first
        return false
    case let (.some(lhsDate), .some(rhsDate)): // both purchased, so most recent purchase first
        return lhsDate < rhsDate
    case (.none, .none): // neither purchased, so ABC order by name
        return lhs.name.caseInsensitiveCompare(rhs.name) == .orderedAscending
    }
}

The switch statement and comments make it quite clear what the order is. The fact that the ordering is in a separate function makes it very easy to test.

1 Comment

While Sweeper answer (stackoverflow.com/a/68710997/1801544) details what to do according to your logic, this is a very nice factorization of the code... I'll add maybe var hasPurchased: Bool { purchasedDate != nil } (or maybe even a lazy var) to keep the user "logic" and I guess other quick access for later in his/her code..

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.