How to sort through an array of dictionary values AND keys??

Hi all,


I am storing different user data in a dictionary with a String (playerID) as the key, and an Int (ranking number) as the value. I have multiple dictionaries for 4-5 players. Each player will have their own dictionary allocated to them with their ranking and playerID as the key. I then want to sort the player's by value (ranking) so that I can get the highest ranked players ranking and playerID. I think the best way to do this is by storing all the dictionaries in an array? I am a little confused, but this is what I have so far.


var allPlayersArray = [[String:Int]]()

var player1DataDict = [String:Int]()
player1DataDict.updateValue(2, forKey: player1IDString)

var player2DataDict = [String:Int]()
player2DataDict.updateValue(1, forKey: player2IDString)

var player3DataDict = [String:Int]()
player3DataDict.updateValue(3, forKey: player3IDString)

allPlayersArray.append(player1DataDict)
allPlayersArray.append(player2DataDict)
allPlayersArray.append(player3DataDict)

//Do some sort of sorting to allPlayersArray??


I want to sort allPlayersArray based on ranking so that I can do something like:


var highestRankedPlayer = allPlayersArray[0] as Dictionary


This way I can get the ranking and playerID from the variable highestRankedPlayer, in this case it would return player2DataDict as his ranking value is 1.

Answered by OOPer in 57409022

Sorry just tried OOPER's technique and it's telling me that [String:Int] from 'allPlayersRanking' does not have a member named 'sort'

I checked my code only in the latest version of Xcode 7, while I couldn't find what version of Xcode you are using.

If you are using Xcode 6.4, you need to change the line as:

let sortedRanking = sorted(allPlayersRanking){$0.1 < $1.1} //<-Swift 1.2


But, as Jens already suggested, you'd better have a Player class or struct.


EDIT: A simple example using Player class, which runs on Xcode 6.4 playground.

class Player {
    let id: String
    var rank: Int
    var position: CGPoint = CGPoint(x: 0, y: 0)
    func distanceTo(point: CGPoint) -> CGFloat {
        let dx = position.x - point.x
        let dy = position.y - point.y
        return sqrt(dx * dx + dy * dy)
    }
    init(id: String, rank: Int) {
        self.id = id
        self.rank = rank
    }
}
let allPlayers = [
    Player(id: "Foo", rank: 2),
    Player(id: "Bar", rank: 1),
    Player(id: "Baz", rank: 3)
]
allPlayers[0].position = CGPoint(x: 100, y: 100)
allPlayers[1].position = CGPoint(x: 110, y: 100)
allPlayers[2].position = CGPoint(x: -10, y: -10)
var startingPoint = CGPoint(x: 0, y: 0)
let sortedByDistance = sorted(allPlayers){$0.distanceTo(startingPoint) < $1.distanceTo(startingPoint)}
println(sortedByDistance[0].id) //->Baz
let sortedByRank = sorted(allPlayers){$0.rank < $1.rank}
println(sortedByRank[0].id) //->Bar
let sortedByIdDesc = sorted(allPlayers){$0.id > $1.id}
println(sortedByIdDesc[0].id) //->Foo


ADDITION2: If you can use Swift2, and you just need only top one player, using minElement or maxElement is more efficient.

let topByDistance = allPlayers.minElement{$0.distanceTo(startingPoint) < $1.distanceTo(startingPoint)}!
print(topByDistance.id) //->Baz
let topByRank = allPlayers.minElement{$0.rank < $1.rank}!
print(topByRank.id) //->Bar
let bottomById = allPlayers.maxElement{$0.id < $1.id}!
print(bottomById.id) //->Foo

If you can accept tuple rather than Dictionary, you can do the similar thing like this:

var allPlayersRanking: [String: Int] = [:]
allPlayersRanking[player1IDString] = 2
allPlayersRanking[player2IDString] = 1
allPlayersRanking[player3IDString] = 3
let sortedRanking = allPlayersRanking.sort{$0.1 < $1.1}
var highestRankedPlayer = sortedRanking[0]
print(highestRankedPlayer) //->("player2IDString", 1)
// I added these three lines to make your example complete:

let player1IDString = "Foo"
let player2IDString = "Bar"
let player3IDString = "Baz"


// Your code unmodified:

var allPlayersArray = [[String:Int]]()

var player1DataDict = [String:Int]()
player1DataDict.updateValue(2, forKey: player1IDString)

var player2DataDict = [String:Int]()
player2DataDict.updateValue(1, forKey: player2IDString)

var player3DataDict = [String:Int]()
player3DataDict.updateValue(3, forKey: player3IDString)

allPlayersArray.append(player1DataDict)
allPlayersArray.append(player2DataDict)
allPlayersArray.append(player3DataDict)


// I added the following to show what your array looks like:

print(allPlayersArray) // [["Foo": 2], ["Bar": 1], ["Baz": 3]]

allPlayersArray.sortInPlace { (playerDictA: [String: Int], playerDictB: [String: Int]) -> Bool in
    guard let rankA = playerDictA.first else { fatalError("No playerID/rank!") }
    guard let rankB = playerDictB.first else { fatalError("No playerID/rank!") }
    return rankA.1 < rankB.1
}

print(allPlayersArray) // [["Bar": 1], ["Foo": 2], ["Baz": 3]]

var highestRankedPlayer = allPlayersArray[0]
print(highestRankedPlayer) // ["Bar": 1]


But IMHO you should rethink your design.

Why should the player's id and rank be represented as the (single) key-value pair of a Dictionary?


Here's an example of why I think your current design is problematic:

In many parts of your code (as in the sorting code above) you'll have no idea what the player's id is, which makes it hard to get the rank. It's also not guaranteed to be there at all, etc. I had to write that ugly sorting code with guards, fatalErrors and getting the optional .first element out of the dictionary because of that, even though that sorting code has no reason at all to care/know anything about the player id.


Would it be possible to represent a player with eg a simple struct like:

struct Player {
    let id: String
    let rank: Int
}

?


If so, the equivalent of the above would be:

struct Player : CustomStringConvertible {
    var description: String { return "Player \(id) with rank \(rank)" }
    let id: String
    let rank: Int
}
let allPlayers = [
    Player(id: "Foo", rank: 2),
    Player(id: "Bar", rank: 1),
    Player(id: "Baz", rank: 3)
]
print(allPlayers) // [Player Foo with rank 2, Player Bar with rank 1, Player Baz with rank 3]

let playersSortedByRank = allPlayers.sort { $0.rank < $1.rank }
print(playersSortedByRank) // [Player Bar with rank 1, Player Foo with rank 2, Player Baz with rank 3]

let highestRankedPlayer = playersSortedByRank[0]
print(highestRankedPlayer) // Player Bar with rank 1

**EDIT**


Just tried both of your techniques and it's telling me that [String:Int] from 'allPlayersRanking' does not have a member named 'sort' and 'sortInPlace'


I'm actually using the dictionary to hold the player's distance from the starting location (CGPoint). I wanted to use a dictionary this way I could easily look up the player's distance value by using their playerID as the key.


Before I click on the correct answer button, is there a better way to store each player's distance from the starting point data in a way I could easily look it up using the playerID for that player? I also wanted it sorted so that I could see who is closest to the starting point.

Accepted Answer

Sorry just tried OOPER's technique and it's telling me that [String:Int] from 'allPlayersRanking' does not have a member named 'sort'

I checked my code only in the latest version of Xcode 7, while I couldn't find what version of Xcode you are using.

If you are using Xcode 6.4, you need to change the line as:

let sortedRanking = sorted(allPlayersRanking){$0.1 < $1.1} //<-Swift 1.2


But, as Jens already suggested, you'd better have a Player class or struct.


EDIT: A simple example using Player class, which runs on Xcode 6.4 playground.

class Player {
    let id: String
    var rank: Int
    var position: CGPoint = CGPoint(x: 0, y: 0)
    func distanceTo(point: CGPoint) -> CGFloat {
        let dx = position.x - point.x
        let dy = position.y - point.y
        return sqrt(dx * dx + dy * dy)
    }
    init(id: String, rank: Int) {
        self.id = id
        self.rank = rank
    }
}
let allPlayers = [
    Player(id: "Foo", rank: 2),
    Player(id: "Bar", rank: 1),
    Player(id: "Baz", rank: 3)
]
allPlayers[0].position = CGPoint(x: 100, y: 100)
allPlayers[1].position = CGPoint(x: 110, y: 100)
allPlayers[2].position = CGPoint(x: -10, y: -10)
var startingPoint = CGPoint(x: 0, y: 0)
let sortedByDistance = sorted(allPlayers){$0.distanceTo(startingPoint) < $1.distanceTo(startingPoint)}
println(sortedByDistance[0].id) //->Baz
let sortedByRank = sorted(allPlayers){$0.rank < $1.rank}
println(sortedByRank[0].id) //->Bar
let sortedByIdDesc = sorted(allPlayers){$0.id > $1.id}
println(sortedByIdDesc[0].id) //->Foo


ADDITION2: If you can use Swift2, and you just need only top one player, using minElement or maxElement is more efficient.

let topByDistance = allPlayers.minElement{$0.distanceTo(startingPoint) < $1.distanceTo(startingPoint)}!
print(topByDistance.id) //->Baz
let topByRank = allPlayers.minElement{$0.rank < $1.rank}!
print(topByRank.id) //->Bar
let bottomById = allPlayers.maxElement{$0.id < $1.id}!
print(bottomById.id) //->Foo
How to sort through an array of dictionary values AND keys??
 
 
Q