Even Spaced Primary / Secondary Columns in Rails
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.
精彩评论