Handling a nil value from an instance method in Rails - ruby-on-rails

I have a Product model which has many Items. The application lists unique items which belong to a product. So think of items as inventory. The following query grabs featured items for a product and removes the first item (irrelevant, but it becomes a featured item, displayed separately, if you're curious).
# product.rb
has_many :items_in_stock, -> { Item.in_stock }, class_name: 'Item'
def featured_items
items_in_stock.select("DISTINCT ON (condition) id, items.*")
.order(:condition, :price)
.sort_by { |item| item[:price] }[1..-1]
end
# item.rb
scope :in_stock, -> { where(status: 'in_stock') }
The trouble is when the feaured_items are empty, the method returns nil, and not a relation object. This means I get an error if I call #product.featured_items.any? on a product that has no items. If I remove the sort_by block, I get an empty relation object.
Is there a good way to handle this other than:
items = items_in_stock.select("DISTINCT ON (condition) id, items.*").order(:condition, :price)
if items.any?
items.sort_by { |item| item[:price] }[1..-1]
end
I can't reverse the ordering of the query because I get an error saying the order of the conditions in the order by statement must match the group conditions.

I'm confused...why call .any? on it then since nil is treated as false in ruby. If what you get back is nil then you know that you don't have any featured_items.
I ran this in irb and I think your issue is the [1..-1].
a = []
# => []
a.sort_by { |w| w.length }
# => []
a.sort_by { |w| w.length }[1..-1]
# => nil
The easiest way is to just do
items = items_in_stock.select("DISTINCT ON (condition) id, items.*")
.order(:condition, :price)
.sort_by { |item| item[:price] }
items.any? ? items[1..-1] : items
Then you don't actually have to do a check in other parts of your code unless it's necessary.

instead of if items.any? you can use unless items.blank? if it's nil or empty, it won't run the condition
items.blank? checks both items.empty? and items.nil?
And of course you can use it in your featured_items
items = items_in_stock.select("DISTINCT ON (condition) id, items.*")
.order(:condition, :price)
.sort_by { |item| item[:price] }[1..-1]
return Array.new if items.blank?
That way you know that result will be an array, no matter what
And for the proof, you can use .blank? on a nil object, and it works on nil itself, nil.blank? returns true

Related

Return an array of items that satisfies a specific rule

I have an example class which acts as a rule like below and they will be more rules in the future. In this case it checks if an item is an insurance and if it is then the satisfied? method comes in play:
class ItemAvailabilityRule
def applicable?(item:)
item.name == Item::INSURANCE
end
def satisfied?(bookable:, item:)
applicable?(item) && bookable.duration_in_days < 365
end
end
I have another class which applies the rules to items as below:
class ItemsAvailabilityPolicy
def initialize(rules: [])
#rules = rules
end
def apply(bookable:, items:)
items.map do |item|
applicable_rules = rules.select { |rule| rule&.applicable?(item: item) && rule&.applicable?(bookable: bookable, item: item) }
applicable_rules.detect { |rule| !rule.satisfied?(bookable: bookable, item: item) }
end
end
end
My apply method is not quite right I think..
What I want to achieve through this apply method is that for an individual item using this:
applicable_rules = rules.select { |rule| rule.applicable?(item: item) } will give me all of the rules that apply to that item.
I then want to check if there is at least one unsatisfied rule
for which I did:
applicable_rules.detect { |rule| !rule.satisfied?(bookable: bookable, item: item) }
I then want to remove items from the array that do not satisfy all of the applicable rules and returns an array with only the ones that satisfied the rule.. How can I achieve that?
You almost had it:
class ItemsAvailabilityPolicy
def initialize(rules: [])
#rules = rules
end
def apply(bookable:, items:)
items.select do |item|
applicable_rules = rules.select { |rule| rule&.applicable?(item: item) && rule&.applicable?(bookable: bookable,
item: item) }
non_satisfactory = applicable_rules.detect { |rule| !rule.satisfied?(bookable: bookable, item: item) }
non_satisfactory.blank?
end
end
end
The changes are minimal, first change map to select so you are only selecting items that return true from your block. Enumerable#select needs a truthy or falsy return from each item in the block so we use blank?.
If I understand you right (as Stefan asked) you want to return all items satisfying all applicable rules ? And you have a "bookable" object that you want to test against all the items in a collection. This assumes that each rule will be applied in the same way (i.e. with the same variables etc)
You dont really need to use any local variables here since Array#select is what you need (for the items) and you can use select again for the applicable rules and chain that with Array#all? to ensure the item (which you are possibly selecting) passes all the (applicable) rules....
Here's the code (easier to understand than the explanation!)
class ItemsAvailabilityPolicy
def initialize(rules: [])
#rules = rules
end
def apply(bookable:, items:)
items.select do |item|
#rules.select { |rule| rule.applicable?(item: item) }.all? do |rule|
rule.satisfied?(bookable: bookable, item: item)
end
end
end
end
N.B. I used #rules because you used an instance var and no attr_reader or attr_accessor
Hope that helps

Sorting by integers represented as strings

I would like sort array of ActiveRecord objects by related object's attribute value. Meaning something like this:
Item has one product which has an attribute SKU. The SKU is mostly integer stored as a string, but could be alphanumeric as well.
sorted = items.sort_by { |item| Integer(item.product.sku) } rescue items
For now in case of error the items with original order returns.
What would I like to do?
Extend the Array class to achieve something like:
items.numeric_sort { |item| item.product.sku }
What I did so far?
1. Building a lambda expression and passing it
class Array
def numeric_sort(&lambda)
if lambda.respond_to? :call
self.sort_by(&lambda) rescue self
else
self.sort_by { |el| Integer(el) } rescue self
end
end
end
product_bin = lambda { |task_item| Integer(item.product.bin) }
items.numeric_sort(&product_bin)
2. Building lambda expression from methods chain
class Object
def send_chain(keys)
keys.inject(self, :send)
end
end
class Array
def numeric_sort_by(*args)
(args.length == 1) ? lam = lambda {|el| Integer(el.send(args))} : lam = lambda {|el| Integer(el.send_chain(args))}
self.sort_by(&lam) rescue self
end
end
items.numeric_sort_by(:product, :sku)
Is it all makes any sense?
Can you please point me in the right direction to implement the syntax I mentioned above, if it is possible at all.
Thanks.
EDIT: the sku could be alphanumeric as well. Sorry for the confusion.
Try this solution.
There is no error handling.
It's just an idea to develop if you like it.
class Array
def numeric_sort_by(*args)
self.sort_by do |element|
object = element
args.size.times { |n| object = object.send(args[n]) }
object.to_f
end
end
end
items.numeric_sort_by 'product', 'sku'
So the straightforward implementation was:
sorted = items.sort_by { |item| Integer(item.product.sku) } rescue items
And the desired was:
items.numeric_sort_by { |item| item.product.sku }
I was manage to achieve it by yielding a block into the sort_by:
class Array
def numeric_sort_by(&block)
return to_enum :numeric_sort_by unless block_given?
self.sort_by { |element| Integer(yield(element)) } rescue self
end
end

Clean up messy code that query's based on multiple options

I'm using Rails, but the underlying question here applies more broadly. I have a report page on my web app that allows the user to specify what they're filtering on, and query the database based on those filters (MongoDB).
The data is based around hotels, the user must first select the regions of the hotels (state_one, state_two, state_three), then the statuses of the hotels (planning, under_construction, operational), then an optional criteria, price range (200, 300, 400). Users can select multiple of each of these options.
My way of doing this currently is to create an empty array, iterate through each region, and push the region into the array if the user selected that region. Then, I'm iterating through THAT array, and assessing the status of the hotels in those regions, if any hotel has the status the user has selected, then I'm adding that hotel to a new empty array. Then I do the same thing for price range.
This works, but the code is offensively messy, here's an example of the code:
def find_hotel
hotels = find_all_hotels
first_array = []
hotels.each do |hotel|
if params[:options][:region].include? 'state_one' and hotel.state == :one
first_array.push(hotel)
elsif params[:options][:region].include? 'state_two' and hotel.state == :two
first_array.push(hotel)
elsif params[:options][:region].include? 'state_three' and hotel.state == :three
first_array.push(hotel)
end
end
second_array = []
first_array.each do |hotel|
if params[:options][:region].include? 'planning' and hotel.status == :planning
first_array.push(hotel)
elsif params[:options][:region].include? 'under_construction' and hotel.status == :under_construction
first_array.push(hotel)
elsif params[:options][:region].include? 'operational' and hotel.status == :operational
first_array.push(hotel)
end
end
third_array = []
second_array.each do |hotel|
# More of the same here, this could go on forever
end
end
What are some better ways of achieving this?
How about this:
STATES = [:one, :two, :three]
STATUSES = [:planning, :under_construction, :operational]
PRICES = [200, 300, 400]
def find_hotel
region = params[:options][:region]
first_array = set_array(region, find_all_hotels, STATES, :state)
second_array = set_array(region, first_array, STATUSES, :status)
third_array = set_array(region, second_array, PRICES, :price_range)
end
def set_array(region, array, options, attribute)
array.each_with_object([]) do |element, result|
options.each do |option|
result << element if region.include?(option) && element[attribute] == option
end
end
end
UPDATE
Added attribute parameter to set_array in order to make the code work with your updated example.
Since second_array is empty, whatever you get by iterating over it (perhaps third_array) would also be empty.
def find_hotel
hotels = find_all_hotels
first_array = hotels
.select{|hotel| params[:options][:region].include?("state_#{hotel.state}")}
first_array += first_array
.select{|hotel| params[:options][:region].include?(hotel.status.to_s)}
second_array = third_array = []
...
end

Rails: each for multiple and one object data

I'm new in rails and need to clear one question:
for example my method return such data:
#<Article ART_ID: 1151754, ART_ARTICLE_NR: "0 281 002 757", ART_SUP_ID: 30, ART_DES_ID: nil, ART_COMPLETE_DES_ID: 62395, ART_CTM: nil, ART_PACK_SELFSERVICE: 0, ART_MATERIAL_MARK: 0, ART_REPLACEMENT: 0, ART_ACCESSORY: 0, ART_BATCH_SIZE1: nil, ART_BATCH_SIZE2: nil, datetime_of_update: "2012-09-25 17:49:18">
or array, not only one object: how could use each func then?
for example:
articles = ArtLookup.search_strong_any_kind_without_brand(params[:article_nr].gsub(/[^0-9A-Za-z]/, ''))
binding.pry
if articles.present?
articles.each do |a|
#all_parts_result <<
{
analogue_manufacturer_name: a.supplier.SUP_BRAND,
analogue_code: a.ART_ARTICLE_NR,
delivery_time_min: '',
delivery_time_max: '',
min_quantity: '',
product_name: a.art_name,
quantity: '',
price: '',
distributor_id: '',
link_to_tecdoc: a.ART_ID
}
end
end
now i get errors like
`undefined method `each' for `#<Article:0x007f6554701640>
i think it is becouse i have sometimes one object, sometimes 10, and sometime 0.
how is it beatifull and right to do in rails?
Your search_strong_any_kind_without_brand method is looping through your articles based on the search condition. If the article matches then you are setting #art_concret to the match and then returning the match. However, you're not finding all matches, just the last one.
.
loop
#art_concret = art
end
.
return #art_concret
If you set the #art_concret as an array and inject results into this instance variable, then you will have the resulting search in array form. However, keep in mind that this does kind of break the ActiveRecord ORM as you would be returning a simple array and not an ActiveRecord Relation array.
def self.search_strong_any_kind_without_brand(search)
search_condition = search.upcase
#art_concret = []
#search = find(:all, :conditions => ['MATCH (ARL_SEARCH_NUMBER) AGAINST(? IN BOOLEAN MODE)', search_condition])
#articles = Article.find(:all, :conditions => ["ART_ID in (?)", #search.map(&:ARL_ART_ID)])
#binding.pry
#articles.each do |art|
if art.ART_ARTICLE_NR.gsub(/[^0-9A-Za-z]/, '') == search
#art_concret << art
end
end
return #art_concret
end
If you want to keep the code a bit cleaner then use select on your matching condition instead of looping through each article in #articles.
def self.search_strong_any_kind_without_brand(search)
search_condition = search.upcase
#search = find(:all, :conditions => ['MATCH (ARL_SEARCH_NUMBER) AGAINST(? IN BOOLEAN MODE)', search_condition])
#articles = Article.find(:all, :conditions => ["ART_ID in (?)", #search.map(&:ARL_ART_ID)])
#binding.pry
return #articles.select { |art| art.ART_ARTICLE_NR.gsub(/[^0-9A-Za-z]/, '') == search }
end
Unrelated: is there a reason why you're using instance variables in search_strong_any_kind_without_brand?
I think the right thing to do is to make sure your method always returns an array (or enumerable).
looking at the code you posted in to pastebin I would recommend you use Array#select in your method
for example you might be able to just return this:
#articles.select { |art| art.ART_ARTICLE_NR.gsub(/[^0-9A-Za-z]/, '') == search }
assuming #articles is an array or collection you will always get an array back, even if it is 0, or 1 element
This answer would be a bit offtopic, but I would like to mention a splat operator:
[*val]
will produce array, consisting of either single val value whether it’s not an array, or the array itself whether val is an array:
▶ def array_or_single param
▷ [*param].reduce &:+ # HERE WE GO
▷ end
=> :array_or_single
▶ array_or_single [1,2,3]
=> 6
▶ array_or_single 5
=> 5
That said, you code would work with this tiny improvement:
- articles.each do |a|
+ [*articles].each do |a|
Hope it gives a hint on how one might handle the data, coming from the 3rd party. As an answer to your particular question, please follow the advises in the other answers here.

Rails Search and Category Dropdown (No Method Error)

I have the following in my events controller:
def index
#event = Event.search(params[:search]).events_by_category(params[:cat]).order(...).paginate(...)
end
And in my events model, I have the following class method:
def self.events_by_category(cat)
if cat == 0
all
elsif cat && cat != 0
where('category = ?', cat)
else
scoped
end
end
And in my view I have a standard search box for the search and dropdown for the category selection. The category options_for_select has an array that includes ["All Categories", 0] in it.
My question is: Why does this return no results instead of all results when All Categories is selected in the dropdown. And, when I change the array to ["All Categories", "ALL"] and the if statement to if cat == "ALL" it returns Undefined method 'order'?
I think it has something to do with stringing the search and events_by_category together in the controller, but searches and categories work just fine in conjunction when it's not All Categories being selected...
Any help would be GREATLY appreciated!
To answer your second question first: the problem is that your cat == "ALL" condition returns Event.all, which is a Ruby Array, not an ActiveRecord::Relation. order is an activerecord method not an array method, that's why you're getting the error Undefined method 'order'.
If you want to return all results for the category ALL, then change all to scoped (which will return a scope with no conditions on it). That will be chainable, so that you can call it with order and paginate.
As to your first question, params[:cat] is a string so you should be checking whether cat == "0" not cat == 0. I think that should solve the problem.
Your conditional, by the way, is a bit convoluted: you're testing if cat is 0, then checking that it is not 0 in the else statement, but you already know that it is not 0. I'd suggest simplifying your method code to this:
def self.events_by_category(cat)
(cat && cat != "0") ? where('category = ?', cat) : scoped
end
This says: if the category is present and not "0" (i.e. not the category "all results"), then return results for that category, if not return all results.
def self.events_by_category(cat)
if cat == "0"
all
elsif cat && cat != "0"
where('category = ?', cat)
else
scoped
end
end
The above will work.

Resources