3

I have a case where I'm provided with one UL, and an unknown number of LIs. I'm looking to use JS to split the LIs into 2 ULs (I'm using jQuery). I'm able to split the ULs evenly by number of LIs, but I'd like to split it based on the height of each LI as well, so that both ULs are close to the same height.

Any help with this would be appreciated, I don't feel like I'm getting anywhere with the approach I started with.

Thanks.

EDIT: JS code I currently have. The HTML is just a straight UL/LI, each LI can be of varying height.

var $sections = $('div.subsection');

$sections.each(function(){
  var $section = $(this);

  var $list = $section.children('ul');
  var $items = $list.children('li');
  var itemCount = $items.size();
  var leftover = itemCount % 2;
  var itemsPerColumn = Math.floor(itemCount / 2);
  var $newList = $('<ul />');

  $items.each(function(){
    var $this = $(this);
    var index = $items.index($this);

    if (index >= (itemsPerColumn + leftover)) {
      $this.remove().appendTo($newList);
    }
  });

  $list.after($newList);

  _equalizeListHeights();

  function _equalizeListHeights(){
    var listHeight = $list.height();
    var newListHeight = $newList.height();

    if (listHeight > newListHeight){
      var $lastItem = $list.children('li:last');
      var lastItemHeight = $lastItem.height();

      if (listHeight - lastItemHeight > newListHeight + lastItemHeight){
        $lastItem.remove().prependTo($newList);
        _equalizeListHeights();
      }
    }
  }

});
Alex Heyd
  • 1,323
  • 1
  • 10
  • 17

4 Answers4

2

You can do it via CSS:

.double_column_list li {float: left; width: 50%;}

<ul class="double_column_list">
    <li>Awesome Stuff Awesome Stuff Awesome Stuff Awesome Stuff Awesome Stuff</li>
    <li>Awesome Stuff</li>
    <li>Awesome Stuff</li>
    <li>Awesome Stuff</li>
</ul>

To get 3 column, set width: 33.333%, 4 column width: 25% and so on.

Of course, if you keep increasing the height of one li to a point where rest of the lis can't match, this would look bad. But then, that issue cannot be fixed through JS either.

http://jsfiddle.net/rQJQb/

Update: As pointed out by commenters, if list items are not sorted by height (i.e. height of any one list item in the middle may be bigger/smaller than the ones preceding it), a sorting is needed: http://jsfiddle.net/rQJQb/2/

Mrchief
  • 75,126
  • 20
  • 142
  • 189
  • [Doesn't always work as intended](http://jsfiddle.net/Eric/rQJQb/1/) (borders added for clarity). – Eric Aug 02 '11 at 15:31
  • @Eric: That would be a problem anyway, unless it is Ok to reorder the list items based on their height. A pre-flight of list items to sort based on heights would be required. – Mrchief Aug 02 '11 at 15:35
  • That's not what I'm trying to achieve. It's not extra columns that I'm having issues with, it's arranging the LIs in such a way that both columns are close to the same height. Say I have my original UL of 10 items, if each LI were the same height, I'd split it 5/5. But if one LI is 3x taller than any other LI, then I'd split it 3/7. Make sense? – Alex Heyd Aug 02 '11 at 15:35
  • The CSS will do that as long as your list items are sorted by height. IF not, try this: http://jsfiddle.net/rQJQb/2/ – Mrchief Aug 02 '11 at 15:43
  • There's absolutely no way that CSS can resolve my problem... Please read my question more carefully... – Alex Heyd Aug 02 '11 at 15:48
  • @alex heyd: Can you update my fiddle to show what is not working for you? Or how you'd want it to be? – Mrchief Aug 02 '11 at 15:50
  • @Mrchief, sorry I was a little hasty, your approach with CSS is viable after all, not as robust as physically splitting the UL into 2 however as Eric points out above, but your method is something worth considering definitely, thanks – Alex Heyd Aug 02 '11 at 15:57
  • @Eric: It's not a corner case if you increase the width of HTML pane a bit but I see your point. – Mrchief Aug 02 '11 at 16:00
  • @alex heyd: Glad you found it useful. As Eric points out, if you have lot many corner cases as he pointed out, then probably its not the way to go (although I'm hopeful that with a bit of fiddling that can be handled as well). – Mrchief Aug 02 '11 at 16:03
1

I think I can at least see the approach here:

  1. Calculate the total height of all the list items (total), and store all the individual heights
  2. Calculate the height of one list (total / 2)
  3. Determine an algorithm to sum a set of heights to come as as possible to total / 2, without exceeding it.
  4. Put the elements with these heights into the first list, and put the rest into the second

Step 3 is the tricky bit. It's related to the Subset Sum Problem.


EDIT

Here's a brute-force algorithm which solves your problem. It doesn't run on window.resize, because that would be silly. If you want to see it change, resize the result window, then push run.

//Sum a jQuery wrapped array
$.fn.sum = function() {
    var total = 0;
    this.each(function() { total += this; });
    return total;
};
//Mask elements with a bitmask
$.fn.mask = function(mask) {
    return this.filter(function(i) {
        return (mask >> i) & 1;
    })
}

//Get the sizes, and sneakily return a jQuery object
var sizes = $('.first li').map(function() { return $(this).outerHeight() });

var total = sizes.sum();
var maxTotal = total / 2;

var best = {
    total: 0,
    mask: 0
}

for (var subsetMask = 1; subsetMask < (1 << sizes.length); subsetMask++) {
    //Sum all the heights in this subset
    var subsetTotal = sizes.mask(subsetMask).sum();

    //New optimal solution?
    if (subsetTotal > best.total && subsetTotal <= maxTotal) {
        best = {
            total: subsetTotal,
            mask: subsetMask
        };
    }
}

$('.first li').mask(best.mask).appendTo('.second');
Community
  • 1
  • 1
Eric
  • 95,302
  • 53
  • 242
  • 374
  • Agreed. I asked the question over at math.stackexchange like you suggested, hopefully they can help with the algorithm part. – Alex Heyd Aug 02 '11 at 15:37
  • Thanks for the link to the Subset Sub Problem. Led me to this which is exactly the type of algorithm I'm looking for: http://en.wikipedia.org/wiki/Partition_problem – Alex Heyd Aug 02 '11 at 15:44
  • @alex heyd: That's where I came from to find the Subset Sum Problem! – Eric Aug 02 '11 at 15:46
  • An unknown number. Realistically though, it should be around 10ish max (soft max) – Alex Heyd Aug 02 '11 at 16:02
  • Yes it would be. I came across the "greedy" solution (http://stackoverflow.com/questions/6597180/divide-an-array-into-two-sets-with-minimal-difference), which although not perfect, may be suitable for what I need (if I'm unable to suitably refactor the code Cam linked to) – Alex Heyd Aug 02 '11 at 16:07
  • Can you guarantee there are fewer than 53? That's when I run out of bits for my brute force solution. – Eric Aug 02 '11 at 16:13
  • Yeah I can guarantee there'd be less than 53 – Alex Heyd Aug 02 '11 at 16:14
  • No problem. I might want to use this in future anyway. It might stop working at 32 elements, depending on how many bits subsetMask has. – Eric Aug 02 '11 at 17:23
1

The CS problem you are trying to solve

You should look into the Backpack Problem. The items to be 'inserted into the backpack' will be the LIs. Each LI will have a weight and value equivalent to its height. The backpack capacity should be half the sum of all the LI heights. Run an algorithm to solve the backpack problem and your LIs will be divided into two sets with heights as you've described.

Intuitive explanation

The backpack algorithm finds a subset of items such that the value is as large as possible, but the weight doesn't exceed the backpack capacity. But our weight and value of each LI is its height, and the backpack capacity is half the total height of all LIs combined.

So essentially what it will give us is a set of all LIs such that the height is as high as possible without exceeding 1/2 the total height. In the case where you should end up with two equal-height sets of LIs, this will be one of the sets. In the case where you should end up with two sets of LIs with different heights, the backpack problem solution will return the set with a smaller height (and the remaining, unchosen LIs would be the second set).

Solution

Try the code used here: http://rosettacode.org/wiki/Knapsack_problem/Bounded#JavaScript

Or if you want to roll your own (not recommended - why bother?): http://en.wikipedia.org/wiki/Knapsack_problem#Unbounded_knapsack_problem

Community
  • 1
  • 1
Cam
  • 14,930
  • 16
  • 77
  • 128
  • Thanks, very helpful. I'll need to refactor the code from the link you posted, but it seems to handle my situation – Alex Heyd Aug 02 '11 at 16:05
  • @alex: np. Depending on the nature of your project, just be careful of licensing issues with that code (check the rosetta code license info). Otherwise I'm sure you can find another implementation in JS, or worst-case in another language and you can port it over. – Cam Aug 02 '11 at 16:06
0
$("ul.first li").each(function() {
    if ($("ul.first").outerHeight() > $("ul.second").outerHeight()) {
        $(this).appendTo($("ul.second"));
    }
});

Use this jQuery code.

avetarman
  • 1,244
  • 9
  • 8
  • That's more or less what I'm already currently doing. But if among all the LIs, there's one that's much taller than the others, this method wouldn't work (either UL would be way too tall compared to the other one). So I was looking for something that would be smart about how it arranges the LIs, based on their height. So if one LI is as tall as 3 other LIs, the UL with this tallest LI would have less LIs in it than the other one. Does this make sense? – Alex Heyd Aug 02 '11 at 15:20
  • You can't make them exactly equal. – avetarman Aug 02 '11 at 15:24