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.