开发者

Even Spaced Primary / Secondary Columns in Rails

开发者 https://www.devze.com 2023-01-05 22:09 出处:网络
I have a set of regions and cities (nested) and want to be able to output them in a few even length columns ordered alphabetically. For example:

I have a set of regions and cities (nested) and want to be able to output them in a few even length columns ordered alphabetically. For example:

[Alberta]   [Ontario]   [Quebec]
Calgary     Hamilton    Hull
Edmonton    Kitchener   Laval
[Manitoba]  Ottawa      Montreal
Winnipeg    Toronto
            Waterloo

I took a look at开发者_JAVA百科 'in_groups' (and 'in_groups_of') however, I need to group based on the size of a relationship (i.e. the number of cities a region has). Not sure if a good Rails way of doing this exists. Thus far my code looks something like this:

<% regions.in_groups(3, false) do |group| %>
  <div class="column">
    <% group.each do |region| %>
      <h1><%= region.name %></h1>
      <% region.cities.each do |city| %>
        <p><%= city.name %></p>
      <% end %>
    <% end %>
  </div>
<% end %>

However, certain regions are extremely unbalanced (i.e. have many cities) and don't display correctly.


I agree this should be helper code, not embedded in a view.

Suppose you have the province-to-city map in a hash:

 map = {
   "Alberta" => ["Calgary", "Edmonton"],
   "Manitoba" => ["Winnipeg"],
   "Ontario" => ["Hamilton", "Kitchener", "Ottawa", "Toronto", "Waterloo"],
   "Quebec" => ["Hull", "Laval", "Montreal"]
 }

It's easier to start by thinking about 2 columns. For 2 columns, we want to decide where to stop the 1st column and begin the 2nd. There are 3 choices for this data: between Alberta and Manitoba, Manitoba and Ontario and between Ontario and Quebec.

So let's start by making a function so that we can split a list at several places at once:

def split(items, indexes)
  if indexes.size == 0
    return [items]
  else
    index = indexes.shift
    first = items.take(index)
    indexes = indexes.map { |i| i - index }
    rest = split(items.drop(index), indexes)
    return rest.unshift(first)
  end
end

Then we can look at all of the different ways we can make 2 columns:

require 'pp' # Pretty print function: pp

provinces = map.keys.sort

1.upto(provinces.size - 1) do |i|
  puts pp(split(provinces, [i]))
end

=>

[["Alberta"], ["Manitoba", "Ontario", "Quebec"]]
[["Alberta", "Manitoba"], ["Ontario", "Quebec"]]
[["Alberta", "Manitoba", "Ontario"], ["Quebec"]]

Or we can look at the different ways we can make 3 columns:

1.upto(provinces.size - 2) do |i|
  (i+1).upto(provinces.size - 1) do |j|
    puts pp(split(provinces, [i, j]))
  end
end

=>

[["Alberta"], ["Manitoba"], ["Ontario", "Quebec"]]
[["Alberta"], ["Manitoba", "Ontario"], ["Quebec"]]
[["Alberta", "Manitoba"], ["Ontario"], ["Quebec"]]

Once you can do this, you can look for the arrangement where the columns have the most uniform heights. We'll want a way to find the height of a column:

def column_height(map, provinces)
  provinces.clone.reduce(0) do |sum,province|
   sum + map[province].size
  end
end

Then you can use the loop from before to look for the 3 column layout with the least difference between the tallest and shortest columns:

def find_best_columns(map)
  provinces = map.keys.sort
  best_columns = []
  min_difference = -1
  1.upto(provinces.size - 2) do |i|
    (i+1).upto(provinces.size - 1) do |j|
      columns = split(provinces, [i, j])
      heights = columns.map {|col| column_height(map, col) }
      difference = heights.max - heights.min
      if min_difference == -1 or difference < min_difference
        min_difference = difference
        best_columns = columns
      end
    end
  end
  return best_columns
end

That'll give you a list for each column:

puts pp(find_best_columns(map))

=>

[["Alberta", "Manitoba"], ["Ontario"], ["Quebec"]]

This is great because you figure out which provinces belong in each column independently of the model structure, and it doesn't generate HTML directly. So both the models and views can change but you can still reuse this code. Since these functions are self-contained, they're also easy to write unit tests for. If you need to balance 4 columns, you just need to adjust the find_best_columns function, or you could rewrite it recursively to support n columns, where n is another parameter.


if you want to keep them left to right alphabetical, I cannot come up with a good way. Using what you have. Here is something for what I had in mind. This should be divided up into helper/controller/model a bit but should give you an idea if this is something along the lines of what you were thinking

def region_columns(column_count)
  regions = Region.all(:include => :cities)
  regions.sort!{|a,b| a.cities.size <=> b.cities.size}.invert
  columns = Array.new(column_count, [])

  regions.each do |region|
    columns.sort!{|a,b| a.size <=> b.size}
    columns[0] << "<h1>#{region.name}</h1>"
    columns[0] << region.cities.map{|city| "<p>#{city.name}</p>"}
    columns[0].flatten
  end

  columns
end

that would give you columns of html that you would just need to loop through in your view.

0

精彩评论

暂无评论...
验证码 换一张
取 消