1

My app has a top scores table, which uses .queryOrdered(byChild: "userHighScore") and .queryLimited(toLast: 100) to get the 100 highest scores:

func getTopScores() {
    var scores = [String]()

    self.reference
        .child("users")
        .queryOrdered(byChild: "userHighScore")
        .queryLimited(toLast: 100)
        .observeSingleEvent(of: .value, with: { (snapshot) in

            for child in snapshot.children.reversed() {

                if let dataSnapshot = child as? DataSnapshot, let user = dataSnapshot.value as? [String:AnyObject] {

                    if let userHighScore = user["userHighScore"] as? Int {
                        var stringToAppend = "\(scores.count+1). "


                        if let userName = user["userName"] as? String {
                            stringToAppend += " \(userName)   •  \(userHighScore)"
                        }

                        scores.append(stringToAppend)
                    }
                }
            }
        })
    }

The problem is when there's a tie score.

Ideally, the first user to get that score would be above the others with the same score on the table (and the most recent user to get that score would be the lowest of those with the same score).

However, the table displays those with tied scores in the same random order that they appear in the Firebase Realtime Database.

Is there a way to sort tied scores chronologically?

I've spun my wheels on this for many hours now. Thanks in advance for any help!

Derence
  • 408
  • 5
  • 18

2 Answers2

6

Ideally, the first user to get that score would be above the others with the same score on the table

You're essentially trying to order on two properties: userHighScore and userHighScoreTimestamp (I made up a name, since you didn't specify it in your question). The Firebase Realtime Database can only query on a single property. See Query based on multiple where clauses in Firebase

What you can do is define an additional property that combines the values of the high score and the timestamp that is was accomplished:

users: {
  derenceUid: {
    userHighScore: 42,
    userHighScoreTimestamp: 1557695609344,
    userHighScore_Timestamp: "000042_1557695609344",
  },
  dougUid: {
    userHighScore: 31,
    userHighScoreTimestamp: 1557609264895,
    userHighScore_Timestamp: "000005_1557609264895",
  },
  pufUid: {
    userHighScore: 42,
    userHighScoreTimestamp: 1557695651730,
    userHighScore_Timestamp: "000042_1557695651730",
  }
}

With this structure you can get the most-recent highest-scoring user with:

ref.queryOrdered(byChild: "userHighScore_Timestamp"). queryLimited(toLast: 1)

One thing to note tis that I padded the user's high score to be six digits. This is necessary since Firebase will do a lexicographical comparison of these keys, and without the padding "5_1557609264895" would be larger than "42_1557695609344". You'll have to figure out how many digits your maximum score can hold. In my example I used 6 digits, so it can hold scores up to 1 million.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • It's working! Thanks puf for taking the time to share your expertise and for your kindness in explaining it clearly. – Derence May 12 '19 at 23:09
  • 1
    Note: Using this concept worked perfectly well to get the tied scores to be arranged chronologically; however, as Frank mentioned, this structure puts the most-recent highest-scoring in the top spot on the table. For my purposes, I needed the most-recent user to be ranked lower than any earlier users in the case of a tie. To achieve this, I just subtracted the time stamp from a much higher number (I chose 5000000000) before tacking it onto the high score. There's probably a more elegant way to do this, but I'm a beginner and feel quite proud about how it's working now :D – Derence May 12 '19 at 23:22
  • 1
    Ah, good point on needing the first and not the latest highscore. Subtracting it from a sufficiently high number indeed does the trick in that case. I hope you have a few more zeroes in there though, e.g. `new Date(5000000000000) = Fri Jun 11 2128` (that's 3 more zeroes than in your comment). – Frank van Puffelen May 13 '19 at 01:21
  • 1
    Yes, that is better. (And I just set a reminder for 109 years from now for the robot into which my consciousness will have been uploaded to check back in on this and increase if needed.) Thanks again for your direction in solving this! I really appreciate it :) – Derence May 13 '19 at 02:57
1

Ideally, what you need is orderByChild for two values, score and date. As far as i know Firebase Realtime Database does not support that. I could suggest two alternatives:

  1. Store scores not as strings but as objects, including two values: "Score" and "Timestamp". Then when you get the list of score objects, sort them on your client side by score, and then sort the ties again by date.
  2. Add a small value to each score based on the date. The value needs to be small enough so it won't affect the score. For example: calculate number of minutes from epoch (right now it's about 25961) divide by 1 million, should give you 0.025961. Adding that to your every score won't change score sorting, and will help to keep them chronological. Just don't forget to get rid of the fractional part when presenting the scores.
bbenis
  • 485
  • 2
  • 6
  • 10
  • 1
    Thank you @bbenis! I hadn't heard of epoch yet and that sent me on a path to understanding Frank van Puffelen's answer and figuring this all out. – Derence May 12 '19 at 23:11