3

I currently use the sort function to sort my div elements based on the count value. Here's how it's being done now: (I'm not sure if it's an efficient method or not..)

$('#list .list_item').sort(sortDescending).appendTo('#list');

function sortDescending(a, b) {
  return $(a).find(".count").text() < $(b).find(".count").text() ? 1 : -1;
};

I'm thinking of adding a timestamp field and am unsure how I can extend it to support this.

I have a list of div elements with its own count and date/time/timestamp. Here's how the html code would look like:

<div id="list">
<div id="list_item_1" class="list_item">
  <div class="count">5</div>
  <div class="timestamp">1272217086</div>
  <div class="text">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis ipsum rutrum metus rhoncus feugiat non vel orci. Etiam sit amet nisi sit amet est convallis viverra</div>
</div>
<div id="list_item_2" class="list_item">
  <div class="count">5</div>
  <div class="timestamp">1272216786</div>
  <div class="text">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis ipsum rutrum metus rhoncus feugiat non vel orci. Etiam sit amet nisi sit amet est convallis viverra</div>
</div>
<div id="list_item_3" class="list_item">
  <div class="count">10</div>
  <div class="timestamp">1272299966</div>
  <div class="text">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis ipsum rutrum metus rhoncus feugiat non vel orci. Etiam sit amet nisi sit amet est convallis viverra</div>
</div>
</div>

I would like to sort by count (decreasing), followed by timestamp (decreasing - newest at the top).

Any help is greatly appreciated! Thanks :)

Lyon
  • 7,354
  • 10
  • 33
  • 46

2 Answers2

5

Only the sort comparator function needs to change. I'm sure there are plugins available to do this, and you might want to take a look at them, but implementing what you want is fairly trivial. The sortDescending method gets two divs each time, and comparison must follow the criteria you've specified:

  1. First by count
  2. If count is equal, then by timestamp
  3. If timestamps are equal, then return 0

Here's the ugly straightforward non-optimized version:

function sortDescending(a, b) {
    if(getCount(a) < getCount(b)) {
        return -1;
    }
    else if(getCount(a) > getCount(b)) {
        return 1;
    }
    else if(getTimestamp(a) < getTimestamp(b)) {
        return -1;
    }
    else if(getTimestamp(a) > getTimestamp(b) {
        return 1;
    }
    else {
        return 0;
    }
}

If you see the if-else structure, it may seem obvious that you can genericize this approach to be able to handle any type of custom ordering. So here's a jab at a sortBy method that takes in a number callback functions, where each callback defines one sorting criteria.

function sortBy() {
    var callbacks = arguments;

    return function(a, b) {
        for(var i = 0; i < callbacks; i++) {
            var value = callbacks[i](a, b);
            if(value != 0) {
                return value;
            }
        }
        return 0;
    };
}

Then pass all criteria's as callbacks to this sortBy function. Here's a rough example for your code:

function compareCount(a, b) {
    return getCount(a) - getCount(b);
}

function compareTimestamp(a, b) {
    return getTimestamp(a) - getTimestamp(b);
}

$("selector").sort(sortBy(compareCount, compareTimestamp));

And while we are at it, let's also make a jQuery plugin out of this. It will have a nice and easy interface:

$("parent selector").sortBy("child selector 1", "child selector 2", ...);

The idea is to pass a jQuery selector that will select a node whose text will determine the value to sort by. We will give integers a higher priority and first try to sort numerically if both values are so, otherwise do a regular comparison.

jQuery.fn.sortBy = function() {  
    var selectors = arguments;

    this.sort(function(a, b) {
        // run through each selector, and return first non-zero match
        for(var i = 0; i < selectors.length; i++) {
            var selector = selectors[i];

            var first = $(selector, a).text();
            var second = $(selector, b).text();

            var isNumeric = Number(first) && Number(second);
            if(isNumeric) {
                var diff = first - second;
                if(diff != 0) {
                    return diff;
                }
            }
            else if(first != second) {
                return first < second ? -1 : 1;
            }
        }

        return 0;
    });

    this.appendTo(this.parent());

    return this;
};

Use as

$('#list .list_item').sortBy('.count', '.timestmap');

See an example of the plugin here.

Btw, none of this will actually sort the elements in the document itself. See this question for how to do that.

Community
  • 1
  • 1
Anurag
  • 140,337
  • 36
  • 221
  • 257
  • Thanks Anurag. I'm not sure if I lost you somewhere but both the plugin method and the non-plugin method could not sort the divs. I used this to sort: `$('#list .list_item).sort(sortBy(compareCount, compareTimestamp)).appendTo('#list');` – Lyon Jun 25 '10 at 09:20
  • That's right Lyon. These methods only sort the list in the jQuery wrapped object, not in the document. See the related [question](http://stackoverflow.com/questions/3050830/reorder-list-elements-jquery/3051100#3051100). `appendTo` works just as well, and you can move it into the plugin or function too. – Anurag Jun 25 '10 at 14:46
  • @Anurag: I only managed to get the non-plugin version to work. I'm really lost with implementing the plugin thhough. Could you guide me on how I can use your plugin to sort my divs based on 2 parameters? Thanks :) – Lyon Jun 25 '10 at 17:18
  • @Lyon - yeah that plugin code wasn't complete - fixed it now, and example here - http://jsfiddle.net/qJCew/ – Anurag Jun 25 '10 at 23:07
  • @Anurag: Thanks! that really worked very well. I flipped the `first-second` and the comparator `>` to get a descending order. Can I ask, 2 side questions...if I want to add pagination capability to your plugin, conceptually, how would I go about doing it? Also..is what i'm doing the best way, performance wise? Should I perhaps store everything in an array and sort before outputting. As jAndy mentioned, he wouldn't be parsing these values through `element`s. I'm currently fetching the list via an xml file. What wld you do? Thanks, im learning a lot due to your explanations and guidance. :) – Lyon Jun 26 '10 at 03:34
  • 1
    @Lyon - to add pagination, and still keep things simple, I would separate the content by pages, and then the same sortBy plugin can be used as in `$("#list #page-1").sortBy('..', '..', '..');`. How many list items/pages are you expecting? If it's below 200-300, I wouldn't bother with optimizations much. jQuery's resultset isn't live, so directly sorting that list is fine. However, you may see performance gains by collecting all values in a separate array first, then sorting those. I did some performance tests for something very similar on a SO question. Will post the link if I can find it. – Anurag Jun 26 '10 at 06:48
  • Also the above version comment about pagination will provide paginating within a single page results. If you want overall pagination, you will have to resort the entire array, and then update pages accordingly. Found it - http://stackoverflow.com/questions/2910341/ie-8-prompts-user-on-slow-jquery-script/2911908#2911908 and test results are here - http://jsfiddle.net/hwxmJ/4/ – Anurag Jun 26 '10 at 06:49
  • Thanks Anurag, I'll give it a go! Really appreciate your advice and help on this. :) – Lyon Jul 06 '10 at 02:41
1

Performance:

I'd highly recommend to cache the wrapped set before performing a sort().

var $items = $('#list .list_item');
$items.sort(sortDescending);

That should give sizzle / your DOM a break like bigtime.

To link your timestamp value aswell, you have to extend your sorting.

function sortDescending(a, b) {
  var a_count = parseInt((a).find(".count").text(), 10),
      b_count = parseInt((b).find(".count").text(), 10),
      a_time  = parseInt((a).find(".timestamp").text(), 10),
      b_time  = parseInt((b).find(".count").text(), 10);

  return (a_count < b_count && a_time > b_time);
};

While writting this I actually realized that I wouldn't parse those values through element content. If you generate those nodes somewhere dynamically, it's maybe a better idea to use jQuerys $.data() method to store date and use/access that within your sorting method.

jAndy
  • 231,737
  • 57
  • 305
  • 359
  • May I ask what the difference between "var $items =" and "var items =" is? e.g. the additional $ sign. I noticed that if the .count field is 10 and above, it is sorted below 0, when it should be above "9". Is there a way to deal with such? Thanks! – Lyon Jun 25 '10 at 07:26
  • 1
    @Lyon: `$items` or `items` makes no difference, it's just to remember that this variable is holding a jQuery wrapped set. For the second issue, see my update, I forgot the base nummer for parseInt(). – jAndy Jun 25 '10 at 07:35
  • @Andy: Thanks. I tried implementing your sorting function but it wouldn't sort. As in, the order is as per default from the server. Regarding your suggestion to use $.data, I'm actually fetching a xml file with all the list items, then iterating through it and adding to the document. I suppose I could store it all in $.data, then iterate through that to display the lists. How would sorting be done in that manner then and would it be more efficient/performance-wise? Thanks for your help! – Lyon Jun 25 '10 at 07:53
  • @Andy: while finding out more information about $.data, i found out it doesn't work on the iPhone mobile safari. I need this to run on desktop and mobile browsers so I suppose using $.data is out of the question now? – Lyon Jun 25 '10 at 09:21
  • 1
    @Lyon: Well accessing a `data object` would look like `$.data(a, 'identifier');` within the sort method. But you are right, the mobile Safari browser from the iPhone seems to have trouble storing/accessing jQuery `data` objects. – jAndy Jun 25 '10 at 09:25
  • @Andy: Thanks! I've been trying to get your code to work. parseInt() fixed sorting counts >9. But sorting timestamp does not. Could it be the type of value that's being sorted? the timestamp values are all from php's time() where it's seconds since 1970. I also noticed that only this "works" `a_count < b_count && a_time < b_time` but `a_count < b_count && a_time > b_time` doesn't, where timestamp is sorted descreasingly... any thoughts? – Lyon Jun 25 '10 at 10:20