2

I‘m trying to learn Swift. I am going thru https://docs.swift.org/swift-book/LanguageGuide/Closures.html trying to understand and adapt. I’m trying to write my own sort function: sort by length of name and then by alphabet. This is what I came up with - but it’s not sorting properly:

let names = ["Chris D", "Alex Greenwalt", "Ewa McCormac", "Gustav Ganz", "Anna Ärliger", "Barry White, MD", "Daniella"]
var sortedNames = names.sorted(by: { s1, s2 in
    if s1.count < s2.count {
        return true
    } else {
        return s1 > s2
    }
})
for name in names {
    print("Unsorted: \(name) is \(name.count) long")
}
for name in sortedNames {
    print("Sorted: \(name) is \(name.count) long")
}

I am getting this output which obviously has gone thru some sorting but not what I intended:

Unsorted: Chris D is 7 long
Unsorted: Alex Greenwalt is 14 long
Unsorted: Ewa McCormac is 12 long
Unsorted: Gustav Ganz is 11 long
Unsorted: Anna Ärliger is 12 long
Unsorted: Barry White, MD is 15 long
Unsorted: Daniella is 8 long
Sorted: Daniella is 8 long
Sorted: Gustav Ganz is 11 long
Sorted: Ewa McCormac is 12 long
Sorted: Chris D is 7 long
Sorted: Barry White, MD is 15 long
Sorted: Anna Ärliger is 12 long
Sorted: Alex Greenwalt is 14 long

Any ideas why it fails?

Gin Tonyx
  • 383
  • 1
  • 11
  • Yeah, your function looks incorrect, because you are not handling case where "s1.count > s2.count" properly. So you should rewrite it to something like this if s1.count == s2.count {return s1 > s2} return s1.count < s2.count – Mikhail Vasilev Nov 28 '20 at 18:36

3 Answers3

3

Since Swift 3 a Tuple of up to 6 elements is Comparable. So you can simply sort the string's count and itself:

let names = ["Chris D", "Alex Greenwalt", "Ewa McCormac", "Gustav Ganz", "Anna Ärliger", "Barry White, MD", "Daniella"]
let sortedNames = names.sorted { ($0.count, $0) < ($1.count, $1) }
for name in sortedNames {
    print("Sorted: \(name) is \(name.count) long")
}

This will print:

Sorted: Chris D is 7 long
Sorted: Daniella is 8 long
Sorted: Gustav Ganz is 11 long
Sorted: Anna Ärliger is 12 long
Sorted: Ewa McCormac is 12 long
Sorted: Alex Greenwalt is 14 long
Sorted: Barry White, MD is 15 long

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
1

You need to first sort based on the length, and then sort on name if the length is equal. I haven't tried the code below, but it should look like this:

[edit] Now I tried it in a playground, and fixed typos.

var sortedNames = names.sorted(by: { s1, s2 in
    if s1.count < s2.count {
        return true
    } else if s1.count > s2.count {
        return false
    }
    
    return s1 > s2
})

You could rewrite it like this:

var sortedNames = names.sorted(by: { s1, s2 in
    if s1.count == s2.count {
        return s1 < s2
    }
    return s1.count < s2.count
})
Rickard Elimää
  • 7,107
  • 3
  • 14
  • 30
  • `if s1.count < s2.count { return true } else if s1.count > s2.count { return false }` is an error prone way of just writing `if s1.count != s2.count { return s1.count < s2.count }`. I would suggest that instead, and I would recommend it over the `==` check, because it "scales" better when there's more predicates. See https://stackoverflow.com/a/37604010/3141234 – Alexander Nov 28 '20 at 18:40
  • I might not be understanding this, but if `s1.count == s2.count`, it would fall through to the `return s1 > s2` and ignore the count sort (in the first example). If you tested this with `let names = ["AB", "ZZ"]` the result would be `["ZZ", "AB"]`. It should be `else if s1.count >= s2.count {`. – aheze Nov 28 '20 at 18:40
  • Thank you, this helped me understand where I got it wrong. – Gin Tonyx Nov 28 '20 at 21:06
0

Before you write the implementation code for something like this, I suggest starting with a test.

To aid my thinking, I would make a table, with one column for every "attribute", and one for the result. In this case, the attributes being compared are length and lexicographic (alphabetic) ordering.

The precise values (lengths, string values, etc.) don't matter, only their relative values.

| Length               | Lexicographic Ordering | s1 comes before s2? | Reason                                             |
|----------------------|------------------------|---------------------|----------------------------------------------------|
| s1.count < s2.count  |        s1 < s2         |         true        | s1 is shorter                                      |
| s1.count < s2.count  |        s1 == s2        |                     | Not possible                                       |
| s1.count < s2.count  |        s1 > s2         |                     | Not possible                                       |
| s1.count == s2.count |        s1 < s2         |         true        | Length is same, but s1 is first alphabetically     |
| s1.count == s2.count |        s1 == s2        |        false        | They're the same string, neither is first          |
| s1.count == s2.count |        s1 > s2         |        false        | Length is same, but s2 is first alphabetically     |
| s1.count > s2.count  |        s1 < s2         |        false        | Not possible                                       |
| s1.count > s2.count  |        s1 == s2        |                     | Not possible                                       |
| s1.count > s2.count  |        s1 > s2         |        false        | s2 is shorter                                      |

From there, you can make some sample inputs that can reproduce each of these cases:

| Case | s1    | s2    | Length               | Lexicographic Ordering | s1 comes before s2? | Reason                                             |
|------|-------|-------|----------------------|------------------------|---------------------|----------------------------------------------------|
|   1  |   "*" | "***" | s1.count < s2.count  |        s1 < s2         |         true        | s1 is shorter                                      |
|      |       |       | s1.count < s2.count  |        s1 == s2        |                     | Not possible                                       |
|      |       |       | s1.count < s2.count  |        s1 > s2         |                     | Not possible                                       |
|   2  |   "a" |   "z" | s1.count == s2.count |        s1 < s2         |         true        | Length is same, but s1 is first alphabetically     |
|   3  | "foo" | "foo" | s1.count == s2.count |        s1 == s2        |        false        | They're the same string, neither is first          |
|   4  |   "z" |   "a" | s1.count == s2.count |        s1 > s2         |        false        | Length is same, but s2 is first alphabetically     |
|      |       |       | s1.count > s2.count  |        s1 < s2         |        false        | Not possible                                       |
|      |       |       | s1.count > s2.count  |        s1 == s2        |                     | Not possible                                       |
|   5  | "***" |  "*"  | s1.count > s2.count  |        s1 > s2         |        false        | s2 is shorter                                      |

You can use this table to then create some test cases:

func sorter(_ s1: String, _ s2: String) -> Bool {
    fatalError("implement me")
}

final class SorterTests: XCTestCase {
    func testShorterStringsComeFirst() {
        let short = "*"
        let long = "***"

        let case1 = sorter(short, long)
        XCTAssertEqual(true, case1)

        let case6 = sorter(long, short)
        XCTAssertEqual(false, case6)
    }

    func testEqualLengthStringsAreLexicographicallySorted() {
        let earlier = "a"
        let later = "z"

        let case2 = sorter(earlier, later)
        XCTAssertEqual(true, case2)

        let case4 = sorter(later, earlier)
        XCTAssertEqual(false, case4)
    }

    func testEqualStringsArentBeforeThemselves() {
        let anyString = "foo"
        let case5 = sorter(anyString, anyString)
        XCTAssertEqual(false, case5)
    }
}

You can then use these tests to guide the creation of your sorting closure.

Alexander
  • 59,041
  • 12
  • 98
  • 151