Compare 2 ActiveRecord Relations rails - ruby-on-rails

I have an interest form which is used to add an interest to a user. The structure of the database is as follows:
Users - id ...
Interests - id ..
UsersMapInterests - user_id interest_id
So when a user selects an interest from the Interests table, it gets added to the UsersMapInterests table.
A query for user 1 would return something like {{user_id: 1, interest_id 3},{user_id: 1, interest_id 7},{user_id: 1, interest_id 25}}.
A query for user 22 would return {user_id: 22, interest_id 3},{user_id: 22, interest_id 3}
Now I would like to compare the results with each other.
I assume that using to_a and comparing them like this would be too much:
user1.each do |int1|
user22.each do |int2|
if int1.interest_id == int2.interest_id
count++
end
end
end
How else could I compare them?

You can just use the following line of code
(user1.interest_ids & user22.interest_ids).count
Explanation:
user1.interest_ids
#=> [3, 7, 25]
user22.interest_ids
#=> [3, 3]
[3, 7, 25] & [3, 3] # Returns common element from both arrays
#=> [3]
[3].count
#=> 1

I will do something like this:
user1_interest_ids = user1.map { |o| o[:interest_id] }
user22_interest_ids = user22.map { |o| o[:interest_id] }
count = (user1_interest_ids & user22_interest_ids).size

Related

Rails ActiveRecord join table with array of ids without n+1 request

I need to retrieve records from an array of Ids like:
User.where(id: [1,1,2])
BUT the problem is that I only get two records from this request and I want to have the [#User id:1] in double as number 1 appears twice in the array.
I could do a n+1 request but that's not very optimistic...
The steps you should follow if you want a single simple query to database:
Create a hash based on the array like:
hash = {1=>2, 2=>1, 3=>1, 4=>3} for [1, 1, 2, 3, 4, 4, 4]
Query simply:
users = User.where(id: your_array.uniq)
# Your case
users = User.where(id: [1, 1, 2].uniq) # because [1, 1, 2].uniq => [1, 2]
make your array:
the_way_i_want = []
hash.each do |key, value|
value.times do
the_way_i_want.push users.where(id: key)
end
end
set the users = the_way_i_want and return
users = the_way_i_want
crude but will save database access :-)
How about creating 1 request to DB, and then just format your data
user_ids = [1, 1, 2] # this you have
Create a hash { user_id: user_record, .... }
users = User.where(id: user_ids).map { |u| [u.id, u]}.to_h
Then create an array with all the records you want
user_ids.map do |u_id|
users[u_id]
end

Iterating Over 2 Arrays And Building A New Array From Matches

Ok so I have an array of 'winner ids' this array represents users who have won in the previous round of a tournament. I then have an array of objects called 'tournament participations', this represents all the users that have participated in the tournament (many to many relationship join table).
For each 'winner id' I want to iterate through the 'tournament participations' array and find the tournament participation with a user_id that matches the 'winner id', then push it into a new array called 'round participations'...
I have tried the code below but I always get returned the original 'winner_ids' array....
#challenges = Challenge.where(tournament_id: #tournament.id)
#winner_ids = #challenges.pluck(:winner_id)
#tournament_participations = #tournament.tournament_participations
#round_participations = []
#round_participations = #winner_ids.each do |winner_id|
#round_participation = #tournament_participations.where(user_id: winner_id)
#round_participations << #round_participation
end
each returns the enumerable it was called on; in this case, #winner_ids.each is returning #winner.ids. You don't need to assign the result of the iteration to #round_participations.
Also, check out the map method.
Use the map method, instead of .each.
The map method can be used to create a new array based on the original array, but with the values modified by the supplied block. See the below example.
In case of each method
irb(main):001:0> arr = [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5]
irb(main):002:0> arr.each { |a| print a -= 10, " " }
-9 -8 -7 -6 -5
=> [1, 2, 3, 4, 5]
you can see after iterations it returned the original array. But in the case of map
irb(main):005:0> arr = [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5]
irb(main):006:0> arr.map { |a| 2*a }
=> [2, 4, 6, 8, 10]
map can transform the contents of an array, meaning that it can perform an operation on each element in the array.

remove array from multidimensional array if first 3 values aren't unique in ruby on rails

OK, so I have this array of arrays. Each array within the larger array is very much the same, ten specific values. If my value at location 3 is a specific value, then I want to iterate through the rest of the remaining arrays within the larger array and see if the first 3 values at locations 0, 1, and 2 match. if they then match, I'd like to delete the original array. I'm having a hard time with it, maybe there is an easy way? I'm sure there is, I'm fairly new to this whole coding stuff =) So much appreciation in advance for your help....
here's where I'm at:
#projectsandtrials.each do |removed|
if removed[3] == ["Not Harvested"]
#arraysforloop = #projectsandtrials.clone
#arraysforloop1 = #arraysforloop.clone.delete(removed)
#arraysforloop1.each do |m|
if (m & [removed[0], removed[1], removed[2]]).any?
#projectsandtrials.delete(removed)
end
end
end
end
Lets look at your situation:
#projectsandtrials.each do |removed|
// some logic, yada yada
#projectsandtrials.delete(removed)
end
You can't just delete stuff out of an array you're iterating through. At least not until you finish iterating through it. What you should be using instead is a filtering method like reject instead of just an each.
So instead of deleting right there, you should just return true when using reject.
I think about it like this when iterating through arrays.
Do I want the array to stay the same size and have the same content?
Use each.
Do I want the array to be the same size, but have different content?
Use map.
Do I want the array to be less than or equal to the current size?
Use select or reject.
Do I want it to end up being a single value?
Use reduce.
Code
def prune(arr, val)
arr.values_at(*(0..arr.size-4).reject { |i| arr[i][3] == val &&
arr[i+1..i+3].transpose[0,3].map(&:uniq).all? { |a| a.size==1 } }.
concat((arr.size-3..arr.size-1).to_a))
end
Example
arr = [ [1,2,3,4,0],
[3,4,5,6,1],
[3,4,5,4,2],
[3,4,5,6,3],
[3,4,5,6,4],
[3,4,0,6,5],
[2,3,5,4,6],
[2,3,5,5,7],
[2,3,5,7,8],
[2,3,5,8,9],
[2,3,5,7,0]
]
Notice that the last values of the elements (arrays) of arr are consecutive. This is to help you identify the elements of prune(arr, 4) (below) that have been dropped.
prune(arr, 4)
# => [[3, 4, 5, 6, 1],
# [3, 4, 5, 4, 2],
# [3, 4, 5, 6, 3],
# [3, 4, 5, 6, 4],
# [3, 4, 0, 6, 5],
# [2, 3, 5, 5, 7],
# [2, 3, 5, 7, 8],
# [2, 3, 5, 8, 9],
# [2, 3, 5, 7, 0]]
Explanation
The arrays at indices 0 and 6 have not been included in array returned.
arr[0] ([1,2,3,4,0]) has not been included because arr[0][3] = val = 4 and arr[1], arr[2] and arr[3] all begin [3,4,5].
arr[6] ([2,3,5,4,6]) has not been included because arr[6][3] = 4 and arr[7], arr[8] and arr[9] all begin [2,3,5].
arr[2] ([3,4,5,5,2]) has been included because, while arr[2][3] = 4, arr[3][0,3], arr[4][0,3] and arr[5][0,3] all not all equal (i.e., arr[5][2] = 0).
Notice that the last three elements of arr will always be included in the array returned.
Now let's examine the calculations. First consider the following.
arr.size
#=> 11
a = (0..arr.size-4).reject { |i| arr[i][3] == val &&
arr[i+1..i+3].transpose[0,3].map(&:uniq).all? { |a| a.size==1 } }
#=> (0..7).reject { |i| arr[i][3] == val &&
arr[i+1..i+3].transpose[0,3].map(&:uniq).all? { |a| a.size==1 } }
#=> [1, 2, 3, 4, 5, 7]
Consider reject's block calculation for i=0 (recall val=4).
arr[i][3] == val && arr[i+1..i+3].transpose[0,3].map(&:uniq).all? {|a| a.size==1 }}
#=> 4 == 4 && arr[1..3].transpose[0,3].map(&:uniq).all? { |a| a.size==1 }
#=> [[3,4,5,6,1],
# [3,4,5,4,2],
# [3,4,5,6,3]].transpose[0,3].map(&:uniq).all? { |a| a.size==1 }
#=> [[3, 3, 3],
# [4, 4, 4],
# [5, 5, 5],
# [6, 4, 6],
# [1, 2, 3]][0,3].map(&:uniq).all? { |a| a.size==1 }
#=> [[3, 3, 3],
# [4, 4, 4],
# [5, 5, 5]].map(&:uniq).all? { |a| a.size==1 }
#=> [[3], [4], [5]].all? { |a| a.size==1 }
#=> true
meaning arr[0] is to be rejected; i.e., not included in the returned array.
The remaining block calculations (for i=1,...,10) are similar.
We have computed
a #=> [1, 2, 3, 4, 5, 7]
which are the indices of all elements of arr except the last 3 that are to be retained. To a we add the indices of the last three elements of arr.
b = a.concat((arr.size-3..arr.size-1).to_a)
#=> a.concat((8..10).to_a)
#=> a.concat([8,9,10])
#=> [1, 2, 3, 4, 5, 7, 8, 9, 10]
Lastly,
arr.values_at(*b)
returns the array given in the example.
Your code snippet seems fine, although there are couple of things to note:
#arraysforloop.clone.delete(removed) removes all the occurences of removed array (not only the first one). E.g. [1,2,3,1].delete(1) would leave you with [2,3]. You could fix it with using an iterator for #projectsandtrials and delete_at method.
delete method returns the same argument you pass to it (or nil if no matches found). So #arraysforloop1 = #arraysforloop.clone.delete(removed) makes your #arraysforloop1 to contain the removed array's elements only! Removing an assignment could save you.
I see no reason to have two cloned arrays, #arraysforloop and #arraysforloop1, as the former one is not used anyhow later. May be we could omit one of them?
#projectsandtrials.delete(removed) leaves you in a strange state, as long as you're iterating the same array right now. This could end up with you missing the right next element after the removed one. Here is a simple snippet to illustrate the behaviour:
> a = [1,2,3]
> a.each{|e, index| puts("element is: #{e}"); a.delete(1);}
element is: 1
element is: 3
As you see, after deleting element 1 the loop moved to element 3 directly, omitting the 2 (as it became the first element in array and algorithm thinks it's been handled already).
One of the possibilities to make it less messy is to split it to a bundle of methods. Here is an option:
def has_searched_element? row
# I leave this method implementation to you
end
def next_rows_contain_three_duplicates?(elements, index)
# I leave this method implementation to you
end
def find_row_ids_to_remove elements
[].tap do |result|
elements.each_with_index do |row, index|
condition = has_searched_element?(row) && next_rows_contain_three_duplicates?(elements, index)
result << index if condition
end
end
end
row_ids_to_remove = find_row_ids_to_remove(#projectsandtrials)
# now remove all the elements at those ids out of #projectsandtrials

Ruby on Rails group_by is not grouping in the correct order using DateTime

First I get a collection of channels from my scope
Channel.not_archived.map { |c| channels << c }
Then I sort those by the start_time attribute:
channels.sort! { |a, b| a.start_time <=> b.start_time }
Then I want to group them by their start times. So channels that start at 8:00am will be grouped together. So I use the group_by method:
#grouped_channels = #channels.group_by { |c| time_with_offset(c).strftime("%I:%M %P") }
the time_with_offset method:
# Returns time with offset
def time_with_offset(channel)
user = current_user.time_zone.to_i
organization = channel.organization.time_zone.to_i
time_offset = organization -= user
channel.start_time - time_offset.hours
end
And I get back all of my records in the correct group. The issue i'm having is that the groups are not in order. So the group of 8:00am should be before the group of 9:00am. It's just in weird random order now. Can anyone help me get these in correct order?
If you wish to reorder the key-value pairs of any hash h in key order given by an array keys, which contains all the keys in the desired order, write
(keys.zip(h.values_at(*keys))).to_h
For Ruby versions prior to 2.0, write
Hash[keys.zip(h.values_at(*keys))]
For example,
h = { b: 1, d: 2, a: 3, c: 4 }
#=> {:b=>1, :d=>2, :a=>3, :c=>4}
keys = [:a, :b, :c, :d]
(keys.zip(h.values_at(*keys))).to_h
#=> {:a=>3, :b=>1, :c=>4, :d=>2}
The steps are as follows.
a = h.values_at(*keys)
#=> same as h.values_at(:a, :b, :c, :d)
#=> [3, 1, 4, 2]
b = keys.zip(a)
# => [[:a, 3], [:b, 1], [:c, 4], [:d, 2]]
b.to_h
#=> {:a=>3, :b=>1, :c=>4, :d=>2}
First you are sorting by one time, then you are grouping by a different time. I expect this explains your undesired order.
Sort by the offset time.
channels.sort_by { |c| time_with_offset(c) }.group_by { |c| time_with_offset(c).strftime("%I:%M %P") }

make sure two fields in array are unique

I need to make sure the follower_id and followed_id are unique in an array which also includes a third number, called value. All are integers. It is the combination of follower_id and followed_id that needs to be unique not the individual numbers themselves. Here is what I have
Relationship.populate 1..20 do |relationship|
relationship.follower_id = (1..20)
relationship.followed_id = (1..20)
relationship.value = rand(1..5)
end
this would ensure that
1,3,5
1,3,5
2,3,5
1,2,5
would be
1,3,5
2,3,5
1,2,5
Assuming, that the order in pairs is not to be taken into account, and you want to eliminate triples, even having different values, here you go:
a = [[1,3,5], [3,1,5], [2,3,5], [2,3,6], [1,2,5]]
# to count [1,3,5] and [3,1,5] as similar
a.group_by { |(fd,fr,_)| [fd,fr].sort }.values.map &:first
# to count [1,3,5] and [3,1,5] as different
a.group_by { |(fd,fr,_)| [fd,fr] }.values.map &:first
#⇒ [[1,3,5], [3,1,5], [2,3,5], [1,2,5]]
a = [[1,3,5], [1,3,4], [3,1,2], [2,3,2], [2,3,1], [1,2,3]]
If the order of the first two elements of each element of a is important:
a.uniq { |e| e[0,2] }
#=> [[1, 3, 5], [3, 1, 2], [2, 3, 2], [1, 2, 3]]
If the order of the first two elements of each element of a is not important:
require 'set'
a.uniq { |e| Set.new e[0,2] }
#=> [[1, 3, 5], [2, 3, 2], [1, 2, 3]]
Perfect Uniqueness
Here's a solution assuming the order within the triples matters and you want each triple to be perfectly unique. Just use the uniq method on the Array class. It works like this:
[
[1,3,5],
[1,3,5],
[2,3,5],
[1,2,5]
].uniq
#=> [[1, 3, 5], [2, 3, 5], [1, 2, 5]]
Partial Uniqueness
If instead, you only care about the first two being unique, pass the uniq method a block that returns whatever subset you want to be unique. If you only want the first two elements of the triple and can discard duplicates even when the third element is unique, you can just pass it the range 0..1.
[
[1,3,5],
[1,3,5],
[2,3,5],
[1,2,5],
[1,2,6]
].uniq { |triple| triple[0..1] }
#=> [[1, 3, 5], [2, 3, 5], [1, 2, 5]]
Note that the last element, [1,2,6] was discarded even though it ended in 6 because it was considered a duplicate of [1,2,5]. This is because [1,2,5][0..1] #=> [1,2] and [1,2,6][0..1] #=> [1,2].

Resources