Check if certain value exists in array of hash map - ruby-on-rails

I have an array of hash map. It looks like this:
params = []
CSV.foreach(......) do
one_line_item = {}
one_line_item[:sku] = "Hello"
one_line_item[:value] = "20000"
params << one_line_item
end
I want to check if :sku is in this array of hash or not. I am doing it like this:
# Reading new line of csv in a hash and saving it in a temporary variable (Say Y)
params.each do |p|
if p[:sku] == Y[:sku]
next
end
end
I am iterating through the complete list for every value of sku, and thus time complexity is going for a toss [O(n^2)], need less to say it is useless.
Is there a way I can use include??
If I can get an array of values corresponding to the key :sku from the whole array in one shot, it would solve my problem. (I know I can maintain another array for these values but I want to avoid that)
One example of params
params = [{:sku=>"hello", :value=>"5000"}, {:sku=>"world", :value=>"6000"}, {:sku=>"Hi", :value=>"7000"}]

The any? and include? methods sound like what you need.
Example:
params.any? { |param| param.include?(:sku) }
This is an efficient way to do it, as it "short circuits", stopping as soon as a match is found.

So what you want is to collect a list of all SKUs. Are you looking to key sku => value?
Hash[*params.map { |p| [p[:sku], p[:value]] }.flatten]
That will give you a map of each sku to the value, which you can then do quick key lookups with sku_hash.key?(tester)

You may use rails_param gem for doing the same. I find it a very useful utility for validation request params in controller:
https://github.com/nicolasblanco/rails_param
# primitive datatype syntax
param! :integer_array, Array do |array,index|
array.param! index, Integer, required: true
end
# complex array
param! :books_array, Array, required: true do |b|
b.param! :title, String, blank: false
b.param! :author, Hash, required: true do |a|
a.param! :first_name, String
a.param! :last_name, String, required: true
end
b.param! :subjects, Array do |s,i|
s.param! i, String, blank: false
end
end

Related

Rails Strong Params how to Permit a nested Array

I have the following params:
params={"data"=>
{"type"=>"book",
"id"=>14,
"attributes"=>
{"id"=>14,
"created_at"=>"2022-06-27 21:15:39",
"title"=>"sdfdsf",
"targeting"=> { "release_times"=>[["4:00", "5:00"], ["5:00", "6:00"]],
"days"=>["Monday", "Tuesday", "Wednesday"],
"gender"=>["male", "female"]
}
}
}
When I use this, I can get every value but release_times is always null:
When I use this:
safe_params = params.require(:data).permit( attributes: [:id, :created_at, :title, { targeting: {} }])
How can I extract the release times value?
I tried doing this
safe_params = params.require(:data).permit( attributes: [:id, :created_at, :title, { targeting: [:days, :gender, release_times:[]]}])
But I get the error:
Validation failed: Targeting gender should be a list of values, Targeting days should be a list of values
How can I extract all the values from targeting including the release_times?
As Ruby on Rails API states, when using ActionController::Parameters you want to declare that a parameter should be an array (list) by mapping it to a empty array. Like you did with release_times.
You should permit targeting params with [days: [], gender: []] instead of [:days, :gender]. This should solve the error.
But even them, release_times is an array of arrays, which I believe is not supported at the moment (there is an old issue for it).
One way you could bypass this would be by changing the way you're communicating release_times. Using an arrays of hashes instead of nested arrays.
From this:
"release_times"=>[["4:00", "5:00"], ["5:00", "6:00"]]
To this (or something similar):
"release_times"=>[{"start" => "4:00", "end"=>"5:00"}, {"start" =>"5:00", "end" => "6:00"}]
That way, you could do this:
safe_params = params.require(:data).permit(attributes: [:id, :created_at, :title, { targeting: [days: [], gender: [], release_times: [:start, :end]] }])
Exactly how you would implement that is up to you, but I hope it helps.
**Also, there was a typo with release_times.
You can do some testing yourself. Open rails c and do something like this:
param = ActionController::Parameters.new("targeting"=> { "release_times"=>[["4:00", "5:00"], ["5:00", "6:00"]]})
param.require(:targeting).permit(release_times: []) # > Doesn't return times.
new_param = ActionController::Parameters.new("targeting"=> { "release_times"=>[{"start" => "4:00", "end"=>"5:00"}, {"start" =>"5:00", "end" => "6:00"}] })
new_param.require(:targeting).permit(release_times: [:start, :end]) # > Return times.
Just an observation, using permit! would work. But as strong params doc says:
Extreme care should be taken when using permit! as it will allow all
current and future model attributes to be mass-assigned.
So you could try to slice arguments yourself and them permit! - but I can't tell you that's the way to go.
Learn more about Mass Assignment Vulnerability here.

How to store string as array in database column using Ruby on Rails

This question is asked many times on SO. The main problem is nothing got fits into my situation.
Case is, I am not able to store typed content as array in database column.
text_field whose code is:
= text_field_tag 'product[keywords][]', #product.keywords, class: 'tab-input
product_keywords'
In controller strong parameters are:
params.require(:product).permit(:id, :name, :keywords => [])
Jquery code that is not not removing value upon deletion when typed wrong value but it add commas after each element as I want to take commas seperated value in one column.
$(document).on 'keyup', '.product_keywords', ->
keyword = #value.replace(/(\w)[\s,]+(\w?)/g, '$1, $2')
if keyword != #value
#value = keyword
return
model code:
serialize :keywords, Array
migration code:
class AddKeywordsToProducts < ActiveRecord::Migration[5.1]
def change
add_column :products, :keywords, :text
end
end
So, if someone writes, abc and hit space a comma is added in the end. after three typed words it will look like:
abc, dbx, she
now I want to store it as array in column but its not storing properly.
it stores as:
["abc, dbx, she"]
Also please can anybody tell me the best cases to handle these cases?
Plus best practices to deal with such cases using ruby so I will learn it for future?
You probably want a custom serializer as shown here. So instead of:
serialize :keywords, Array
You might do somewhat like:
serialize :keywords, KeywordSerializer
And somewhere in helpers:
class KeywordSerializer
def self.dump(what)
what.join(", ")
end
def self.load(what)
what.split(/\s*,\s*/)
end
end
Passing array elements using single form tag is not possible to pass as a array and passing array as a string, you need to process it near white-listing your params,
permitted_params = params.require(:product).permit(:id, :name, :keywords => [])
permitted_params[:keywords] = permitted_params[:keywords][0].split(/\s*,\s*/)

Is there a more efficient way of returning a hash value given array of keys if hash key/value is present?

I have a method which accepts a hash, and I have an array of keys (ordered by preference) which I want to check the hash for and return the value of the first matching key that's found which is not blank?. So far I have the following, but since I utilize this method heavily, I'm wondering if there is a more efficient way of going about it?
result = [:title, :name, :identifier, :slug].each do |key|
if my_hash[key].present?
return my_hash[key]
break
end
end
So given the following hash:
{
id: 10,
title: "",
name: "Foo",
slug: "foo"
}
I would expect result to be: "Foo"
Your way is probably close to the best from the point of view of efficiency. But if you want it to be more elegant, retaining the efficiency, then:
my_hash[%i[title name identifier slug].find{|key| my_hash[key].present?}]
You can do the following:
whitelisted_keys = [:title, :name, :identifier, :slug]
filtered_hash = your_hash.slice(*whitelisted_keys) # will only get pair matching your whitelisted_keys
# return all `.present?` key/values pairs:
filtered_hash.select{ |k,v| v.present? }
# return an array of the first key/value pair present:
filtered_hash.find{ |k,v| v.present? } # append .last to get the value
As Sawa pointed out in the comment, this is not the most efficient way but might be more "readable"

is there something like 'with_indifferent_access' for arrays usable for include?

I try to only use :symbols for key words in my app. I try to strict decide between :symbol => logic or string => UI/language specific
But I get some "values" (ie. options and so on) per JSON, too, since there are no :symbols in JSON, all my invoked hashes have the "with_indifferent_access" attribute.
but: is there something equal for array? like that
a=['std','elliptic', :cubic].with_indifferent_access
a.include? :std => true
?
edit: added rails to tags
a = ['std','elliptic', :cubic].map(&:to_sym)
a.include? :std
#=> true
Edit - regarding the comment by maxigs probably better to convert to strings:
a = ['std', 'elliptic', :cubic].map(&:to_s)
a.include? "std"
#=> true
tl;dr
['std','elliptic', :cubic].flat_map { |s| [s.to_sym, s.to_s] }
Many times with Rails, I'll have a validation of inclusion in array. I, too, like to work with symbols. It might look something like this:
validates :source, inclusion: { in: [:source1, :source2] }
And that might be valid when I first create the object with a symbol, but becomes invalid when it reads source from the db and returns a string. I could monkey-patch the getters to always return symbols, but I'd rather not. Instead, I do:
validates :source, inclusion: { in: [:source1, :source2].flat_map { |s| [s, s.to_s] } }

Sorting objects by field availability

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

Resources