tags:

views:

73

answers:

2

Hi SO-ers,

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 :)

+1  A: 

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
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
@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
@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
@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
@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
Lyon
+1  A: 

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.

Anurag
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
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
@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
@Lyon - yeah that plugin code wasn't complete - fixed it now, and example here - http://jsfiddle.net/qJCew/
Anurag
@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
@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
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
Thanks Anurag, I'll give it a go! Really appreciate your advice and help on this. :)
Lyon