Swift: Getting sorted()


Sticking with the spreadsheet analogy from previous posts, to sort a column of figures low to high or high to low using sort() or sorted() is straightforward (note: sorted() has in Swift 2 changed to sortInPlace()), but usually in a spreadsheet the reordering of one column means the reordering of other columns to keep rows together. And here we need a bit more mutability, because simply put things are mutating.

Rows

Let's suppose there are three rows in the spreadsheet, one with objects, another with prices and a third with colours.
var objects = ["car","van","balloon","flower"]
var prices = [100,123.33,207.66,36.5]
var colours = ["blue","yellow","green","red"]
And that one of those rows is highlighted and determined to be the one to be in charge of reordering all the rows in our spreadsheet table.

Mapping

In this instance we're going to suppose that the object names are going to be in charge of ordering and so they are going to be at the centre of the mapping. In the process of mapping, an array of tuples is created of type [(object:String, price:Double, colour:String)]
var rows = objects.map{(object:$0, price:removeAtIndex(&prices, 0), colour: removeAtIndex(&colours, 0))}
And this array of tuples, which represents the rows displayed can now be sorted however we wish.

Sorting

For now I'm going to stick with sorting by object names:
rows.sort{$0.object < $1.object}
If you wish to sort by other means simply replace 'object' with 'price' or 'colour'.

Time for a refill

With the sorting complete our original arrays can be replaced with those in the new and correct order:
objects = rows.map{$0.object}
prices = rows.map{$0.price}
colours = rows.map{$0.colour}

objects // ["balloon", "car", "flower", "van"]
prices // [207.66, 100, 36.5, 123.33]
colours // ["green", "blue", "red", "yellow"]

Full code

And for completenes, here's the code all in one place:
var objects = ["car","van","balloon","flower"]
var prices = [100,123.33,207.66,36.5]
var colours = ["blue","yellow","green","red"]

var rows = objects.reverse().map{(object:$0, price:prices.removeLast(), colour: colours.removeLast())}
rows.sort{$0.object < $1.object}

objects = rows.map{$0.object}
prices = rows.map{$0.price}
colours = rows.map{$0.colour}

objects // ["balloon", "car", "flower", "van"]
prices // [207.66, 100, 36.5, 123.33]
colours // ["green", "blue", "red", "yellow"]
For the sake of performance, I've tweaked the code by reversing the objects array so that I can work backwards through the prices and colours arrays (using removeLast() lowers the complexity over removeAtIndex()).

Conclusion

It might be that storing separate arrays is considered fragile, mutability undesirable or that the need to identify the names of items unnecessary, but from here experiments can be made to make things more functional and generic. For example:
var objects = ["car","van","balloon","flower"]
var prices = [100,123.33,207.66,36.5]
var colours = ["blue","yellow","green","red"]


func secondarySort<A:Comparable, B>(arr1:[A], inout arr2:[B]) {
    arr2 = arr1.map{(object1:$0, object2:arr2.removeFirst())}.sort({$0.object1 < $1.object1}).map{$0.object2}
}

secondarySort(objects, &colours) // ["green", "blue", "red", "yellow"]
secondarySort(objects, &prices) // [207.66, 100, 36.5, 123.33]
// the array around which the sort is based must be sorted last
objects.sort(<) // ["balloon", "car", "flower", "van"]
Here two arrays are passed to a function and the second array is sorted based on the first, it is a function that can be called time and again for any number of "columns". And it overcomes the hurdle of trying to process multiple "columns" (or arrays, of potentially different types) all at once.

To remain in line with sort() and sorted() being mutating and non-mutating functions, I'll also throw in a secondarySorted() function:
func secondarySorted()<A:Comparable, B>(arr1:[A], var arr2:[B]) -> [B] {

    var rows = arr1.reverse().map{(object1:$0, object2:arr2.removeLast())}
    rows.sort({$0.object1 < $1.object1})
    return rows.map{$0.object2}

}

Comments

  1. Hi Anthony

    I have got your example working in Swift 2.1 but I am trying to sort an array with 5 columns by sorting the first column which has an Int. As a newbie I am struggling to achieve this, can you help please? So I need the row data kept together after sorting (which I think your secondarySort example does). My data is from json so think it’s in Dictionary format

    ReplyDelete
    Replies
    1. This is exactly what the secondarySort() function is for, just make sure you sort the row that defines the order last of all as in the example. Note that a Dictionary is not an ordered type so if you want to utilise this approach you'll need to extract the data and arrange it in Arrays.

      Delete
    2. I have now created 5 arrays and am using your secondarySort() function but maybe I am expecting too much. I can get it to sort 2 arrays but not all of them. So I have Distances which is what I want to sort by, IDs, Descriptions. Lats and Longs. Do I need a secondarySort() function with 5 Comparables? As I am creating the string arrays, could I use ‘sort’ rather than ‘sortInPlace’ method? As I don’t understand how they work, can you offer any advice please?

      Delete
    3. Alan, Here's a gist: https://gist.github.com/sketchytech/32ff1f77037847b5be65

      It sorts five arrays, only the array that you are sorting by needs to be comparable the others could be anything, e.g. UIImages, UIColors, etc.

      Hope this helps.

      A.

      Delete
    4. Thanks you so much for your help and we are getting closer but unfortunately I must be explaining myself badly. To help I changed the values from your example to hopefully make it clearer. So distance of 10 has Description = nearest, ID = n and lat/long smallest values. Here is the code I am using and the print out below it, where the first two sets of values need swapping. The printout I would like is below the line.

      var Distances = [40,30,10,20]
      var IDs = ["f","nc","n","c"]
      var Descriptions = ["furthest","not close","nearest","close"]
      var Lats = [100.12,88.3,28.07,36.0]
      var Longs = [100.12,88.3,28.07,36.0]

      secondarySort(Distances, arr2: &IDs)
      secondarySort(Distances, arr2: &Descriptions)
      secondarySort(Distances, arr2: &Lats)
      secondarySort(Distances, arr2: &Longs)

      Distances = Distances.sort(<) // [10, 20, 30, 40]

      print("Distances = \(Distances)")
      print("IDs = \(IDs)")
      print("Descriptions = \(Descriptions)")
      print("Lats = \(Lats)")
      print("Longs = \(Longs)")

      Distances = [10, 20, 30, 40]
      IDs = ["c", "n", "nc", "f"]
      Descriptions = ["close", "nearest", "not close", "furthest"]
      Lats = [36.0, 28.07, 88.3, 100.12]
      Longs = [36.0, 28.07, 88.3, 100.12]


      Distances = [10, 20, 30, 40]
      IDs = ["n", "c", "nc", "f"]
      Descriptions = ["nearest", "close", "not close", "furthest"]
      Lats = [28.07, 36.0, 88.3, 100.12]
      Longs = [28.07, 36.0, 88.3, 100.12]

      Delete
    5. Alan, I see what's happened. There's an error in the function code, should read:

      func secondarySort(arr1:[A], inout arr2:[B]) {
      arr2 = arr1.map{(object1:$0, object2:arr2.removeFirst())}.sort({$0.object1 < $1.object1}).map{$0.object2}
      }

      I should also note that when I wrote this post Apple used sort() and sorted() but now use sortInPlace() and sort(). Methods called on instances rather than free functions.

      Delete
    6. Thanks for all your help and the sample code but I can't compile it. I had this with your first suggestion in Github. Both snippets above gives this error but I can't work out how to declare them:-
      Use of undeclared type 'A' & type 'B'

      Delete
    7. Sorry, that's blogger removing the treating the angle brackets as HTML. Should be, if this works, func secondarySort&ltl;A: Comparable, B>(arr1:[A], inout arr2:[B]) {
      arr2 = arr1.map{(object1:$0, object2:arr2.removeFirst())}.sort({$0.object1 < $1.object1}).map{$0.object2}
      }

      Delete
    8. one more time:
      func secondarySort<A: Comparable, B>(arr1:[A], inout arr2:[B]) {
      arr2 = arr1.map{(object1:$0, object2:arr2.removeFirst())}.sort({$0.object1 < $1.object1}).map{$0.object2}
      }

      Delete
    9. You are a STAR, thank you so much. If I am ever in Kent I would like to shake your hand!
      Can you please update what's in github. I have one comment which could effect others, this code sorts all the array data 'apart' from the distance so you need the "Distances = Distances.sort(<)" but if you try a reverse sort "Distances = Distances.sort(>)" Distances change but all the other arrays data doesn't change order.

      Delete
    10. Thanks, I've updated gist with ascending and descending sort (default is ascending and doesn't need to be specified).

      Delete
    11. So, sorting with the example data in my post on 9th the snippet works great BUT when I try with my actual arrays which are all strings, it only sorts the Distances, I have also tried with the Distances as Int.
      Also I can't find your code in GitHub, please send a link.

      I am using :-
      secondarySort(idArray, arr2: &descrArray)
      secondarySort(idArray, arr2: &latArray)
      secondarySort(idArray, arr2: &lngArray)
      secondarySort(idArray, arr2: &distArray)
      // the array around which the sort is based must be sorted last
      distArray.sortInPlace(<)
      print("descrArray = \(descrArray)")
      print("distArray = \(distArray)")
      print("idArray = \(idArray)")

      Delete
    12. you need to replace disArray.sortInPlace(<) with idArray.sortInPlace(<). So something like this:

      secondarySort(IDs, arr2: &Distances) // [20, 40, 10, 30]
      secondarySort(IDs, arr2: &Descriptions) // ["close", "furthest", "nearest", "not close"]
      secondarySort(IDs, arr2: &Lats) // [36, 100.12, 28.07, 88.3]
      secondarySort(IDs, arr2: &Longs) // [36, 100.12, 28.07, 88.3]

      IDs.sortInPlace(<) // ["c", "f", "n", "nc"]

      Delete
    13. Sorry Anthony but that just sorts the IDs. Please send link to GitHub to make sure I have the correct func secondarySort().

      Delete
    14. This is driving me mad and probable you too! This is the actual output after sort and where the IDs should be ["1626", "1625", "1627"] and descrArray is wrong :-
      descrArray = ["2 Spaces at Aquadrome", "5 Spaces at Bowlplex", "15 Spaces at Warner Village Cinema"]
      distArray = ["2543", "2618", "2687"]
      idArray = ["1625", "1626", "1627"]

      My arrays are declared like this :-
      var idArray:[String] = []
      and created using :-
      idArray.insert(id, atIndex: row)
      could that be the problem as the example strings we are using work perfectly?

      Delete
    15. As long as you are using insert:atIndex: with the understanding that it inserts (pushing every other value higher than the index on one) and does not replace values then it won't be an issue. It doesn't matter how the array is created if the end result is identical.

      Delete
    16. Found problem. By coincidence my IDs were already in correct order, so the sort failed. I changed sort array to Latitude (which will NEVER be exactly the same) and it works fine.
      latArray.sortInPlace(<)

      For the future could your func secondarySort() be changed to overcome this? If not please make sure the explanation notes warn of this problem.

      Thanks again for all your help.

      Delete
    17. When you write that the "sort failed" do you mean that nothing changed because the array was already in order, or is something else happening?

      Delete
    18. It didn't sort any of the arrays. I ended up with this :-

      secondarySort(latArray, arr2: &descrArray)
      secondarySort(latArray, arr2: &idArray)
      secondarySort(latArray, arr2: &lngArray)
      secondarySort(latArray, arr2: &distArray)
      // the array around which the sort is based must be sorted last and sortInPlace below.
      distArray.sortInPlace(<)

      print("descrArray = \(descrArray)")
      print("idArray = \(idArray)")
      print("distArray = \(distArray)")

      Delete
    19. Yes, the array against which others are sorted must be sorted last. But if the array against which others are sorted is already in order before any sorting is done, then there will be no sorting required to put things into order across any array. This is correct behaviour.

      Delete
    20. I have found that it doesn't seem to sort >3 arrays, the code below works for 3 and I have tried EVERY combination to correct out but have run out of ideas.

      For some reason the distance values are wrong (see output below line) where ID=1619 is actually FARTHEST away.

      func sortBBLocations() {
      secondarySort(latArray, arr2: &descrArray)
      secondarySort(latArray, arr2: &lngArray)
      secondarySort(latArray, arr2: &idArray)
      secondarySort(latArray, arr2: &distArray)
      // the array around which the sort is based must be sorted last
      latArray.sortInPlace(<) // This almost works
      // lngArray.sortInPlace(<)
      // idArray.sortInPlace(<)
      // descrArray.sortInPlace(<)
      distArray.sortInPlace(<) // This almost works
      print("distArray = \(distArray)")

      --------------------------------------------
      distArray = ["2537", "2612", "2682", "3887"]
      idArray = ["1619", "1626", "1625", "1627"]

      Delete
    21. It's very difficult to understand what's going wrong for you without seeing all the code. And I don't understand why you are doing sortInPlace() for all arrays. A sortInPlace(<) is built into Swift and will order everything in ascending order resorting any array that has already been sorted. The point of the secondarySort() is to sort one array based on another. So imagine [3,1,2] and [1,2,3]. Where we want the first array to determine the order and the second array to be the array that is actually reordered. This would result in the second array looking like this: [2,3,1]. The secondarySort() doesn't change the order of the first array however, because as soon as we did that it would no longer provide a reference point for the change in positions that need to take place. This is why we sort the ordering array last. There is no limit to how many times the array can be used before this to order other arrays, because it is not mutated.

      Delete
    22. As I said I don't understand how the sort works in your code or swift's built in, so I apologise for being so stupid. Can I send you my code snippet by email directly? Because I append the data to create the arrays I always thought there must be a way if inserting the new 'columns' in the correct location based on 'size' of the 'row'.

      Delete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Hi Anthony

    Where can I send my code as I am now stuck?

    ReplyDelete
    Replies
    1. Hi Anthony

      Did you get a chance to look at your sort code func, as I haven't been able to made any progress?

      Delete
  4. Hi Anthony

    Did you have any luck improving the sort func as my app crashes when code runs and produces zero value randomly in array.

    ReplyDelete
  5. Hi Anthony

    Would you mind if I asked a question on Stackoverflow and quote some of your code?

    ReplyDelete
  6. Hi Anthony

    I presume I am asking too much, so thanks for your help and goodbye.

    ReplyDelete

Post a Comment