Even Spaced Primary / Secondary Columns in Rails - ruby-on-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 '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.

Related

How to store data from an each where rows has the same id

I'm a newbie in rails development, i'm sorry if i can't express myself well.
I've a rails each cycle that do:
r.round_matches.each do |m|
m.round_matches_team.each do |mt|
sheet.add_row [m.round_id, mt.team_name]
end
end
Every round_match has :round_id doubled
The output is:
round_id: 2 team_name: TEST A
round_id: 2 team_name: TEST B
How i can group round by id in the each cycle and estrapolate the team_name from round_match_teams for every same round_id? I would like that my output will be:
round_id: 2 team_name[1]: TEST A team_name[2]: TEST B
This should work
r.round_matches.each do |m|
team_names = m.round_matches_team.map.with_index do |team, index|
"team_name[#{index + 1}]: #{team.team_name}"
end.join(' ')
sheet.add_row ["round_id: #{m.round_id} #{team_names}"]
end
I would handle this a little differently: I would manipulate the data to be in a better format, and create the sheet from that data.
sheet_data = Hash.new([])
r.round_matches.each do |m|
m.round_matches_team.each do |mt|
sheet_data[mt.round_id] << mt.team_name
end
end
sheet_data.each do |round_id, teams|
sheet.add_row [round_id, *teams]
end
Explained: I will generate a hash with as key the round_id and as value an array containing the collected team-names. Then when adding the row, I use the splat-operator (*) to make sure each team-name will get a separate column.
You could even sort the team-names if this might make more sense before using the splat, or instead of using *teams, use something like teams.sort.join(", ") to combine all teams into one column (if wanted/preferred).

Rails set filter by the current user - joins association issue

I have a filter I'm using for a form that I'd like to only show those that have an email that matches the current user.
Attributes involved: Users' email, Career's recipient, Region name
Careers belongs_to :region and I'm currently displaying Region with the following in my view:
-#careers.each do |career|
th =career.region&.name
So the logic would be to compare current_user.email against all Careers.recipienct and if present then display only those Regions that are represented.
Example would be:
Region | Career | Recipients
Pacific Northwest | Data Analyst | john#doe.com, jill#excite.com
So I know it needs to hit my region select which looks like:
= select_tag :region_id,
options_from_collection_for_select(sort_region, :id, :name, params[:region_id]),
include_blank: 'All Regions'
sort_region currently has:
def sort_region
Region.where.not(name: ['HQ', 'Canada'].sort_by(&:position)
end
So my thought was to tackle the comparison in something similar with:
def user_region
if current_user.super_admin?
return sort_region
else
arr = []
Career.all.each do |emails|
arr << emails.recipient
end
if arr.include?(current_user.email)
return BLANK
end
end
end
The blank is where I'm stuck. I only want to show those Regions where there is a match of the Career.recipient with the current_user.email.
I wrote the following for the return that basically I hate
return Region.joins(:career).where("region.name = career.region&.name")
This actually returns
Can't join 'Region' to association named 'career'
Same issue when I try
return Region.joins(:career).where("careers.region_id = region.id")
Is there a correct joins association I should be using here?
EDIT:
I think I could address the return if I could figure out how to push to the array a second value with the email.
So something like:
arr = []
Career.all.each do |emails|
arr << emails.recipient
arr << emails.region_id
end
if arr.include?(current_user.email)
return region_id
end
So this doesn't create a pair like I'd hope/want of [["john#doe.com", 2], ["jane#excite.com", 3]] but instead ["john#doe.com", 2, "jane#excite.com", 3]. Also I'm not able to do a return on region_id. So I'd need to access the integers only.
Maybe I should be creating a hash of all the items and then pull from there?
Another EDIT:
Tried putting the conditional within the iteration.
arr = []
Career.all.each do |emails|
if emails.recipient == current_user.email
arr << emails.region_id
end
end
This however shows the arr containing [1] which is not the associated region_id.
Third EDIT:
My latest attempt has been to see about doing a join where I simply pull out those Regions where Career Recipient equals the current_user.email.
Region.find(:all, joins: ' JOIN regions ON careers.region_id', conditions: "#{current_user.email} = careers.recipient")
This results in The Region you were looking for could not be found.
Arel_table for the win
Region.joins(:careers).where(Career.arel_table[:recipient].matches("%#{current_user.email}%"))

I need to iterate through 2 arrays in ruby on rails.. I know zip function

Let me show you the code first..
in my controller I have 2 arrays =
#confessions = Confession.where(amitian_id: #amitian.ids).order('created_at DESC') if amitian_signed_in?
#anonyconfess = Confession.where(amitian_id: #anonymous.ids).order('created_at DESC')
so 2 arrays #confessions and #anonyconfess... what I want is to iterate through both the arrays at the same time and post the confessions .. here is my view
-#anonyconfess.each do |c|
#code
%br
-#confessions.each do |c|
#code
%br
I want to do this at the same time rather than this 2 separate iterations...
should I use #confessions.zip(#anonymous)
knowing that array size of both are different I don't think zip is a good approach or is it ?
You could combine both queries into one:
ids = #anonymous.ids
ids.concat(#amitian.ids) if amitian_signed_in?
#confessions = Confession.where(amitian_id: ids).order('created_at DESC')
And in your view:
- #confessions.each do |c|
#code
%br
Each-each
If you want them one after the other :
-[#anonyconfess, #confessions].each do |confess|
-confess.each do |c|
#code
%br
Set union
Good tip from #JagdeepSingh :
-(#anonyconfess | #confessions).each do |c|
#code
%br
zip
If you want them mixed, zip should be the right tool, you'd just need to make sure the first array is longer than the second one :
array_1 = [1, 2, 3, 4, 5]
array_2 = %w(a b c)
array_1.zip(array_2).each do |x, y|
p x
p y
puts
end
# 1
# "a"
# 2
# "b"
# 3
# "c"
# 4
# nil
# 5
# nil
array_2.zip(array_1).each do |y, x|
p x
p y
puts
end
# 1
# "a"
# 2
# "b"
# 3
# "c"
NOTE: #Stefan's answer is better anyway :)
Unless I am really mistaken, you don't have two arrays, but two ActiveRecord::Relation instances. But don't worry, they really work like arrays most of the time and you may just write something like
#all_confessions = #anonyconfess + #confessions
and then in your view use
-#all_confessions.each do |confess|
#code
%br
Please refer to this article here to learn more about this.
This will show the posts in the same order you would have with
-#anonyconfess.each do |c|
#code
%br
-#confessions.each do |c|
#code
%br
In other words, #anonyconfess will appear first and then #confessions.
If you want then to really 'mix', maybe you should consider one of the other answers.

How to sort array of strings with multiple conditions?

I have an array of strings which need to be sorted by multiple criteria (two string attributes). However the two sorts need to be sorted in opposite directions.
Example:
Array must be sorted by attribute_a in desc order and then within that
sorted by attribute_b in asc order.
I have been using .sort_by! which works fine, however I am just unsure how to implement two criteria sorting in opposite sort directions
If these attributes are database columns, you can use:
Organization.order(attribute_a: :desc, attribute_b: :asc)
Otherwise, use sort with an array:
Arrays are compared in an “element-wise” manner; the first two elements that are not equal will determine the return value for the whole comparison.
Exchanging the first elements sorts them in descending order:
array.sort { |x, y| [y.attribute_a, x.attribute_b] <=> [x.attribute_a, y.attribute_b] }
# ^ ^
# | |
# +-------- x and y exchanged -------+
To generate a list as mentioned in your comment, you can use group_by:
<% sorted_array.group_by(&:attribute_a).each do |attr, group| %>
<%= attr %> #=> "Type z"
<% group.each do |item| %>
<%= item.attribute_b %> #=> "name a", "name b", ...
<% end %>
<% end %>
You can do something like this:
sorted = a.group_by(&:attribute_a).each do |k, v|
v.sort_by!(&:attribute_b)
end.sort_by(&:first).map(&:last)
sorted.reverse.flatten
This solution groups all the elements by attribute_a, sorts every group by attribute_b (asc), and the groups by attribute_a (asc).
The second line reverses the order of the groups (without changing the order of the elements within the groups), and then flattens the results, resulting in the original list sorted by attribute_a (desc), attribute_b (asc)
Try something like that:
a.sort! do |x, y|
xa = get_a(x)
ya = get_a(y)
c = ya - xa
return c unless c == 0
xb = get_b(x)
yb = get_b(y)
return xb - yb
end
get_a and get_b are functions to extract a and b params
This is a straight forward solution using sort. The reverse order is created by negating the result of <=>:
sorted = some_things.sort do |x,y|
if x.a == y.a
x.b <=> y.b
else
-(x.a <=> y.a) # negate to reverse the order
end
end
Here's the complete program that tests it:
class Thing
attr_reader :a, :b
def initialize(a, b)
#a = a
#b = b
end
end
# Create some Things
some_things = [Thing.new("alpha","zebra"), Thing.new("gamma", "yoda"),
Thing.new("delta", "x-ray"), Thing.new("alpha", "yoda"),
Thing.new("delta", "yoda"), Thing.new("gamma", "zebra")]
sorted = some_things.sort do |x,y|
if x.a == y.a
x.b <=> y.b
else
-(x.a <=> y.a) # negate to reverse the order
end
end
p sorted
Which produces this output (newlines inserted):
[#<Thing:0x007fddca0949d0 #a="gamma", #b="yoda">,
#<Thing:0x007fddca0947f0 #a="gamma", #b="zebra">,
#<Thing:0x007fddca094958 #a="delta", #b="x-ray">,
#<Thing:0x007fddca094868 #a="delta", #b="yoda">,
#<Thing:0x007fddca0948e0 #a="alpha", #b="yoda">,
#<Thing:0x007fddca094a48 #a="alpha", #b="zebra">]

Ruby on Rails: How to strip insignificant zeros on dynamically added items?

I have an Invoice to which Items can be added using some jQuery magic.
Controller:
def new
#invoice = Invoice.new
#invoice.with_blank_items(current_user)
#title = "New invoice"
end
Model:
def with_blank_items(user, n = 1)
n.times do
items.build(:price => user.preference.hourly_rate)
end
self
end
View:
<%= f.text_field number_with_precision(:price, :strip_insignificant_zeros => true) %>
Now the problem is that the price of a newly added item is always displayed in the format XX.X, i.e. with one decimal place, no matter if it is zero or not.
I don't like that and I want a price of 50 to be displayed as 50 and not as 50.0.
Once the invoice gets saved to the database, unnecessary zeros get dropped and that's perfect.
How can I strip insignificant zeros on newly added items as well?
You can try to format the values the right way before assigning:
items.build(:price => '%g' % user.preference.hourly_rate)
If I understand your question, I believe your issue can fixed with a JavaScript function (which removed the decimal places).
Try something like:
function removeDecimal(val){
return val.toFixed(0);
}
These are my test cases:
removeDecimal(123.45) -> 123
removeDecimal(123.4) -> 123
removeDecimal(123) -> 123

Resources