I have an array of objects. I need to use an SQL-like condition WHERE field like '%value%' for some object fields in this array.
How to do it?
Edited:
For example I have array with Users and I need to find all users with first_name like ike and email like 123.
Edited2:
I need method to get Users with first_name like smth and email like smth from ARRAY of my Users. Users have first_name and email.
Edited3:
All my users are in database. But I have some business logic, at the end of this logic I have array with Users. Next I need to filter this array with some text: ike for first_name and 123 for email. How to do it?
arr = %w[hello quick bool boo foo]
arr.select { |x| x.include?("foo") }
=> ["bool", "boo", "foo"]
or in your case, if you have an array of objects, you can do:
x.first_name.include?("foo") && x.email.include?("123")
For more customization, you can use Array#select with Regexeps
If you can just use ruby methods for this that do something like this:
User = Struct.new(:email, :first_name) # Just creating a cheap User class here
users = [
User.new('1#a.com' , 'ike'),
User.new('123#a.com', 'bob'),
User.new('123#a.com', 'ike'),
]
# results will be an array holding only the last element in users
results = users.find_all do |user|
user.email =~ /123/ and
user.first_name =~ /ike/
end
Writing your own sql parser seems like a pretty bad idea, but if you really need to parse simple SQL where clauses you could do something like this:
User = Struct.new(:email, :first_name) # Just creating a cheap User class here
users = [
User.new('1#a.com' , 'ike'),
User.new('123#a.com', 'bob'),
User.new('123#a.com', 'ike'),
]
def where(array, sql)
sql = sql.gsub(/\s+AND\s+/, ' ') # remove AND's
terms = Hash[ *sql.split(/\s+LIKE\s+| /) ] # turn "a LIKE 'b'" into {'a': "'b'"}
array.find_all do |item|
terms.all? do |attribute, matcher|
matcher = matcher.gsub('%', '.*') # convert %
matcher = matcher.gsub(/^['"]|["']$/, '') # strip quotes
item.send(attribute) =~ /^#{matcher}$/
end
end
end
# results will be an array holding only the last element in users
results = where(users, "first_name LIKE '%ike%' AND email LIKE '%123%'")
This will only work for where clauses what only contain LIKE statements connected by AND's. Adding support for all valid SQL is left as an exercise for the reader, (or better yet, just left alone).
I've built a ruby gem uber_array to enable sql-like syntax for arrays of Hashes or Objects you might want to try.
require 'uber_array'
# Array of Hash elements with strings as keys
items = [
{ 'name' => 'Jack', 'score' => 999, 'active' => false },
{ 'name' => 'Jake', 'score' => 888, 'active' => true },
{ 'name' => 'John', 'score' => 777, 'active' => true }
]
uber_items = UberArray.new(items)
uber_items.where('name' => 'John')
uber_items.where('name' => /Ja/i)
uber_items.like('ja')
uber_items.where('name' => %w(Dave John Tom))
uber_items.where('score' => 999)
uber_items.where('score' => ->(s){s > 900})
uber_items.where('active' => true, 'score' => 800..900)
Related
I am looking for the Ruby/Rails way to approach the classic "select items from a set based on matches with another set" task.
Set one is a simple hash, like this:
fruits = {:apples => "red", :oranges => "orange", :mangoes => "yellow", :limes => "green"}
Set two is an array, like this:
breakfast_fruits = [:apples, :oranges]
The desired outcome is a hash containing the fruits that are listed in Breakfast_fruits:
menu = {:apples => "red", :oranges => "orange"}
I've got a basic nested loop going, but am stuck on basic comparison syntax:
menu = {}
breakfast_fruits.each do |brekky|
fruits.each do |fruit|
//if fruit has the same key as brekky put it in menu
end
end
I'd also love to know if there is a better way to do this in Ruby than nested iterators.
You can use Hash#keep_if:
fruits.keep_if { |key| breakfast_fruits.include? key }
# => {:apples=>"red", :oranges=>"orange"}
This will modify fruits itself. If you don't want that, a little modification of your code works:
menu = {}
breakfast_fruits.each do |brekky|
menu[brekky] = fruits[brekky] if breakfast_fruits.include? brekky
end
ActiveSupport (which comes with Rails) adds Hash#slice:
slice(*keys)
Slice a hash to include only the given keys. Returns a hash containing the given keys.
So you can say things like:
h = { :a => 'a', :b => 'b', :c => 'c' }.slice(:a, :c, :d)
# { :a => 'a', :c => 'c' }
In your case, you'd splat the array:
menu = fruits.slice(*breakfast_fruits)
I have a model Event that is connected to MongoDB using Mongoid:
class Event
include Mongoid::Document
include Mongoid::Timestamps
field :user_name, type: String
field :action, type: String
field :ip_address, type: String
scope :recent, -> { where(:created_at.gte => 1.month.ago) }
end
Usually when I use ActiveRecord, I can do something like this to group results:
#action_counts = Event.group('action').where(:user_name =>"my_name").recent.count
And I get results with the following format:
{"action_1"=>46, "action_2"=>36, "action_3"=>41, "action_4"=>40, "action_5"=>37}
What is the best way to do the same thing with Mongoid?
Thanks in advance
I think you'll have to use map/reduce to do that. Look at this SO question for more details:
Mongoid Group By or MongoDb group by in rails
Otherwise, you can simply use the group_by method from Enumerable. Less efficient, but it should do the trick unless you have hundreds of thousands documents.
EDIT: Example of using map/reduce in this case
I'm not really familiar with it but by reading the docs and playing around I couldn't reproduce the exact same hash you want but try this:
def self.count_and_group_by_action
map = %Q{
function() {
key = this.action;
value = {count: 1};
emit(key, value);
# emit a new document {"_id" => "action", "value" => {count: 1}}
# for each input document our scope is applied to
}
}
# the idea now is to "flatten" the emitted documents that
# have the same key. Good, but we need to do something with the values
reduce = %Q{
function(key, values) {
var reducedValue = {count: 0};
# we prepare a reducedValue
# we then loop through the values associated to the same key,
# in this case, the 'action' name
values.forEach(function(value) {
reducedValue.count += value.count; # we increment the reducedValue - thx captain obvious
});
# and return the 'reduced' value for that key,
# an 'aggregate' of all the values associated to the same key
return reducedValue;
}
}
self.map_reduce(map, reduce).out(inline: true)
# we apply the map_reduce functions
# inline: true is because we don't need to store the results in a collection
# we just need a hash
end
So when you call:
Event.where(:user_name =>"my_name").recent.count_and_group_by_action
It should return something like:
[{ "_id" => "action1", "value" => { "count" => 20 }}, { "_id" => "action2" , "value" => { "count" => 10 }}]
Disclaimer: I'm no mongodb nor mongoid specialist, I've based my example on what I could find in the referenced SO question and Mongodb/Mongoid documentation online, any suggestion to make this better would be appreciated.
Resources:
http://docs.mongodb.org/manual/core/map-reduce/
http://mongoid.org/en/mongoid/docs/querying.html#map_reduce
Mongoid Group By or MongoDb group by in rails
I am trying to do this:
query << ["AND f.name LIKE '%:last_name%' ", :last_name => params[:last_name] ]
But getting an error. Surely the syntax is incorrect.
Does anyone know how to do it?
Is this an array or an hash? the first item looks like an array and the second like a hash. Without some context it is hard to tell. You can start by maybe trying this:
["AND f.name LIKE '%:last_name%' ", {:last_name => params[:last_name]} ]
Where query is an array you may want to pass a string so you don't end up with an array of arrays, you could do this:
Rails 3
query << "AND f.name LIKE '%#{params[:last_name]}%' "
Rails 2
last_name = ActionController::Base.helpers.sanitize(params[:last_name])
query << "AND f.name LIKE '%#{last_name}%' "
since you're not using rails 3, you should use scoped to chain queries
records = Record.scoped(:conditions => { :active => true })
records = records.scoped(:conditions => ["records.name LIKE '%:last_name%' ", { :last_name => params[:last_name] }])
so you don't have to build queries like that.
There is a search action in RoR that can handle some params e.g.:
params[:name] # can be nil or first_name
params[:age] # can be nil or age
params[:city] # can be nil or country
params[:tag] # can be nil or country
The model name is Person. It also has_many :tags.
When finding persons I need like to AND all the conditions that are present. Of course, it not rational and not DRY.
What I tried to do:
conditions = []
conditions << [ "name like ?", params[:name]+"%" ] if params[:name].present?
conditions << [ "age = ?", params[:age] ] if params[:age].present?
conditions << [ "city = like ?", params[:city]+"%" ] if params[:city].present?
#persons = Person.all(:conditions => conditions )
#What about tags? How do include them if params[:tag].present?
Of course, I want my code to be DRY. Now it's not. Even more, it will cause an exception if params[:age] and params[:name] and params[:city] are not present.
How can I solve? And how do I include tags for persons filtered by tag.name=params[:tag] (if params[:tag].present?) ?
You should do something like this:
lets say filters parameters include this:
filters = {
:name_like => "Grienders",
:age_equal => 15
}
Now you define methods for each
class Person
def search_with_filters(filters)
query = self.scoped
filters.each do |key, values|
query = query.send(key, values)
end
return query
end
def name_like(name)
where("name like ?", name)
end
def age_equal(age)
where(:age => age
end
end
Do you see the method search_with_filters will be the "controlling" method that will take a set of query conditions (name_like, age_equal, etc...) and pass them out to matching method name using the send method, and along with that we also pass the condition which will be the parameter of the method.
The reason why this way is good is because you can scale to any number of conditions (your filter lets say get huge) and also the code is very clean because all you have to do is populate your filters parameter. The method is very readable and very modular and also easy to test
To include the tags condition in your query:
if params[:tag].present?
#persons = Person.all(:conditions => conditions).includes(:tags).where("tags.name = ?", params[:tag])
else
#persons = Person.all(:conditions => conditions)
end
As for there error you are getting when params[:age] .. is not present - This is very strange because it is supposed to return false incase the key is not set in params. Could you please paste the error you are getting?
I would work extensively with scopes for this. Starting with default scope, merge other scopes based on conditions.
Write scopes (or class methods):
class Person << AR::Base
...
NAME_PARTS = ['first_name', 'last_name']
scope :by_name, lambda { |q| where([NAME_PARTS.join(' LIKE :q OR ') + ' LIKE :q', { :q => "%#{q}%" }]) }
scope :by_email, lambda { |q| joins(:account).where(["accounts.email LIKE :q", { :q => "%#{q}%" }]) }
scope :by_age, where(["people.age = ?", q])
scope :tagged, lambda { |q| joins(:tags).where(["tags.name LIKE :q", { :q => "%#{q}%" }]) }
end
Refer:
Scopes in Rails 3
Scopes overhaul section in How Rails 3 Makes Your Life Better
Now, merge the scopes only when the condition is met. As I understand your condition, is the value for a particular search item is nil (like, age is not given), do not search for that scope.
...
def search(object)
interested_fields = ['name', 'age', 'email', 'tags']
criteria = object.attributes.extract!(*interesting_fields) # returns { :age => 20, ... }
scope = object.class.scoped
build(criteria).each do |k, v|
scope = scope.send(k.to_sym, v)
end
scope.all
end
# This method actually builds the search criteria.
# Only keep param which has value and reject the rest.
def build(params)
required = ['name', 'age', 'email', 'tags']
params.delete_if { |k, v| required.include?(k) && v.blank? }
params
end
...
Refer:
extract!
I'm working with a User model that includes booleans for 6 days
[sun20, mon21, tue22, wed23, thur24, fri25]
with each user having option to confirm which of the 6 days they are participating in.
I'm trying to define a simple helper method:
def day_confirmed(day)
User.where(day: true).count
end
where I could pass in a day and find out how many users are confirmed for that day, like so:
day_confirmed('mon21')
My problem is that when I use day in the where(), rails assumes that I'm searching for a column named day instead of outputting the value that I'm passing in to my method.
Surely I'm forgetting something, right?
This syntax:
User.where( day: true )
Is equivalent to:
User.where( :day => true )
That's because using : in a hash instead of =>, e.g. { foo: bar }, is the same as using a symbol for the key, e.g. { :foo => bar }. Perhaps this will help:
foo = "I am a key"
hsh = { foo: "bar" }
# => { :foo => "bar" }
hsh.keys
# => [ :foo ]
hsh = { foo => "bar" }
# => { "I am a key" => "bar" }
hsh.keys
# => [ "I am a key" ]
So, if you want to use the value of the variable day rather than the symbol :day as the key, try this instead:
User.where( day => true )
If these are your column names, [sun20, mon21, tue22, wed23, thur24, fri25]
And you are calling day_confirmed('mon21') and trying to find the column 'mon21' where it is true, you can use .to_sym on the date variable
def day_confirmed(day)
User.where(day.to_sym => true).count
end
the .to_sym will get the value of date, and covert it to :mon21