Commit 3ee06535 authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Better implementation of columns-splitting.

This time a brute-force solution.  But at least it works correctly, and
for such a small n as we have it should be fast enough.

Also a suite of more compact test cases that cover more cases than the
previous ones.
parent 279b14fe
Loading
Loading
Loading
Loading
+38 −19
Original line number Diff line number Diff line
@@ -62,7 +62,7 @@ module JsDuck
    def render_columns(groups)
      align = ["lft", "mid", "rgt"]
      i = -1
      return split_to_columns(groups, 3).map do |col|
      return split(groups, 3).map do |col|
        i += 1
        [
          "<div class='#{align[i]}'>",
@@ -83,27 +83,46 @@ module JsDuck
      end
    end

    def split_to_columns(groups, n)
      header_size = 3
      # The size of one item is it's number of classes + the space for header
      total_size = groups.reduce(0) {|sum, g| sum + g["classes"].length + header_size }

      # split into n columns
      avg_col_size = (total_size / n).ceil
      columns = Array.new(n) {|i| [] }

      col_size = 0
      col_index = 0
      groups.each do |g|
        col_size += g["classes"].length + header_size
        columns[col_index] << g
        if col_size >= avg_col_size
          col_index += 1
          col_size = 0
    # Splits the array of items into n chunks so that the sum of
    # largest chunk is as small as possible.
    #
    # This is a brute-force implementation - we just try all the
    # combinations and choose the best one.
    def split(items, n)
      if n == 1
        [items]
      elsif items.length <= n
        Array.new(n) {|i| items[i] ? [items[i]] : [] }
      else
        min_max = nil
        min_arr = nil
        i = 0
        while i <= items.length-n
          i += 1
          # Try placing 1, 2, 3, ... items to first chunk.
          # Calculate the remaining chunks recursively.
          cols = [items[0,i]] + split(items[i, items.length], n-1)
          max = max_sum(cols)
          # Is this the optimal solution so far? Remember it.
          if !min_max || max < min_max
            min_max = max
            min_arr = cols
          end
        end
        min_arr
      end
    end

    def max_sum(cols)
      cols.map {|col| sum(col) }.max
    end

      columns
    # Finds the total size of items in array
    #
    # The size of one item is it's number of classes + the space for header
    def sum(arr)
      header_size = 3
      arr.reduce(0) {|sum, item| sum + item["classes"].length + header_size }
    end

  end
+48 −41
Original line number Diff line number Diff line
@@ -2,66 +2,73 @@ require "jsduck/categories"

describe JsDuck::Categories do

  describe "splitting to one column" do
    before do
      categories = JsDuck::Categories.new({})
      @cols = categories.split_to_columns([
          {"classes" => ["1", "2", "3"]},
          {"classes" => ["4", "5"]},
          {"classes" => ["6"]},
        ], 1)
    end

    it "creates just one column" do
      @cols.length.should == 1
  # Small helper to check the sums
  def sum(arr)
    arr.reduce(0) {|sum,x| sum + x }
  end

    it "places all groups to first column" do
      @cols[0].length.should == 3
  # Replace the sum method with the one that simply sums the numbers,
  # so we can use simpler test-data.
  class JsDuck::Categories
    def sum(arr)
      arr.reduce(0) {|sum,x| sum + x }
    end
  end

  describe "splitting to two equal-height columns" do
  describe "#split" do
    before do
      categories = JsDuck::Categories.new({})
      @cols = categories.split_to_columns([
          {"classes" => ["1", "2", "3", "4", "5", "6"]}, # 6+3 = 9
          {"classes" => ["7", "8"]}, # 2+3 = 5
          {"classes" => ["9"]}, # 1+3 = 4
        ], 2)
      @categories = JsDuck::Categories.new({})
    end

    it "creates two columns" do
      @cols.length.should == 2
    it "split(1 item by 1)" do
      @cols = @categories.split([2], 1)
      @cols.length.should == 1
      sum(@cols[0]).should == 2
    end

    it "places first group to first column" do
      @cols[0].length.should == 1
    it "split(3 items by 1)" do
      @cols = @categories.split([1, 2, 3], 1)
      @cols.length.should == 1
      sum(@cols[0]).should == 6
    end

    it "places other two groups to second column" do
      @cols[1].length.should == 2
    end
    it "split(3 items to two equal-height columns)" do
      @cols = @categories.split([1, 2, 3], 2)
      @cols.length.should == 2
      sum(@cols[0]).should == 3
      sum(@cols[1]).should == 3
    end

  describe "splitting one group to two columns" do
    before do
      categories = JsDuck::Categories.new({})
      @cols = categories.split_to_columns([
          {"classes" => ["1", "2"]}
        ], 2)
    it "split(1 item by 3)" do
      @cols = @categories.split([2], 3)
      @cols.length.should == 3
      sum(@cols[0]).should == 2
      sum(@cols[1]).should == 0
      sum(@cols[2]).should == 0
    end

    it "creates two columns" do
      @cols.length.should == 2
    it "split(3 items by 3)" do
      @cols = @categories.split([1, 2, 3], 3)
      @cols.length.should == 3
      sum(@cols[0]).should == 1
      sum(@cols[1]).should == 2
      sum(@cols[2]).should == 3
    end

    it "places first group to first column" do
      @cols[0].length.should == 1
    it "split(6 items by 3)" do
      @cols = @categories.split([5, 8, 4, 2, 1, 3], 3)
      @cols.length.should == 3
      sum(@cols[0]).should <= 10
      sum(@cols[1]).should <= 10
      sum(@cols[2]).should <= 10
    end

    it "leaves the second column empty" do
      @cols[1].length.should == 0
    it "split(8 items by 3)" do
      @cols = @categories.split([1, 3, 5, 2, 1, 4, 2, 3], 3)
      @cols.length.should == 3
      sum(@cols[0]).should <= 9
      sum(@cols[1]).should <= 9
      sum(@cols[2]).should <= 9
    end
  end