Sorting strings in reverse order with backbone.js

16,897

Solution 1

There are two versions of the comparator function that you can use, either the sortBy version - which was shown in the example, which takes one parameter, or sort - which you can return a more standard sort function, which the documentation says:

"sortBy" comparator functions take a model and return a numeric or string value by which the model should be ordered relative to others. "sort" comparator functions take two models, and return -1 if the first model should come before the second, 0 if they are of the same rank and 1 if the first model should come after.

So in this case, we can write a different comparator function:

var Chapter  = Backbone.Model;
var chapters = new Backbone.Collection;

chapters.comparator = function(chapterA, chapterB) {
  if (chapterA.get('title') > chapterB.get('title')) return -1; // before
  if (chapterB.get('title') > chapterA.get('title')) return 1; // after
  return 0; // equal
};

chapters.add(new Chapter({page: 9, title: "The End"}));
chapters.add(new Chapter({page: 5, title: "The Middle"}));
chapters.add(new Chapter({page: 1, title: "The Beginning"}));

alert(chapters.pluck('title'));

So you should get as a response:

"The Middle", "The End", "The Beginning"

Solution 2

You could:

  • grab the char code for each character in the string,
  • subtract each value from 0xffff (the maximum return value of string.charCodeAt),
  • use String.fromCharCode to turn that back into string of "negated" characters

and that will be your sorting key.

chapters.comparator = function(chapter) {
    return String.fromCharCode.apply(String,
        _.map(chapter.get("title").split(""), function (c) {
            return 0xffff - c.charCodeAt();
        })
    );
}

And voila:

> console.log(chapters.pluck('title'));
["The Middle", "The End", "The Beginning"]

Note: if your comparison strings are long (as in 65 kb or more), you may run into trouble (see Matt's comment below). To avoid this, and speed up comparisons a bit, just use a shorter slice of your comparison string. (In the above example, you could go for chapter.get("title").slice(0, 100).split("") instead.) How long a slice you need will depend on your application.

Solution 3

If you're working with non-numerical values, there is no obvious way to do a reverse sort. Backbone makes use of the _.sortBy() and _.sortedIndex() methods from Underscore to order the models based on the comparator, and these methods automatically sort in ascending order. The naive way to do this would be to use chapters.pluck('title').reverse(), as the result of pluck will be an array. But calling reverse on some Collection methods will reverse the Collection models in place, so next time you call it, the models will be back in ascending order. You could always do something like:

var results = [],
    titles  = chapters.pluck('title');

for(var i=0, len=titles.length; i<len; i++) {
  results.push(titles[i]);
}

results.reverse();

This would not affect the models array in your Backbone collection, as it would create a completely new results array in memory, but retain references to the original models, so calling things like save would still update the Collection state.

But that's not very elegant, and creates a lot of extra coding throughout your project any time you want to reverse the results. I think we can do better.

In order to make this work, you'll need to perform a bit of unwieldy JavaScript ninjary in your comparator method to make this work - note this is untested:

chapters.comparator = function(chapter) {
  var alphabet = '0123456789abcdefghijklmnopqrstuvwxyz',
      title = chapter.get('title').toLowerCase(),
      inverse_title = '',
      index;

  for(var i=0, len=title.length; i<len; i++) {
    index = alphabet.indexOf(title.charAt(i));

    if(index === -1) {
      inverse_title += title.charAt(i);
      continue;
    }

    inverse_title += alphabet.charAt(alphabet.length - index - 1);
  }

  return inverse_title;
};

This concept probably needs improving to take into account symbols, etc., but essentially it inverts the comparator string in such a way that "Z" becomes "0", "Y" becomes "1", etc., which should produce the reverse sort you're after.

Solution 4

As Backbone merely uses the .sortBy method, simply proxy in your own logic:

collectionInQuestion.sortBy = function () {
  var models = _.sortBy(this.models, this.comparator);
  if (forSomeReason) {
    models.reverse();
  }
  return models;
};

..or add it somewhere else..

TweakedCollection = Backbone.Collection.extend({ sortBy: [...] })
Share:
16,897
Emil Stenström
Author by

Emil Stenström

Swedish interface developer and web strategist that blogs about the modern web and how to make best use of it. Have a computer science degree behind me, and worked as a technical consultant for five years. Now runs a startup around customer service.

Updated on June 18, 2022

Comments

  • Emil Stenström
    Emil Stenström almost 2 years

    I'm trying to sort a Backbone.js collection in reverse order. There are previous replies on how to do this with integers, but none with strings.

    var Chapter  = Backbone.Model;
    var chapters = new Backbone.Collection;
    
    chapters.comparator = function(chapter) {
      return chapter.get("title");
    };
    
    chapters.add(new Chapter({page: 9, title: "The End"}));
    chapters.add(new Chapter({page: 5, title: "The Middle"}));
    chapters.add(new Chapter({page: 1, title: "The Beginning"}));
    
    alert(chapters.pluck('title'));
    

    The above code sorts the chapters from A -> Z, but how do I write a comparator that sorts it from Z -> A?

  • reconbot
    reconbot about 12 years
    Your comparator function can now be what's passed to sort() removing the need for this hackery. See github.com/documentcloud/backbone/issues/488
  • Matthew
    Matthew about 12 years
    @Filip, I ran into an issue where fromCharCode.apply would lock up Safari when called with very long arrays. An array with a length greater than 65536 would cause it to fail completely and throw a RangeError. Perhaps this comparator function can be made safer (and potentially more efficient) by extracting a slice of the first n characters from the comparison string.
  • Emil Stenström
    Emil Stenström almost 12 years
    That does not work for strings, which this post is all about.
  • flyfisher1952
    flyfisher1952 over 11 years
    This doesn't work in all cases. When sorting the strings 1) '2010-2013', 2) '2011-2013', 3) '2006-2010' asc order is 3 1 2 and desc order should be 2 1 3. But what you actually get is 1 2 3 for desc order.
  • JD Isaacks
    JD Isaacks over 11 years
    You should convert the strings to the same case before doing this.
  • Dexygen
    Dexygen over 10 years
    Down-modded due to performance concerns if you have thousands of records, which I do
  • Fasani
    Fasani over 10 years
    I created a variation of this which also works with numbers as well as as string. Can be found here: stackoverflow.com/questions/5013819/…
  • Fasani
    Fasani over 10 years
    You can keep the collection sorted in reverse order by using this comparator: stackoverflow.com/questions/5013819/…
  • Dan Smart
    Dan Smart over 10 years
    Nice thinking to make a less specific version.
  • Maximilian Hils
    Maximilian Hils about 9 years
    Note that this method is not always correct. For example, the order of foo and foobar is not reversed. This can be mostly solved by appending String.fromCharCode(0xffff) at the end of the string.