0

i want to sort some array whit a specific condition, where if after you compare 3 other condition and they are the same it will check wether the boolean is true or false. if it's true i want it to be sorted first but idk how to do it since boolean is not comparable here's my array i want to sort

public struct Beef
{
    let country:Country
    var grade:Character
    let condition:Condition
    let marbling:Bool
}

public var beefs:[Beef]
{
    [
        Beef(country: .id, grade: "C", condition: .fresh, marbling: false),
        Beef(country: .us, grade: "B", condition: .frozen, marbling: true),
        Beef(country: .au, grade: "A", condition: .fresh, marbling: true),
        Beef(country: .id, grade: "A", condition: .frozen, marbling: false),
        Beef(country: .us, grade: "A", condition: .fresh, marbling: true),
        Beef(country: .au, grade: "A", condition: .fresh,marbling: false),
        Beef(country: .au, grade: "A", condition: .fresh,marbling: true)
    ]
}

and this is my sort code

func sortBythree()
{
    let sortedBeef = beef.sorted { left, right in
        if left.country == right.country
        {
            if left.grade == right.grade
            {
                if left.condition == right.condition
                {
                    //what to write? for bool comparison
                    
                }
                return left.condition > right.condition
            }
            return left.grade < right.grade
        }
        return left.country < right.country
    }
    
    for beef in sortedBeef
    {
        print(beef.country,beef.grade,beef.condition,beef.marbling)
    }
}

and this is the result

'''
id A frozen false
id C fresh false
us A fresh true
us B frozen true
au A fresh true
au A fresh false
au A fresh true
'''

i want true to be sorted before false but idon't know how

  • 1
    You need to test `if left.marbling && right.marbling { return true }else if left.marbling { return true } else if right.marbling { return false } else { return true }` or something like that? Which can be factorize later, but you should get the idea? – Larme Aug 09 '21 at 14:24
  • @Larme thank you for your help i got the gist of the idea and it works. – Dimas Pagam Aug 09 '21 at 14:31

3 Answers3

0

You can do really easily sorting using tuples. It takes much less code and it's harder to make a mistake. And as to Bool, you can convert it to Int, like this:

.sorted { left, right in
    return (left.country, left.grade, left.condition, left.marbling.toInt())
         < (right.country, right.grade, right.condition, right.marbling.toInt())
}

extension Bool {
    func toInt() -> Int {
        if self {
            return 1
        } else {
            return 0
        }
    }
}

Check out more about sorting with tuples here

Alexander
  • 59,041
  • 12
  • 98
  • 151
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • I wouldn't recommend `Bool.toInt()`, because the behaviour isn't universal. (E.g. in C, true/false is 1/0, but in Bash, a true/"good" return code is 0, a false/error/"bad" return is non-0) – Alexander Aug 09 '21 at 15:06
  • @Alexander agree, you may name it more clearly and even make private where you're using it. – Phil Dukhov Aug 09 '21 at 15:15
0

This is one of two different answers I'm submitting. This one covers how to do this with the the a more imperative style, which is more similar to what you have already. This might be necessary if you have a lot of properties you want to compare, or if you have more complicated comparison logic. For most cases though, I'd probably recommend the tuple-based approach.

You have a bit of a pyramid of doom going on. Not only does it add a lot of indentation, and makes it harder to read, but it also separates the equality comparison of a property from the check of its ordering. So I'd fix that first:

func sortBythree() {
    let sortedBeef = beef.sorted { left, right in
        guard left.country == right.country else { return left.country < right.country }
        guard left.grade == right.grade { return left.grade < right.grade }
        guard left.condition == right.condition {
            //what to write? for bool comparison
        }
    }
    
    for beef in sortedBeef {
        print(beef.country, beef.grade, beef.condition, beef.marbling)
    }
}

As you noticed, Bool doesn't conform to Comparable. We could add our own conformance, but I don't think it would be proper. The Swift standard library intentionally omits such a conformance, because there isn't a universally correct understanding of whether true should be "less" or false should be "less". This is similar to how Optional doesn't conform to comparable, even if its Wrapped typed does.

It's possible to sketch out some truth tables and figure out the correct Boolean expression. The correct expression is return left.condition && !right.condition. You might also express it in terms of a bunch of if/else statements instead, but I wouldn't recommend either of those approaches.

The issue is that you have to be really careful to ensure your predicate is correctly implementing the semantics of implementing <, and not <= by accident. It also needs to be clear to readers of your code that this is what you're doing.

Similar to me nilComparator in the question linked above, I would recommend you write yourself something a helper function for comparing bools. This has two main benefits:

  1. It's more readable at the call-site.
  2. It's reusable, so you don't have to reinvent this logic every time you need to compare some bools.
  3. It's testable, so you can be sure you didn't mess it up.

Here's my take on it:

enum BoolComparisonOrdering { case trueIsLess, falseIsLess }

/// Implements `lhs < rhs` for two booleans, with ordering determined by the provided `BoolComparisonOrdering`
func compare(_ lhs: Bool, _ rhs: Bool, _ ordering: BoolComparisonOrdering) -> Bool {
    switch ordering {
    case .trueIsLess: return lhs && !rhs
    case .falseIsLess: return !lhs && rhs
    }
}

Here's what the tests for it might look like:

final class BoolComparatorTests: XCTestCase {
    let bools = [true, true, false, false]

    func testTrueIsFirst() throws {
        for _ in 1...1000 {
            let input = bools.shuffled()
            let result = input.sorted{ a, b in compare(a, b, .trueIsLess)}
            XCTAssertEqual(result, [true, true, false, false])
        }
    }


    func testFalseIsFirst() throws {
        let bools = [true, true, false, false]
        
        for _ in 1...1000 {
            let input = bools.shuffled()
            let result = input.sorted { a, b in compare(a, b, .falseIsLess)}
            XCTAssertEqual(result, [false, false, true, true])
        }
    }
}

And here's the usage:

func sortBythree() {
    let sortedBeef = beef.sorted { left, right in
        guard left.country == right.country else { return left.country < right.country }
        guard left.grade == right.grade { return left.grade < right.grade }
        guard left.condition == right.condition {
            return compare(left.condition, right.condition, .trueIsLess)
        }
    }

    for beef in sortedBeef {
        print(beef.country, beef.grade, beef.condition, beef.marbling)
    }
}
Alexander
  • 59,041
  • 12
  • 98
  • 151
0

This is one of two different answers I'm submitting. This one covers how to do this with the the built-in tuple comparison operators, which are probably the preferable approach for most cases.

I would define some helpers that make comparable types from your booleans, like so:

enum OrderedBool { // Empty enum that I'm just using as a namespace
    struct TrueIsLess: Comparable {
        var value: Bool

        init(_ value: Bool) { self.value = value }

        static func < (lhs: Self, rhs: Self) -> Bool {
            lhs.value && !rhs.value
        }
    }

    struct TrueIsLess: Comparable {
        var value: Bool

        init(_ value: Bool) { self.value = value }

        static func < (lhs: Self, rhs: Self) -> Bool {
            !lhs.value && rhs.value
        }
    }
}

The call site would look like this:

func sortBythree() {
    let sortedBeef = beef.sorted { a, b in
        return (a.country, a.grade, OrderedBool.TrueIsLess(a.condition))
             < (b.country, b.grade, OrderedBool.TrueIsLess(b.condition))
    }

    for beef in sortedBeef {
        print(beef.country, beef.grade, beef.condition, beef.marbling)
    }
}
Alexander
  • 59,041
  • 12
  • 98
  • 151