Sorting objects by field availability - ruby-on-rails

I have a place object that has the following parameters: phone, category, street, zip, website.
I also have an array of place objects: [place1, place2, place3, place4, place5].
What's the best way to sort the array of places, based on the parameter availability? I.e., if place1 has the most available parameters, or the least number of parameters that are nil, it should be reordered to first and so on.
Edit: These objects are not ActiveRecord objects

I'd let each Place object know how complete it was:
class Place
attr_accessor :phone, :category, :street, :website, :zip
def completeness
attributes.count{|_,value| value.present?}
end
end
Then it is easy to sort your place objects by completeness:
places.sort_by(&:completeness)
Edit: Non-ActiveRecord solution:
I had assumed this was an ActiveRecord model because of the Ruby on Rails tag. Since this is a non-ActiveRecord model, you can use instance_variables instead of attributes. (By the way, congratulations for knowing that domain models in Rails don't have to inherit from ActiveRecord)
class Place
attr_accessor :phone, :category, :street, :website, :zip
def completeness
instance_variables.count{|v| instance_variable_get(v).present?}
end
end
Edit 2: Weighted attributes
You have a comment about calculating a weighted score. In this case, or when you want to choose specific attributes, you can put the following in your model:
ATTR_WEIGHTS = {phone:1, category:1, street:2, website:1, zip:2}
def completeness
ATTR_WEIGHTS.select{|k,v| instance_variable_get(k).present?}.sum(&:last)
end
Note that the sum(&:last) is equivalent to sum{|k,v| v} which in turn is a railsism for reduce(0){|sum, (k,v)| sum += v}.

I'm sure there's a better way to do it, but this is a start :
ruby fat one liner
values = {phone: 5, category: 3, street: 5, website: 3, zip: 5} #Edit these values to ponderate.
array = [place1, place2, place3, place4, place5]
sorted_array = array.sort_by{ |b| b.attributes.select{ |k, v| values.keys.include?(k.to_sym) && v.present? }.inject(0){ |sum, n| sum + values[n[0]] } }.reverse
So we're basically creating a sub-hash of the attributes of your ActiveRecord object by only picking the key-value pairs that are in the values hash and only if they have a present? value.
Then on this sub-hash, we're invoking inject that will sum the ponderated values we've put in the values hash. Finally, we reverse everything so you have the highest score first.
To make it clean, I suggest you implement a method that will compute the score of each object in an instance method in your model, like mark suggested

If you have a class Place:
class Place
attr_accessor :phone, :category, :street, :website, :zip
end
and you create an instance place1:
place1 = Place.new
place1.instance_variables # => []
place1.instance_variables.size # => 0
place1.phone = '555-1212' # => "555-1212"
place1.instance_variables # => [ :#phone ]
place1.instance_variables.size # => 1
And create the next instance:
place2 = Place.new
place2.phone = '555-1212'
place2.zip = '00000'
place2.instance_variables # => [ :#phone, :#zip ]
place2.instance_variables.size # => 2
You can sort by an ascending number of instance variables that have been set:
[place1, place2].sort_by{ |p| p.instance_variables.size }
# => [ #<Place:0x007fa8a32b51a8 #phone="555-1212">, #<Place:0x007fa8a31f5380 #phone="555-1212", #zip="00000"> ]
Or sort in descending order:
[place1, place2].sort_by{ |p| p.instance_variables.size }.reverse
# => [ #<Place:0x007fa8a31f5380 #phone="555-1212", #zip="00000">, #<Place:0x007fa8a32b51a8 #phone="555-1212"> ]
This uses basic Ruby objects, Rails is not needed, and it asks the object instances themselves what is set, so you don't have to maintain any external lists of attributes.
Note: this breaks if you set an instance variable to something, then set it back to nil.
This fixes it:
[place1,place2].sort_by{ |p|
p.instance_variables.reject{ |v|
p.instance_variable_get(v).nil?
}.size
}.reverse
and this shortens it by using Enumerable's count with a block:
[place1,place2].sort_by{ |p|
p.instance_variables.count{ |v|
!p.instance_variable_get(v).nil?
}
}.reverse

Related

Getting rails3-autocomplete-jquery gem to work nicely with Simple_Form with multiple inputs

So I am trying to implement multiple autocomplete using this gem and simple_form and am getting an error.
I tried this:
<%= f.input_field :neighborhood_id, collection: Neighborhood.order(:name), :url => autocomplete_neighborhood_name_searches_path, :as => :autocomplete, 'data-delimiter' => ',', :multiple => true, :class => "span8" %>
This is the error I get:
undefined method `to_i' for ["Alley Park, Madison"]:Array
In my params, it is sending this in neighborhood_id:
"search"=>{"neighborhood_id"=>["Alley Park, Madison"],
So it isn't even using the IDs for those values.
Does anyone have any ideas?
Edit 1:
In response to #jvnill's question, I am not explicitly doing anything with params[:search] in the controller. A search creates a new record, and is searching listings.
In my Searches Controller, create action, I am simply doing this:
#search = Search.create!(params[:search])
Then my search.rb (i.e. search model) has this:
def listings
#listings ||= find_listings
end
private
def find_listings
key = "%#{keywords}%"
listings = Listing.order(:headline)
listings = listings.includes(:neighborhood).where("listings.headline like ? or neighborhoods.name like ?", key, key) if keywords.present?
listings = listings.where(neighborhood_id: neighborhood_id) if neighborhood_id.present?
#truncated for brevity
listings
end
First of all, this would be easier if the form is returning the ids instead of the name of the neighborhood. I haven't used the gem yet so I'm not familiar how it works. Reading on the readme says that it will return ids but i don't know why you're only getting names. I'm sure once you figure out how to return the ids, you'll be able to change the code below to suit that.
You need to create a join table between a neighborhood and a search. Let's call that search_neighborhoods.
rails g model search_neighborhood neighborhood_id:integer search_id:integer
# dont forget to add indexes in the migration
After that, you'd want to setup your models.
# search.rb
has_many :search_neighborhoods
has_many :neighborhoods, through: :search_neighborhoods
# search_neighborhood.rb
belongs_to :search
belongs_to :neighborhood
# neighborhood.rb
has_many :search_neighborhoods
has_many :searches, through: :search_neighborhoods
Now that we've setup the associations, we need to setup the setters and the attributes
# search.rb
attr_accessible :neighborhood_names
# this will return a list of neighborhood names which is usefull with prepopulating
def neighborhood_names
neighborhoods.map(&:name).join(',')
end
# we will use this to find the ids of the neighborhoods given their names
# this will be called when you call create!
def neighborhood_names=(names)
names.split(',').each do |name|
next if name.blank?
if neighborhood = Neighborhood.find_by_name(name)
search_neighborhoods.build neighborhood_id: neighborhood.id
end
end
end
# view
# you need to change your autocomplete to use the getter method
<%= f.input :neighborhood_names, url: autocomplete_neighborhood_name_searches_path, as: :autocomplete, input_html: { data: { delimiter: ',', multiple: true, class: "span8" } %>
last but not the least is to update find_listings
def find_listings
key = "%#{keywords}%"
listings = Listing.order(:headline).includes(:neighborhood)
if keywords.present?
listings = listings.where("listings.headline LIKE :key OR neighborhoods.name LIKE :key", { key: "#{keywords}")
end
if neighborhoods.exists?
listings = listings.where(neighborhood_id: neighborhood_ids)
end
listings
end
And that's it :)
UPDATE: using f.input_field
# view
<%= f.input_field :neighborhood_names, url: autocomplete_neighborhood_name_searches_path, as: :autocomplete, data: { delimiter: ',' }, multiple: true, class: "span8" %>
# model
# we need to put [0] because it returns an array with a single element containing
# the string of comma separated neighborhoods
def neighborhood_names=(names)
names[0].split(',').each do |name|
next if name.blank?
if neighborhood = Neighborhood.find_by_name(name)
search_neighborhoods.build neighborhood_id: neighborhood.id
end
end
end
Your problem is how you're collecting values from the neighborhood Model
Neighborhood.order(:name)
will return an array of names, you need to also collect the id, but just display the names
use collect and pass a block, I beleive this might owrk for you
Neighborhood.collect {|n| [n.name, n.id]}
Declare a scope on the Neighborhood class to order it by name if you like to get theat functionality back, as that behavior also belongs in the model anyhow.
edit>
To add a scope/class method to neighborhood model, you'd typically do soemthing like this
scope :desc, where("name DESC")
Than you can write something like:
Neighborhood.desc.all
which will return an array, thus allowing the .collect but there are other way to get those name and id attributes recognized by the select option.

Initializing a variable RUBY

I have a class Sample
Sample.class returns
(id :integer, name :String, date :date)
and A hash has all the given attributes as its keys.
Then how can I initialize a variable of Sample without assigning each attribute independently.
Something like
Sample x = Sample.new
x.(attr) = Hash[attr]
How can I iterate through the attributes, the problem is Hash contains keys which are not part of the class attributes too
class Sample
attr_accessor :id, :name, :date
end
h = {:id => 1, :name => 'foo', :date => 'today', :extra1 => '', :extra2 => ''}
init_hash = h.select{|k,v| Sample.method_defined? "#{k}=" }
# This will work
s = Sample.new
init_hash.each{|k,v| s.send("#{k}=", v)}
# This may work if constructor takes a hash of attributes
s = Sample.new(init_hash)
Take a look at this article on Object initialization. You want an initialize method.
EDIT You might also take a look at this SO post on setting instance variables, which I think is exactly what you're trying to do.
Try this:
class A
attr_accessor :x, :y, :z
end
a = A.new
my_hash = {:x => 1, :y => 2, :z => 3, :nono => 5}
If you do not have the list of attributes that can be assigned from the hash, you can do this:
my_attributes = (a.methods & my_hash.keys)
Use a.instance_variable_set(:#x = 1) syntax to assign values:
my_attributes.each do |attr|
a.instance_variable_set("##{attr.to_s}".to_sym, my_hash[attr])
end
Note(Thanks to Abe): This assumes that either all attributes to be updated have getters and setters, or that any attribute which has getter only, does not have a key in my_hash.
Good luck!

2 level deep grouping in ruby

So I have a array of records and I would like to group by 2 levels
Essentially I would like to group_by{|x| x.field1 } and then each value in hash to be further grouped in by field2. Effectively leading to a tree that I can dump out.
def treemaker(array = [])
tree = ledgers.group_by{|x|x.master_group}
tree.each{|x,z| tree[x] = z.group_by{|y| y.account_group}}
tree
end
I would then render tree in a way that i can be put into a "tree" javascript plugin.
Is there a more efficient way?
Sample Input: An Array of ActiveRecord objects, where the model contains, fields master_group, account_group and name
Class MyModel < ActiveRecord::Base
validates :master_group, :account_group, :name, :presence => true
end
Sample Ouput:
{"master_group1" => {"account_group1" => ["name1","name2",...],
"account_groupx" => ["name3", "name4",...],
....},
"master_group2" => {"account_group2" => ["namex", "namey"]},
...
}
I'm not specifically looking for an "SQL grouping" solution (but that would be nice too). Just a solution using enumerables on a any given list of ruby objects.
#xaxxon sent me thinking in the right way basically with the "default value of hash" path.
I think i can now add a method to my model where i can use all sorts of scopes and tack on tree at the end to get my models in tree mode.
class MyModel < ActiveRecord::Base
validates :master_group, :account_group, :name, :presence => true
def self.tree(field1 = 'master_group', field2 = 'account_group')
tree = Hash.new{|hash,key| hash[key] = Hash.new{|h,k| h[k] = []}}
all.each do |item|
tree[item.send('field1')][item.send('field2')].push(item)
end
tree # bob's your uncle!
end
end
MyModel.recent.tree => Hash of Hash of arrays
set up some fake data
foo=[{:a=>1,:b=>2},{:a=>3,:b=>4}]
set up the output data structure
tree={}
populate the output data structure - this is weird looking because you have to populate the hashes that don't exist when they don't exist, hence the ||={} stuff.
foo.each{|thing| (tree[thing[:a]]||={})[thing[:b]]=thing}
looks good.. :a is your master group and :b is your account_group
pp tree
{1=>{2=>{:a=>1, :b=>2}}, 3=>{4=>{:a=>3, :b=>4}}}

Convert array of hashes to array of structs?

Let's say I have two objects: User and Race.
class User
attr_accessor :first_name
attr_accessor :last_name
end
class Race
attr_accessor :course
attr_accessor :start_time
attr_accessor :end_time
end
Now let's say I create an array of hashes like this:
user_races = races.map{ |race| {:user => race.user, :race => race} }
How do I then convert user_races into an array of structs, keeping in mind that I want to be able to access the attributes of both user and race from the struct element? (The key thing is I want to create a new object via Struct so that I can access the combined attributes of User and Race. For example, UserRace.name, UserRace.start_time.)
Try this:
class User
attr_accessor :first_name
attr_accessor :last_name
end
class Race
attr_accessor :course
attr_accessor :start_time
attr_accessor :end_time
end
UserRace = Struct.new(:first_name, :last_name, :course, :start_time, :end_time)
def get_user_race_info
user_races = races.map do |r|
UserRace.new(r.user.first_name, r.user.last_name,
r.course, r.start_time, r.end_time)
end
end
Now let's test the result:
user_races = get_user_race_info
user_races[0].first_name
user_races[0].end_time
Create a definition for the UserRace object (as a Struct), then just make an array of said objects.
UserRace = Struct.new(:user, :race)
user_races = races.map { |race| UserRace.new(race.user, race) }
# ...
user_races.each do |user_race|
puts user_race.user
puts user_race.race
end
if your hash has so many attributes such that listing them all:
user_races = races.map{ |race| {:user => race.user, :race => race, :best_lap_time => 552.33, :total_race_time => 1586.11, :ambient_temperature => 26.3, :winning_position => 2, :number_of_competitors => 8, :price_of_tea_in_china => 0.38 } } # garbage to show a user_race hash with many attributes
becomes cumbersome (or if you may be adding more attributes later), you can use the * ("splat") operator.
the splat operator converts an array into an argument list.
so you can populate Struct.new's argument list with the list of keys in your hash by doing:
UserRace = Struct.new(*races.first.keys)
of course, this assumes all hashes in your array have the same keys (in the same order).
once you have your struct defined, you can use inject to build the final array of objects. (inject greatly simplifies converting many objects from one data type to another.)
user_races.inject([]) { |result, user_race| result << UserRace.new(*user_race.values) }
You have this :::
user_races = races.map{ |race| {:user => race.user, :race => race} }
Now create a Struct as shown below :
UserRace = Struct.new(:user, :race)
And then ::
user_races.each do |user_race|
new_array << UserRace.new(user_race[:user],user_race[:race])
end
Haven't tested the code... should be fine... what say?
EDIT: Here I am adding the objects of UserRace to a new_array.

ruby object array... or hash

I have an object now:
class Items
attr_accessor :item_id, :name, :description, :rating
def initialize(options = {})
options.each {
|k,v|
self.send( "#{k.to_s}=".intern, v)
}
end
end
I have it being assigned as individual objects into an array...
#result = []
some loop>>
#result << Items.new(options[:name] => 'name', options[:description] => 'blah')
end loop>>
But instead of assigning my singular object to an array... how could I make the object itself a collection?
Basically want to have the object in such a way so that I can define methods such as
def self.names
#items.each do |item|
item.name
end
end
I hope that makes sense, possibly I am overlooking some grand scheme that would make my life infinitely easier in 2 lines.
A few observations before I post an example of how to rework that.
Giving a class a plural name can lead to a lot of semantic issues when declaring new objects, as in this case you'd call Items.new, implying you're creating several items when in fact actually making one. Use the singular form for individual entities.
Be careful when calling arbitrary methods, as you'll throw an exception on any misses. Either check you can call them first, or rescue from the inevitable disaster where applicable.
One way to approach your problem is to make a custom collection class specifically for Item objects where it can give you the information you need on names and such. For example:
class Item
attr_accessor :item_id, :name, :description, :rating
def initialize(options = { })
options.each do |k,v|
method = :"#{k}="
# Check that the method call is valid before making it
if (respond_to?(method))
self.send(method, v)
else
# If not, produce a meaningful error
raise "Unknown attribute #{k}"
end
end
end
end
class ItemsCollection < Array
# This collection does everything an Array does, plus
# you can add utility methods like names.
def names
collect do |i|
i.name
end
end
end
# Example
# Create a custom collection
items = ItemsCollection.new
# Build a few basic examples
[
{
:item_id => 1,
:name => 'Fastball',
:description => 'Faster than a slowball',
:rating => 2
},
{
:item_id => 2,
:name => 'Jack of Nines',
:description => 'Hypothetical playing card',
:rating => 3
},
{
:item_id => 3,
:name => 'Ruby Book',
:description => 'A book made entirely of precious gems',
:rating => 1
}
].each do |example|
items << Item.new(example)
end
puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
Do you know the Ruby key word yield?
I'm not quite sure what exactly you want to do. I have two interpretations of your intentions, so I give an example that makes two completely different things, one of them hopefully answering your question:
class Items
#items = []
class << self
attr_accessor :items
end
attr_accessor :name, :description
def self.each(&args)
#items.each(&args)
end
def initialize(name, description)
#name, #description = name, description
Items.items << self
end
def each(&block)
yield name
yield description
end
end
a = Items.new('mug', 'a big cup')
b = Items.new('cup', 'a small mug')
Items.each {|x| puts x.name}
puts
a.each {|x| puts x}
This outputs
mug
cup
mug
a big cup
Did you ask for something like Items.each or a.each or for something completely different?
Answering just the additional question you asked in your comment to tadman's solution: If you replace in tadman's code the definition of the method names in the class ItemsCollection by
def method_missing(symbol_s, *arguments)
symbol, s = symbol_s.to_s[0..-2], symbol_s.to_s[-1..-1]
if s == 's' and arguments.empty?
select do |i|
i.respond_to?(symbol) && i.instance_variables.include?("##{symbol}")
end.map {|i| i.send(symbol)}
else
super
end
end
For his example data you will get following outputs:
puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
puts items.descriptions.join(', ')
# => Faster than a slowball, Hypothetical playing card, A book made entirely of precious gems
As I don't know about any way to check if a method name comes from an attribute or from another method (except you redefine attr_accessor, attr, etc in the class Module) I added some sanity checks: I test if the corresponding method and an instance variable of this name exist. As the class ItemsCollection does not enforce that only objects of class Item are added, I select only the elements fulfilling both checks. You can also remove the select and put the test into the map and return nil if the checks fail.
The key is the return value. If not 'return' statement is given, the result of the last statement is returned. You last statement returns a Hash.
Add 'return self' as the last line of initialize and you're golden.
Class Item
def initialize(options = {})
## Do all kinds of stuff.
return self
end
end

Resources