Active Record - Where IN with multiple columns - ruby-on-rails

I have a query that needs to fetch from a table that meet two columns requirements exactly. So if I have users table with columns, age and score.
SELECT * FROM users where (age, score) IN ((5,6), (9,12), (22,44)..)
In my web app I am getting this pairs from an ajax request, and the number could be quite big. How do I construct an Active Record query for this?.
I am working on postgres database

Ideally, we will build a query string based on the input. Eg
ages_and_scores = [ [5, 6], [9, 12], [22, 44] ]
query_string = ages_and_scores.map do |pair|
"(age = #{pair[0]} AND score = #{pair[1]})"
end.join(" OR ")
# => (age = 5 AND score = 6) OR (age = 9 AND score = 12) OR (age = 22 AND score = 44)
Finally, your query will be
User.where(query_string)
You may correct the logic of how to build the query string since ages_and_scores is in a different format to my example.
Improvement
ages_and_scores = [ [5, 6], [9, 12], [22, 44] ]
query_params = []
query_template = ages_and_scores.map{ |_| "(age = ? AND score = ?)" }.join(" OR ")
# => (age = ? AND score = ?) OR (age = ? AND score = ?) OR (age = ? AND score = ?)
User.where(query_template, *ages_and_scores.flatten)

Another method, to reconstruct the exact query used by the OP based on a multicolumn IN clause.
A more compact form of SQL, with arguable better semantics:
ages_and_scores = [ [5, 6], [9, 12], [22, 44] ]
query_string = ages_and_scores.map { |pair| "(#{pair[0]},#{pair[1]})" }.join(",")
User.where("(age, score) IN (#{query_string})")

In rails 5, you can use OR, so you can do:
ages_and_scores = [ [5, 6], [9, 12], [22, 44] ]
ages_and_scores.map do |age, score|
User.where(age: age).where(score: score)
end.reduce(&:or)
# => should produce something like:
# SELECT * FROM users WHERE (`users`.`age` = 5 AND `users`.`score` = 6 OR `users`.`age` = 9 AND `users`.`score` = 12 OR `users`.`age` = 22 AND `users`.`score` = 44)
I believe this is sql-injection free and pure ActiveRecord.

Hey you can try this way in mysql:
ages_and_scores = [ [5, 6], [9, 12], [22, 44] ]
User.where("CONCAT(age,',', score) in (?)",ages_and_scores.map{|b| "#{b[0]},#{b[1]}"})
In PG database you can directly Concat using:
(age || ' ,' || score)

Another solution would be to use Arel, which would help keep things DB agnostic.
ages_and_scores = [ [5, 6], [9, 12], [22, 44] ]
first_age, first_score = ages_and_scores.shift
t = User.arel_table
users = ages_and_scores.inject(User.where(t[:age].eq(first_age)).where(t[:score].eq(first_score))) { |query, age_score| query.or(User.where(t[:age].eq(age_score[0])).where(t[:score].eq(age_score[1]))) }
Essentially how this works:
Separate the first value pair from the array
Get the arel table so that you can build the query
Use the inject method from Ruby's Enumerable module to iterate through the rest of the array adding in all of the or conditions ... but starting with the initial two values in the first where query.
Finally, you will have an Arel query which returns all of the users matching your criteria ... and it should work across any DB supported by Rails (Arel).

Base on Yuki Inoue 's comment. I have modified a little bit and it's working smoothly
ages_and_scores = [ [5, 6], [9, 12], [22, 44] ]
ages_and_scores.each do |age, score|
User.where(age: age).map(score: score)
end.reduce(&:or)
# => should produce something like:
# SELECT * FROM users WHERE (`users`.`age` = 5 AND `users`.`score` = 6 OR `users`.`age` = 9 AND `users`.`score` = 12 OR `users`.`age` = 22 AND `users`.`score` = 44)

Related

Distribute items into containers in twos - Rails

I have a list of 10 items -- it is an array of hashes.
[{ id: 1, name: 'one'}, { id: 2, name: 'two' } .. { id: 10, name: 'ten' }]
I also have a random number of containers -- let's say 3, in this case. These containers are hashes with array values.
{ one: [], two: [], three: [] }
What I want to do, is iterate over the containers and drop 2 items at a time resulting in:
{
one: [{id:1}, {id:2}, {id:7}, {id:8}],
two: [{id:3}, {id:4}, {id:9}, {id:10}],
three: [{id:5}, {id:6}]
}
Also, if the item list is an odd number (11), the last item is still dropped into the next container.
{
one: [{id:1}, {id:2}, {id:7}, {id:8}],
two: [{id:3}, {id:4}, {id:9}, {id:10}],
three: [{id:5}, {id:6}, {id:11}]
}
note: the hashes are snipped here so it's easier to read.
My solution is something like this: (simplified)
x = 10
containers = { one: [], two: [], three: [] }
until x < 1 do
containers.each do |c|
c << 'x'
c << 'x'
end
x -= 2
end
puts containers
I'm trying to wrap my head around how I can achieve this but I can't seem to get it to work.
Round-robin pair distribution into three bins:
bins = 3
array = 10.times.map { |i| i + 1 }
# => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
array.
each_slice(2). # divide into pairs
group_by. # group into bins
with_index { |p, i| i % bins }. # round-robin style
values. # get rid of bin indices
each(&:flatten!) # join pairs in each bin
Completely different approach, stuffing bins in order:
base_size, bins_with_extra = (array.size / 2).divmod(bins)
pos = 0
bins.times.map { |i|
length = 2 * (base_size + (i < bins_with_extra ? 1 : 0)) # how much in this bin?
array[pos, length].tap { pos += length } # extract and advance
}
# => [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10]]
If you absolutely need to have this in a hash,
Hash[%i(one two three).zip(binned_array)]
# => {:one=>[1, 2, 7, 8], :two=>[3, 4, 9, 10], :three=>[5, 6]}
The lovely (but likely not as performant) solution hinted at by Stefan Pochmann:
bins.times.with_object(array.to_enum).map { |i, e|
Array.new(2 * (base_size + (i < bins_with_extra ? 1 : 0))) { e.next }
}
This is just to show a different approach (and I would probably not use this one myself).
Given an array of items and the containers hash:
items = (1..10).to_a
containers = { one: [], two: [], three: [] }
You could dup the array (in order not to modify the original one) and build an enumerator that cycles each_value in the hash:
array = items.dup
enum = containers.each_value.cycle
Using the above, you can shift 2 items off the array and push them to the next container until the array is emtpy?:
enum.next.push(*array.shift(2)) until array.empty?
Result:
containers
#=> {:one=>[1, 2, 7, 8], :two=>[3, 4, 9, 10], :three=>[5, 6]}
You can use Enumerable#each_slice to iterate over a range from 0 to 10 in 3s and then append to an array of arrays:
containers = [
[],
[],
[]
]
(1...10).each_slice(3) do |slice|
containers[0] << slice[0]
containers[1] << slice[1]
containers[2] << slice[2]
end
p containers
# [[1, 4, 7], [2, 5, 8], [3, 6, 9]]

How to compare two tables using Ruby

I have two arrays like this: [[word],[number of occurence] , ... ]
Table 1 : [[["web"], 9], [["paris"], 8], [["html5"], 6], [["css3"], 6] ... ]
Table 2 : [[["web"], 2], [["paris"], 3], [["word"], 5], [["class"], 6] ... ]
I want to compare table 2 with table 1 and only show words NOT on table 2.
With the example I would have get:
Table 2 doesn't have html5, css3
Does Ruby have a gem that can do that?
The structure used here is quite irregular, but remapping it to something easier to work with isn't hard:
def hashify(list)
list.map do |(word), count|
[ word, count ]
end.to_h
end
The |(word), count| declaration pulls word out of the nested array, it simplifies the code.
Given sample data it works like this:
table1 = [[["web"], 9], [["paris"], 8], [["html5"], 6], [["css3"], 6] ]
table2 = [[["web"], 2], [["paris"], 3], [["word"], 5], [["class"], 6] ]
hashify(table1)
# => {"web"=>9, "paris"=>8, "html5"=>6, "css3"=>6}
Then you can use this to compute the difference:
hashify(table1).keys - hashify(table2).keys
# => ["html5", "css3"]
No need for any gem. Array difference works just fine, but you need to extract the interesting words first :
table1 = [[["web"], 9], [["paris"], 8], [["html5"], 6], [["css3"], 6]]
table2 = [[["web"], 2], [["paris"], 3], [["word"], 5], [["class"], 6]]
def extract_words(table)
table.map{|sub_array| sub_array.flatten.first }
end
puts extract_words(table1) - extract_words(table2)
# html5
# css3
With hashes, it would be easier :
hash1 = {"web"=>9, "paris"=>8, "html5"=>6, "css3"=>6}
hash2 = {"web"=>2, "paris"=>3, "word"=>5, "class"=>6}
puts hash1.keys - hash2.keys
# html5
# css3
I assume that you don't need the numerical data, so i would propose a one-line solution to your problem :
(table1.flatten - table2.flatten).reject {|elem| elem.is_a?(Integer)}
That returns the following array :
=> ["html5", "css3"]
table1 = [[["web"], 9], [["paris"], 8], [["html5"], 6], [["css3"], 6] ]
table2 = [[["web"], 2], [["paris"], 3], [["word"], 5], [["class"], 6] ]
table1.flat_map(&:first) - table2.flat_map(&:first)
# => ["html5", "css3"]

How to optimize the solution for Two_sum code in ruby

I am woking on the solution for the following question.
Given an array of integers, return indices of the two numbers such that they add up to a specific target.
You may assume that each input would have exactly one solution.
Example:
Given nums = [2, 7, 11, 15], target = 9,
Because nums[0] + nums[1] = 2 + 7 = 9,
return [0, 1].
This is the solution submitted in ruby after referring the C++ code http://leetcodeunlock.com/2016/05/20/leetcode-1-two-sum-easy/ .
def two_sum(nums, target)
hash = {}
arr = []
nums.each_with_index do |value,index|
y = target - value
if(hash.find{|key,val| key == value})
arr << hash[value]
arr << index
return arr
else
hash[y] = index
end
end
end
My submission failed with the message : Time limit exceeded. Can anyone point out the mistake and help me optimise the code?
nums = [2, 7, 11, 15]
target = 9
# this will find all combinations of 2 elements that add up to 9
results = (0...nums.size).to_a.combination(2).select { |first, last| nums[first] + nums[last] == target }
results.first #=> [0, 1]
Explanation of some parts of the code:
# Get indexes of all elements of nums array
(0...nums.size).to_a #=> [0, 1, 2, 3]
# Generate all combinations of indexes of each 2 elements
(0...nums.size).to_a.combination(2).to_a #=> [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]]
I have modified the line
if(hash.find{|key,val| key == value})
to
if(hash.key?(value))
to find if a specific key is present in the hash and this solved the issue.
Code
def sum_to_num(arr, num)
return [num/2, num/2] if num.even? && arr.count(num/2) > 1
a = arr.uniq.
group_by { |n| (2*n-num).abs }.
find { |_,a| a.size > 1 }
a.nil? ? nil : a.last
end
This method requires three or four passes through the array, if num is even, one to count the instances of num/2, one to remove duplicate values, one to group_by and one to find the pair of numbers that sum to the desired total. It therefore should be much faster than methods that evaluate every pair of the array's elements, particularly as the size of the array is increased.
Examples
sum_to_num [2, 11, 7, 15], 9
#=> [2, 7]
sum_to_num [2, 5, 2, 6, 1, -5, 4], 10
#=> [6, 4]
sum_to_num [2, 7, 11, -7, 15], 0
#=> [7, -7]
sum_to_num [2, 7, 11, 7, 15], 14 #???
sum_to_num [2, -7, 11, -7, 15], -14 #???
sum_to_num [2, 7, 11, 15], 17
#=> [2, 15]
sum_to_num [2, -11, 8, 15], 4
#=> [-11, 15]
sum_to_num [2, -11, 8, 15], -3
#=> [-11, 8]
sum_to_num [2, -11, 8, 15], 100
#=> nil
Explanation
Assume x and y sum to num. Then
2*x-num + 2*y-num = 2*(x+y) - 2*num
= 2*num - 2*num
= 0
meaning that 2*x-num and 2*y-num are either both zero or they have the opposite signs and the same absolute value. Similarly, if 2*x-num and 2*y-num sum to zero, then
2*x-num + 2*y-num = 0
2*(x+y) - 2*num = 0
meaning that n+m = num (which is hardly surprising considering that 2*x+num is a linear transformation.
Suppose
arr = [2, 5, 2, 6, 1, -5, 4]
num = 10
then
if num.even? && arr.count(num/2) > 1
#=> if 10.even? && arr.count(5) > 1
#=> if true && false
#=> false
Therefore, do not return [5,5].
b = arr.uniq
#=> [2, 5, 6, 1, -5, 4]
c = b.group_by { |n| (2*n-num).abs }
#=> {6=>[2], 0=>[5], 2=>[6, 4], 8=>[1], 20=>[-5]}
a = c.find { |_,a| a.size > 1 }
#=> [2, [6, 4]]
return nil if a.nil?
# do not return
a.last
#=> [6, 4]
I was doing this challenge for fun and wrote a cleaned up ruby solution.
def two_sum(nums, target)
hash = {}
nums.each_with_index { |number, index| hash[number] = index }
nums.each_with_index do |number, index|
difference = target - number
if hash[difference] && hash[difference] != index
return [index, hash[difference]]
end
end
end
# #param {Integer[]} nums
# #param {Integer} target
# #return {Integer[]}
def two_sum(nums, target)
length = nums.length
for i in 0..length
j = i+1
for a in j..length
if j < length
if nums[i] + nums[a] == target
return [i, a]
end
end
j+=1
end
end
[]
end
Well this is my way of solving this
def two_sum(nums, target)
nums.each_with_index do |value, index|
match_index = nums.find_index(target - value)
return [index, match_index] if match_index
end
nil
end
The above has the advantage that it stops execution when a match is found and so hopefully won't time out. :)

ruby/rails how to insert column into multidimensional array

I have an array arr = [[1,2],[3,4]] and a column col = [5,6]
Is there an easy way to get an output of [[1,2,5],[3,4,6]] without looping? Thanks
Yes, using Array#transpose as follows:
arr = [[1,2],[3,4]]
col = [5,6]
pp (arr.transpose << col).transpose # => [[1, 2, 5], [3, 4, 6]]

Time select form helper with 12 hour format for Rails 3?

Is there a user-friendly time_select for Rails 3? The default time_select form helper gives you hours[00-23], minutes[00-59] and optionally seconds[00-59]. A dropdown list of hours 0-23 is pretty frustrating for those of us who aren't on military time. A user-friendly solution would display hours 1-12 and an extra am/pm dropdown list.
Is there an option or a separate plugin to handle this? I can't be the first person to want this, but I haven't found any solutions.
Thanks!
You can set :ampm option to true which will show the hours as: 12 PM, 01 AM .. 11 PM.
time_select 'game', 'game_time', {:ampm => true}
http://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html#method-i-time_select
I don't know of a build in helper for exactly what you want. I suggest you build one, and share it! You likely don't want the first item in the dropdown to be 12am either, so that should be a config option.
Also, if 12am is 0 or 24 should be configurable too.
Here's a base using vanilla select_tag:
<%= f.select :your_attribute, [
["6am", 6], ["7am", 7], ["8am", 8], ["9am", 9], ["10am", 10], ["11am", 11], ["12pm", 12], ["1pm", 13], ["2pm", 14], ["3pm", 15], ["4pm", 16], ["5pm", 17], ["6pm", 18], ["7pm", 19], ["8pm", 20], ["9pm", 21], ["10pm", 22], ["11pm", 23], ["12am", 24], ["1am", 1], ["2am", 2], ["3am", 3], ["4am",4 ], ["5am", 5]
], class:''%>
It displays 12hour time to your user, and posts a 24 hour integer to your server.
Update
I needed one anyway, so I went ahead a wrote it....
In the view:
<%= am_pm_hour_select f, :your_method, start:3 %>
(Note the syntax is not f.helper, but helper f, other_options)
In app/helpers/am_pm_form_helper.rb:
module AmPmFormHelper
#pass start:Fixnum in the options hash to set the first drop down selection
def am_pm_hour_select(object, method, options = {}, html_options = {})
select_options = [ ["6am", 6], ["7am", 7], ["8am", 8], ["9am", 9], ["10am", 10], ["11am", 11], ["12pm", 12], ["1pm", 13], ["2pm", 14], ["3pm", 15], ["4pm", 16], ["5pm", 17], ["6pm", 18], ["7pm", 19], ["8pm", 20], ["9pm", 21], ["10pm", 22], ["11pm", 23], ["12am", 24], ["1am", 1], ["2am", 2], ["3am", 3], ["4am",4 ], ["5am", 5]]
unless options[:start].nil?
shift_if_needed = Proc.new{|hour, start| hour<start ? hour+24 : hour}
select_options.sort!{|x, y| shift_if_needed.call(x.last,options[:start]) <=> shift_if_needed.call(y.last, options[:start]) }
end
object.select(method, select_options, options = {}, html_options = {})
end
end
for this kind of purposes i recommend using jQuery UI. it's way more user friendly
here's a link
One simple thing you could do is convert the military 0..23 to standard 12am,1am,2am..10pm,11pm.
This works well for my situation - I'm usually just picking the hour - but it's not ideal, I admit, with the am/pm in the hour drop down.
= javascript_tag "$$('#event_start_at_4i option').each(function(option) { option.update(convert_hour(option.innerHTML)) })"
Then in Javascript (this can probably be cleaner)
function convert_hour(hour_string) {
if (hour_string.substring(0,1) == 0) {
hour_string = hour_string.substring(1);
}
// Funny '08' parses to 0, but '07' parses to 7. Removing leading 0 above fixes.
var mh = parseInt(hour_string)
if (!isNaN(mh)) {
var nh = mh % 12
if (nh == 0) { nh = 12 }
if (mh < 12) {
nh = nh + 'am'
} else {
nh = nh + 'pm'
}
return nh
}
return hour_string
}
I just had this same problem, and I saw this post so I thought I'd give my answer :
module DateTimeSelectorExtensions
def self.included(base)
base.send(:include, InstanceMethods)
base.send(:alias_method_chain, :select_hour, :twelve_hour_time)
end
module InstanceMethods
def select_hour_with_twelve_hour_time
return select_hour_without_twelve_hour_time(#datetime, #options) unless #options[:twelve_hour].eql? true
if #options[:use_hidden] || #options[:discard_hour]
build_hidden(:hour, hour)
else
select_options = []
0.step(23, 1) do |value|
if value == 0 && #options[:midnight]
text = #options[:midnight].eql?(true) ? 'midnight' : #options[:midnight]
elsif value == 12 && #options[:noon]
text = #options[:noon].eql?(true) ? 'noon' : #options[:noon]
else
text = "#{value == 12 ? 12 : (value / 12 == 1 ? value % 12 : value)}#{value <= 11 ? ' AM' : ' PM'}"
end
tag_options = { :value => value }
tag_options[:selected] = "selected" if hour == value
select_options << content_tag(:option, text, tag_options)
end
select_options.rotate!(#options[:offset].to_i)
select_options.unshift(#options[:prompt]) if #options[:prompt].present?
build_select(:hour, (select_options.join("\n") + "\n").html_safe)
end
end
end
end
ActionView::Helpers::DateTimeSelector.send(:include, DateTimeSelectorExtensions)
You use it as such :
select_hour Time.now, :twelve_hour => true, :offset => 6, :prompt => "--", :midnight => true, :noon => "high noon"

Resources