I do hope the title is correct. Using a Rails 5 project with PostgreSQL and Ruby 2.3.1.
I have enabled hstore in my app. I do not know the correct way to update a table column with an object data. Make sense?
# There will be only two arrays:
cars = ["honda", "bmw"]
rate = [1, 2]
cars.zip(rate).map do |c,r|
Foo.find(1).update_attributes(bar: {c => r})
end
# Foo.find(1).bar = {"bmw" => 2}
I expect:
# Foo.find(1).bar = {"honda" => "1", "bmw" => 2}
How to get the two values into bar?
I was trying to fit one of my previous questions into this but not sure where to start.
You can create a hash from the array
a = cars.zip(rate)
Foo.find(1).update_attributes(bar: Hash[a])
or if you need some enumerator
cars.zip(rate).each_slice(2) { |x| Foo.find(1).update_attributes(bar: Hash[x]) }
Related
I want to remove and return the first element in an AR relation object. I used shift, but it does not remove the object.
With a normal array, shift works as expected:
a = [1, 2, 3]
a.shift # => 1
a # => [2, 3]
This does not work. The variable #attachment_versions retains the same size before and after using shift.
#attachment = Attachment.with_associations.find(params[:id])
#attachment_versions = #attachment.attachment_versions
#current_version = #attachment_versions.shift
# this raises `true`
raise #attachment_versions.include?(#current_version).to_s
def self.with_associations
includes(attachment_versions: :owner, comments: [:author, :attachments])
end
I know that an AR relation object is more than just an Array, but I thought shift should work in the same way.
You can pretty easily turn it into an array first.
i.e.
#attachment_versions_array = #attachment_versions.to_a
#attachment_versions_array.shift
=> <Attachment_version......etc
I have the following in my url. I need to extract both 4 and 2 separately for the purpose of searching. These two integer, one is category id and other is sub category id
params[:id].scan(/\d+$/).first
using the above scan i can get 4 but how can i get more than one integers
my-url-for-test-4-2
I have created a helper method
def string_to_int(param,key)
param.scan(/\d+/).map(&:to_i).key
end
And i tried it from my controller like this
id = string_to_int(params[:id],'first')
But getting error as
undefined method `key' for [4, 2]:Array
Why it is not acception.
Answer lies in your question
params[:id].scan(/\d/) will result in array.
params[:id].scan(/\d+/).map(&:to_i)
If you are passing first or last as key :
def string_to_int(param,key)
param[:id].scan(/\d+/).map(&:to_i).send(key)
end
You can match against the last two numerical parts (separated by hyphens) in your :id parameter:
string = params[:id].to_s
ids = string.scan(/-(\d+)-(\d+)$/)[0] # .try(:map, &:to_i)
Update:
Here's a not-too-edge case that would be handled well:
a = "random1-2string-3-4-5"
a.scan(/-(\d+)-(\d+)$/) # => [["4", "5"]]
a.scan(/-(\d+)-(\d+)$/)[0] # => ["4", "5"]
a.scan(/-(\d+)-(\d+)$/)[0].try(:map, &:to_i) # => [4, 5]
I have an application that dynamically creates a drop-down menu based on certain values in the database. Often the drop-down values are just in the order they come up but I would like to put them in a certain order.
An example of my value system:
Newbie = 0
Amateur = 1
Skilled = 2
Pro = 3
GrandMaster = 4
How would I take the data above and use it to sort an array full of those values (Newbie etc). I've thought about creating a hash of the values but even then I still am not sure how to apply that to the sort method.
Any help would be appreciated.
You can sort this array just by using the usual sorting the sorting won't be done by name it will be done by value. and if these are not integer objects and are some user defined class then sorting based on a particular attribute can be achieved very efficiently by
lst.sort_by &:first
where first is the attribute of the object.
Sort has by value:
hash = {:Newbie=>0, :Amateur=>1, :Skilled=>2, :Pro=>3}
> hash.sort { hash{a} <=> hash{b} }
=> [[:Newbie, 0], [:Amateur, 1], [:Skilled, 2], [:Pro, 3]]
Or use Ruby Hash#sort_by method:
hash.sort_by { |k,v| v }
Suppose you have a Level model that has a sort_id identifying the displayed order and a name holding the displayed name. I recommend using default_scope to set the default order for that model because it is likely that you always want to sort Level records this way:
class Level < ActiveRecord::Base
#### attributes
# id (integer)
# name (string)
# sort_id (integer)
default_scope order('sort_id ASC')
# rest of model...
end
Then, the only thing you have to do in your view to display a picklist is
<%= f.select("level", Level.pluck(:name)) %>
An alternate to #padde. I prefer to avoid default scopes.
class Level < ActiveRecord::Base
#### attributes
# id (integer)
# name (string)
# value (integer)
end
In the view
<%= f.select("level", Level.order(:value).map{|l| [l.name, l.value] } %>
Due to my poorly explained question the others trying to answer my question didn't really get a chance but using their help I did manage to figure out my problem.
ex_array = ["GrandMaster", "Newbie", "Pro", "Skilled", "Amateur"]
value_sys = {:Newbie=>0, :Amateur=>1, :Skilled=>2, :Pro=>3, :GrandMaster=>4}
ex_array.sort { |a,b| value_sys[a.to_sym] <=> value_sys[b.to_sym]
=> ["Newbie", "Amateur", "Skilled","Pro", "GrandMaster"]
Thanks for the help guys. Much appreciated.
Parse your value system for further use:
values = <<EOF
Newbie = 0
Amateur = 1
Skilled = 2
Pro = 3
GrandMaster = 4
EOF
value_map = Hash[values.split("\n").map{|v| v.split(/\s*=\s*/)}.map{|v| [v[0], v[1].to_i]}]
#=> {"Newbie"=>0, "Amateur"=>1, "Skilled"=>2, "Pro"=>3, "GrandMaster"=>4}
Assign the array values a weight according to value_map to transform the array into a new one, sort according to the weight, and then transform the new array back.
# here I created a sample array
array = value_map.keys.shuffle
#=> ["Newbie", "Pro", "Skilled", "Amateur", "GrandMaster"]
# transform and sort
sorted = array.map{|v| [v, value_map[v] || 0xFFFF]}.sort_by{|v| v[1]}.map{|v| v[0]}
#=> ["Newbie", "Amateur", "Skilled", "Pro", "GrandMaster"]
Or you can bypass the transform step and just use the sort_by method:
sorted = array.sort_by{|v| value_map[v] || 0xFFFF}
I'm building a custom profile completeness tool in Rails 3.1. Using active_admin, the administrator wants to be able to pick model attributes and assign a score value to them. I create a table with 'name' and 'score' in it. The name being the column name to apply the score to.
I need to compare the values from the name column in the scoring table when the model gets updated. Psuedocode:
ProfileScore.find(:all, :select => "name,score").inject { |p,score|
#this is where im stuck, 'self' == updated model hash
p[:name] == self.attribute.updated
score += p[:score]
}
Of course other approaches are welcome! I looked at completeness-fu but it is way out of date.
score = ProfileScore.select("name, score").inject(0) { |memo, pscore|
# the result is a list of ProfileScore objects, not of pairs.
# if p[:name] == self.attribute.updated # don't you want to take into account
# also old, but present, attributes??
if self.attributes[p[:name]].present?
memo += p[:score]
end
memo
}
or also
present_attributes = self.attributes.reject { |k,v| v.blank? }.keys
score = ProfileScore.where(:name => present_attributes).sum("score")
To get the total score, you can use:
total_score = ProfileScore.where(:name => self.attribute.updated).sum(:score)
I want to obtain an array of ActiveRecord objects given an array of ids.
I assumed that
Object.find([5,2,3])
Would return an array with object 5, object 2, then object 3 in that order, but instead I get an array ordered as object 2, object 3 and then object 5.
The ActiveRecord Base find method API mentions that you shouldn't expect it in the order provided (other documentation doesn't give this warning).
One potential solution was given in Find by array of ids in the same order?, but the order option doesn't seem to be valid for SQLite.
I can write some ruby code to sort the objects myself (either somewhat simple and poorly scaling or better scaling and more complex), but is there A Better Way?
It's not that MySQL and other DBs sort things on their own, it's that they don't sort them. When you call Model.find([5, 2, 3]), the SQL generated is something like:
SELECT * FROM models WHERE models.id IN (5, 2, 3)
This doesn't specify an order, just the set of records you want returned. It turns out that generally MySQL will return the database rows in 'id' order, but there's no guarantee of this.
The only way to get the database to return records in a guaranteed order is to add an order clause. If your records will always be returned in a particular order, then you can add a sort column to the db and do Model.find([5, 2, 3], :order => 'sort_column'). If this isn't the case, you'll have to do the sorting in code:
ids = [5, 2, 3]
records = Model.find(ids)
sorted_records = ids.collect {|id| records.detect {|x| x.id == id}}
Based on my previous comment to Jeroen van Dijk you can do this more efficiently and in two lines using each_with_object
result_hash = Model.find(ids).each_with_object({}) {|result,result_hash| result_hash[result.id] = result }
ids.map {|id| result_hash[id]}
For reference here is the benchmark i used
ids = [5,3,1,4,11,13,10]
results = Model.find(ids)
Benchmark.measure do
100000.times do
result_hash = results.each_with_object({}) {|result,result_hash| result_hash[result.id] = result }
ids.map {|id| result_hash[id]}
end
end.real
#=> 4.45757484436035 seconds
Now the other one
ids = [5,3,1,4,11,13,10]
results = Model.find(ids)
Benchmark.measure do
100000.times do
ids.collect {|id| results.detect {|result| result.id == id}}
end
end.real
# => 6.10875988006592
Update
You can do this in most using order and case statements, here is a class method you could use.
def self.order_by_ids(ids)
order_by = ["case"]
ids.each_with_index.map do |id, index|
order_by << "WHEN id='#{id}' THEN #{index}"
end
order_by << "end"
order(order_by.join(" "))
end
# User.where(:id => [3,2,1]).order_by_ids([3,2,1]).map(&:id)
# #=> [3,2,1]
Apparently mySQL and other DB management system sort things on their own. I think that you can bypass that doing :
ids = [5,2,3]
#things = Object.find( ids, :order => "field(id,#{ids.join(',')})" )
A portable solution would be to use an SQL CASE statement in your ORDER BY. You can use pretty much any expression in an ORDER BY and a CASE can be used as an inlined lookup table. For example, the SQL you're after would look like this:
select ...
order by
case id
when 5 then 0
when 2 then 1
when 3 then 2
end
That's pretty easy to generate with a bit of Ruby:
ids = [5, 2, 3]
order = 'case id ' + (0 .. ids.length).map { |i| "when #{ids[i]} then #{i}" }.join(' ') + ' end'
The above assumes that you're working with numbers or some other safe values in ids; if that's not the case then you'd want to use connection.quote or one of the ActiveRecord SQL sanitizer methods to properly quote your ids.
Then use the order string as your ordering condition:
Object.find(ids, :order => order)
or in the modern world:
Object.where(:id => ids).order(order)
This is a bit verbose but it should work the same with any SQL database and it isn't that difficult to hide the ugliness.
As I answered here, I just released a gem (order_as_specified) that allows you to do native SQL ordering like this:
Object.where(id: [5, 2, 3]).order_as_specified(id: [5, 2, 3])
Just tested and it works in SQLite.
Justin Weiss wrote a blog article about this problem just two days ago.
It seems to be a good approach to tell the database about the preferred order and load all records sorted in that order directly from the database. Example from his blog article:
# in config/initializers/find_by_ordered_ids.rb
module FindByOrderedIdsActiveRecordExtension
extend ActiveSupport::Concern
module ClassMethods
def find_ordered(ids)
order_clause = "CASE id "
ids.each_with_index do |id, index|
order_clause << "WHEN #{id} THEN #{index} "
end
order_clause << "ELSE #{ids.length} END"
where(id: ids).order(order_clause)
end
end
end
ActiveRecord::Base.include(FindByOrderedIdsActiveRecordExtension)
That allows you to write:
Object.find_ordered([2, 1, 3]) # => [2, 1, 3]
Here's a performant (hash-lookup, not O(n) array search as in detect!) one-liner, as a method:
def find_ordered(model, ids)
model.find(ids).map{|o| [o.id, o]}.to_h.values_at(*ids)
end
# We get:
ids = [3, 3, 2, 1, 3]
Model.find(ids).map(:id) == [1, 2, 3]
find_ordered(Model, ids).map(:id) == ids
Another (probably more efficient) way to do it in Ruby:
ids = [5, 2, 3]
records_by_id = Model.find(ids).inject({}) do |result, record|
result[record.id] = record
result
end
sorted_records = ids.map {|id| records_by_id[id] }
Here's the simplest thing I could come up with:
ids = [200, 107, 247, 189]
results = ModelObject.find(ids).group_by(&:id)
sorted_results = ids.map {|id| results[id].first }
#things = [5,2,3].map{|id| Object.find(id)}
This is probably the easiest way, assuming you don't have too many objects to find, since it requires a trip to the database for each id.