Creating a scope from an array constant - ruby-on-rails

I have a situation where I have an array constant that I'd like to perform a string search on through a scope. I usually use AR to accomplish this but wasn't sure how to incorporate this with a static array. Obviously using a where clause wouldn't work here. What would be the best solution?
class Skills
SALES_SKILLS = %w(
Accounting
Mentoring
...
)
# Search above array based on "skill keyword"
scope :sales_skills, ->(skill) { }
end

May be using Enumerable#grep and convert string to case ignoring regexp with %r{} literal
class Skills
SALES_SKILLS = %w(
Accounting
Mentoring
#...
)
def self.sales_skills(skill)
SALES_SKILLS.grep(%r{#{skill}}i)
end
end
Skills.sales_skills('acc')
#=> ["Accounting"]
Skills.sales_skills('o')
#=> ["Accounting", "Mentoring"]
Skills.sales_skills('accounting')
#=> ["Accounting"]
Skills.sales_skills('foo')
#=> []

It would be better to create a method for this as you want to return a string. Scope is designed to return an ActiveRecord::Relation:
Scoping allows you to specify commonly-used queries which can be referenced as method calls on the association objects or models. With these scopes, you can use every method previously covered such as where, joins and includes. All scope bodies should return an ActiveRecord::Relation or nil to allow for further methods (such as other scopes) to be called on it.
Reference: https://guides.rubyonrails.org/active_record_querying.html#scopes

class Skills
SALES_SKILLS = %w(
Accounting
Mentoring
#...
)
def self.sales_skills(skill)
SALES_SKILLS.select do |sales_skill|
sales_skill.downcase.include?(skill.downcase)
end
end
end

Related

How to iterate over an array with its elements and some other variable derived from each element

I'm trying to find a one-liner or better way to get scope and scope_capitalized. How can I do it?
I need scope and scope_capitalized both assigned
Instantiating variables is NOT an option
Code so far
['users', 'companies'].each do |scope|
scope_capitalized = scope.capitalized
# do stuff with scope and scope_capitalized
end
# if I had ['users', 'companies'] in a variable 'a' I could do
a.zip(a.map(&:capitalized)).each do |scope, scope_capitalized|
Depending on how you intend to act on the data, you might do something like this, where you first create an array containing the pairs:
terms = ['users', 'companies']
terms_with_capitals = terms.map{ |scope| [scope, scope.capitalized] }
terms_with_capitals # => [['users', 'Users'],['companies','Companies']]
%w(users companies).map{|s|[s, s.capitalized]}.each do |scope, scope_capitalized|
...
end
This bypasses instantiation
['users','companies'].map(&:capitalize) would return an array of capitalized items as a one-liner. However, if you're trying to "do stuff" with the capitalized string, a one-liner may not be a good idea because you could end up with a very long, unintelligible chain of things. White space doesn't cost anything, and readability is always favorable. Frankly, I would do something like this:
%w(users companies).map(&:capitalize).each do |scope|
# do scope stuff
end
Your question has all the information you need really. You say:
# if I had ['users', 'companies'] in a variable 'a' I could do
a.zip(a.map(&:capitalized)).each do |scope, scope_capitalized|
So then just put your array in place of a
['users', 'companies'].zip(['users', 'companies'].map(&:capitalized)).each do |scope, scope_capitalized|

Execute method on mongoid scope chain

I need to take some random documents using Rails and MongoId. Since I plan to have very large collections I decided to put a 'random' field in each document and to select documents using that field. I wrote the following method in the model:
def random(qty)
if count <= qty
all
else
collection = [ ]
while collection.size < qty
collection << where(:random_field.gt => rand).first
end
collection
end
end
This function actually works and the collection is filled with qty random elements. But as I try to use it like a scope like this:
User.students.random(5)
I get:
undefined method `random' for #<Array:0x0000000bf78748>
If instead I try to make the method like a lambda scope I get:
undefined method `to_criteria' for #<Array:0x0000000df824f8>
Given that I'm not interested in applying any other scopes after the random one, how can I use my method in a chain?
Thanks in advance.
I ended up extending the Mongoid::Criteria class with the following. Don't know if it's the best option. Actually I believe it's quite slow since it executes at least qty queries.
I don't know if not_in is available for normal ActiveRecord modules. However you can remove the not_in part if needed. It's just an optimization to reduce the number of queries.
On collections that have a double (or larger) number of documents than qty, you should have exactly qty queries.
module Mongoid
class Criteria
def random(qty)
if count <= qty
all
else
res = [ ]
ids = [ ]
while res.size < qty
el = where(:random_field.gt => rand).not_in(id: ids).first
unless el.nil?
res << el
ids << el._id
end
end
res
end
end
end
end
Hope you find this useful :)

Rails Association Extensions: Override a HABTM assignment (collection=) method

In a question I've previously answered, I used an association extension to override a HABTM collection's append (<<) method (Also, a similar question):
has_and_belongs_to_many(:countries) do
def <<(country)
if [*country].all? {|c| c.is_a?(String) }
countries = Country.where(:code => country)
concat(*countries)
else
concat(country)
end
end
end
This is probably not encouraged, but my question is, how can one override, if even possible, the assignment operator, so I can do countries = ['IL', 'US'] with the same results?
I think that it could be something like:
def =(countries)
values = countries.kind_of?(Hash) ? countries.values : countries
values.each do |country|
self << country
end
end
I'm not sure if you can use the operator << that way.

How to move this to a base class, its a Ruby enumeration

I read this article that explained how to make enumerations in Ruby, and also showed how to them enumerable like:
class Blah
def Blah.add_item(key, value)
#hash ||= {}
#hash[key] = value
end
def Blah.const_missing(key)
#hash[key]
end
def Blah.each
#hash.each {|key, value| yield(key, value)}
end
end
I have other enumerations that I need, can I create a base class somehow from this so I don't have to repeat the methods add_item, const_missng and .each for each one?
When creating a file for all my enums, I put it in /lib/enums.rb, is that a good practise?
should I be putting this class inside of a module i.e. I believe you do that for a namespace right?
You can just use Blah as your base class.
class C < Blah; end
class D < Blah; end
I think I might just throw it in with the source code of each project it's used with. Yes, DIE, DRY, and all that, but that's mostly important in line-by-line code. It's a fairly common practice to merge external software with each project.
No. It's already a class, so it's using only one name. Put the module around the code that uses Blah, the project or section of a project. That will be large and more in need of namespacing.
DigitalRoss's answer is good. I'll present an alternative. Suppose you'd like each of your enumerations to live in a module. All you need is a little Enumeration module, like so:
module Enumeration
include Enumerable
def self.included(m)
m.extend self
end
def each(&block)
constants.find_all do |name|
name =~ /^[A-Z_\d]+$/
end.map do |name|
[name, const_get(name)]
end.sort_by(&:last).each(&block)
end
end
When you need an enumeration, create a module for it, include Enumeration, and define your keys and values as constants with all-caps names.
module States
include Enumeration
INIT = 1
RUN = 2
DONE = 3
end
The module will respond to any of the methods provided by Enumerable:
p States.to_a
# => [["INIT", 1], ["RUN", 2], ["DONE", 3]]
You may find that you sometimes don't care what the values are, just that they are distinct. Let's add to Enumeration a method value that makes it easy to create constants with auto-incrementing keys:
module Enumeration
def value(name, value = next_value)
const_set(name, value)
end
def next_value
(map(&:last).max || 0) + 1
end
end
Now let's have some planets:
module Planets
include Enumeration
value :MERCURY
value :VENUS
value :EARTH
end
p Planets.to_a
# => [["MERCURY", 1], ["VENUS", 2], ["EARTH", 3]]
Of course, these enumerations are just collections of normal constants, so you can use them directly:
p Planets::MERCURY # => 1

Rails, using time_select on a non active record model

I am trying to use a time_select to input a time into a model that will then perform some calculations.
the time_select helper prepares the params that is return so that it can be used in a multi-parameter assignment to an Active Record object.
Something like the following
Parameters: {"commit"=>"Calculate", "authenticity_token"=>"eQ/wixLHfrboPd/Ol5IkhQ4lENpt9vc4j0PcIw0Iy/M=", "calculator"=>{"time(2i)"=>"6", "time(3i)"=>"10", "time(4i)"=>"17", "time(5i)"=>"15", "time(1i)"=>"2009"}}
My question is, what is the best way to use this format in a non-active record model. Also on a side note. What is the meaning of the (5i), (4i) etc.? (Other than the obvious reason to distinguish the different time values, basically why it was named this way)
Thank you
You can create a method in the non active record model as follows
# This will return a Time object from provided hash
def parse_calculator_time(hash)
Time.parse("#{hash['time1i']}-#{hash['time2i']}-#{hash['time3i']} #{hash['time4i']}:#{hash['time5i']}")
end
You can then call the method from the controller action as follows
time_object = YourModel.parse_calculator_time(params[:calculator])
It may not be the best solution, but it is simple to use.
Cheers :)
The letter after the number stands for the type to which you wish it to be cast. In this case, integer. It could also be f for float or s for string.
I just did this myself and the easiest way that I could find was to basically copy/paste the Rails code into my base module (or abstract object).
I copied the following functions verbatim from ActiveRecord::Base
assign_multiparameter_attributes(pairs)
extract_callstack_for_multiparameter_attributes(pairs)
type_cast_attribute_value(multiparameter_name, value)
find_parameter_position(multiparameter_name)
I also have the following methods which call/use them:
def setup_parameters(params = {})
new_params = {}
multi_parameter_attributes = []
params.each do |k,v|
if k.to_s.include?("(")
multi_parameter_attributes << [ k.to_s, v ]
else
new_params[k.to_s] = v
end
end
new_params.merge(assign_multiparameter_attributes(multi_parameter_attributes))
end
# Very simplified version of the ActiveRecord::Base method that handles only dates/times
def execute_callstack_for_multiparameter_attributes(callstack)
attributes = {}
callstack.each do |name, values|
if values.empty?
send(name + '=', nil)
else
value = case values.size
when 2 then t = Time.new; Time.local(t.year, t.month, t.day, values[0], values[min], 0, 0)
when 5 then t = Time.time_with_datetime_fallback(:local, *values)
when 3 then Date.new(*values)
else nil
end
attributes[name.to_s] = value
end
end
attributes
end
If you find a better solution, please let me know :-)

Resources