Editing params nested hash - ruby-on-rails

Assume we have a rails params hash full of nested hashes and arrays. Is there a way to alter every string value (whether in nested hashes or arrays) which matches a certain criteria (e.g. regex) and still keep the output as a params hash (still containing nested hashes arrays?
I want to do some sort of string manipulation on some attributes before even assigning them to a model. Is there any better way to achieve this?
[UPDATE]
Let's say we want to select the strings that have an h in the beginning and replace it with a 'b'. so we have:
before:
{ a: "h343", b: { c: ["h2", "s21"] } }
after:
{ a: "b343", b: { c: ["b2", "s21"] } }
For some reasons I can't do this with model callbacks and stuff, so it should have be done before assigning to the respective attributes.

still keep the output as a params hash (still containing nested hashes arrays
Sure.
You'll have to manipulate the params hash, which is done in the controller.
Whilst I don't have lots of experience with this I just spent a bunch of time testing -- you can use a blend of the ActionController::Parameters class and then using gsub! -- like this:
#app/controllers/your_controller.rb
class YourController < ApplicationController
before_action :set_params, only: :create
def create
# Params are passed from the browser request
#model = Model.new params_hash
end
private
def params_hash
params.require(:x).permit(:y).each do |k,v|
v.gsub!(/[regex]/, 'string')
end
end
end
I tested this on one of our test apps, and it worked perfectly:
--
There are several important points.
Firstly, when you call a strong_params hash, params.permit creates a new hash out of the passed params. This means you can't just modify the passed params with params[:description] = etc. You have to do it to the permitted params.
Secondly, I could only get the .each block working with a bang-operator (gsub!), as this changes the value directly. I'd have to spend more time to work out how to do more elaborate changes.
--
Update
If you wanted to include nested hashes, you'd have to call another loop:
def params_hash
params.require(:x).permit(:y).each do |k,v|
if /_attributes/ ~= k
k.each do |deep_k, deep_v|
deep_v.gsub!(/[regex]/, 'string'
end
else
v.gsub!(/[regex]/, 'string')
end
end
end

In general you should not alter the original params hash. When you use strong parameters to whitelist the params you are actually creating a copy of the params - which can be modified if you really need to.
def whitelist_params
params.require(:foo).permit(:bar, :baz)
end
But if mapping the input to a model is too complex or you don't want to do it on the model layer you should consider using a service object.

Assuming you have a hash like this:
hash = { "hello" => { "hello" => "hello", "world" => { "hello" => "world", "world" => { "hello" => "world" } } }, "world" => "hello" }
Then add a function that transforms the "ello" part of all keys and values into "i" (meaning that "hello" and "yellow" will become "hi" and "yiw")
def transform_hash(hash, &block)
hash.inject({}){ |result, (key,value)|
value = value.is_a?(Hash) ? transform_hash(value, &block) : value.gsub(/ello/, 'i')
block.call(result, key.gsub(/ello/, 'i'), value)
result
}
end
Use the function like:
new_hash = transform_hash(hash) {|hash, key, value| hash[key] = value }
This will transform your hash and it's values regardless of the nesting level. However, the values should be strings (or another Hash) otherwise you'll get an error. to solve this problem just change the value.is_a?(Hash) conditional a bit.
NOTE that I strongly recommend you NOT to change the keys of the hash!

Related

Writing custom method for a hash on Ruby? on rails

i'm trying (and actually succeded, but i don't understand how it works) to write a custom method for a hash in my model (I'm working on Ruby on Rails 6).
My hash looks like this
my_hash = {
[['name_1', 'slug_1']=>value_1],
[['name_2', 'slug_2']=>value_2],
[['name_1', 'slug_1']=>value_3],
[['name_2', 'slug_2']=>value_4]
}
So basically a hash of arrays. You notice that the 'keys' are arrays that repeat themselves many times, but with different values. What i want to achieve is to write a custom method that "joins" all the keys in only one key, which will have an array of values assigned, so basically i should be able to get:
my_hash = {
['name_1', 'slug_1']=>"values": [value_1, value_3],
['name_2', 'slug_2']=>"values": [value_2, value_4]
}
For that, I have this piece of code, which i use many times:
my_hash.inject({}) do |hash, record|
# each record has the following format => [["unit_name", "axis.slug"]=>average_value(float)]
keys, value = record
# now keys has ["unit_name", "axis.slug"] and values equals average_value
hash[keys.first] ||= {}
hash[keys.first][keys.last] = value.to_f
hash
end
Since I use this many times, i wanted to write a custom method, so i did:
def format_hash_data my_hash
my_hash.inject({}) do |hash, record|
# each record has the following format => [["unit_name", "axis.slug"]=>average_value(float)]
keys, value = record
# now keys has ["unit_name", "axis.slug"] and values equals average_value
hash[keys.first] ||= {}
hash[keys.first][keys.last] = value.to_f
hash
end
end
And used it like: my_hash = format_hash_data(my_hash) with no success(it threw an error saying that 'format_hash_data' was not a valid method for the class).
So I fiddled around and added 'self' to the name of the method, leaving:
def self.format_hash_data my_hash
my_hash.inject({}) do |hash, record|
# each record has the following format => [["unit_name", "axis.slug"]=>average_value(float)]
keys, value = record
# now keys has ["unit_name", "axis.slug"] and values equals average_value
hash[keys.first] ||= {}
hash[keys.first][keys.last] = value.to_f
hash
end
end
Which, to my surprise, worked flawlessly when using my_hash = format_hash_data(my_hash)
I don't really understand why adding 'self' makes my code works, maybe anyone can shed some light? I tried using things like send() or instance_eval first, to just send the piece of code to the actual hash as a method (something like my_hash.instance_eval(my_method)) but I couldn't get it working.
I'm sorry about the long explanation, I hope i was clear enough so any of you who had this same dilemma can understand. Thanks in advance.
Prepending self. to the method name makes it a class method instead of an instance method. If you are not sure of the difference, you should look it up as it is fundamental to properly defining and using classes and methods.
As a class method, you would use it as:
my_hash = MyHash.format_hash_data(my_hash)
Or if you're in scope of the class, simply my_hash = format_hash_data(my_hash), which is why it worked in your case with the self. prepended (class method definition).
If you want to define it as an instance method (a method that is defined for the instance), you would use it like so:
my_hash = my_hash.format_hash_data
And the definition would use the implicit self of the instance:
def format_hash_data
self.inject({}) do |hash, record|
# each record has the following format => [["unit_name", "axis.slug"]=>average_value(float)]
keys, value = record
# now keys has ["unit_name", "axis.slug"] and values equals average_value
hash[keys.first] ||= {}
hash[keys.first][keys.last] = value.to_f
hash
end
end

How can i replace a parameter by looping parameters in ruby?

I have an array of parameters and i want to replace all parameters by looping over array in ruby on rails.
I am using before_action in controller as
before_action :cost_format
This is an array of parameters. The format of the parameter is string, i want to run a function on each parameter and convert it into float. So i am looping an array and calling function on each element.
def cost_format
x = [params[:cost_1], params[:cost_2], params[:cost_3]]
x.each do |i|
convert_cost(i)
end
end
I have a function convert_cost as following
def convert_cost(x)
x.gsub(',', '.').to_f
end
How do i loop an array so that my parameters get replaced.? Parameters are not being replaced by above code. Any hint would be appreciated.
Thanks
I think you'll want something like this:
def cost_format
%i(cost_1 cost_2 cost_3).each do |key|
params[key] = convert_cost(params[key])
end
end
def convert_cost(val)
val.gsub(',', '.').to_f
end
This grabs each key from your params and replaces the value with it passed through the convert_cost method.
Edit: you might be able to ignore this section about convert_cost due if it works with the format you're getting your numbers in. Please excuse my ethnocentrism :)
I've not updated your convert_cost method, though I'm a little wary about whether it will work at the moment. If you've got, for example "1,234,567" and you call your gsub you get "1.234.567". Calling to_f on that gives you 1.234, which I wouldn't think you'd want?
Instead of that, you could use:
def convert_cost(val)
val.gsub(',', '').to_f
end
E.G.
convert_cost("1,234,567")
# => 1234567.0
Combining all that, the following would be converted like so:
params = { cost_1: "1,234,567", cost_2: "123", cost_3: "456.5", cost_4: "I won't be touched" }
# after `cost_format` before_action runs:
# => {:cost_1=>1234567.0, :cost_2=>123.0, :cost_3=>456.5, :cost_4=>"I won't be touched"}
Let me know how you get on or if you have any question - hope this helps.
Could you try something like
x.map{|param| param.gsub(',', '.').to_f}
directly on your array.
So your cost_format method will look like:
def cost_format
x = [params[:cost_1], params[:cost_2], params[:cost_3]]
result = x.map{|param| param.gsub(',', '.').to_f}
# do stuff with result
end
I think the reason it's not working because, each method returns its receiver, but map returns a new array.

Building a hash or array in a helper method in rails 4

I am trying to build an array of hashes (I THINK that's the way I phrase it) with a helper method so that I can use it in my view. I am getting the 2 values from columns #other_events.time_start and #other_events.time_end.
helper.rb
def taken_times()
#taken_times = []
#other_events.each do |e|
#taken_times << { e.time_start.strftime("%l:%M %P") => e.time_end.strftime("%l:%M %P")}
end
#taken_times
end
What I am trying to have is an array of hashes like this:
['10:00am', '10:15am'],
['1:00pm', '2:15pm'],
['5:00pm', '5:15pm'],
which is essentially
['e.time_start', 'e.time_end'],
I think you should refactor your method to this:
def taken_times(other_events)
other_events.map { |event| [event.time_start, event.time_end] }
end
The helper method is not setting a global variable #taken_times anymore but you can easily call #taken_times = taken_times(other_events).
The helper method is using it's argument other_events and not on the global variable #other_events which could be nil in certain views.
The helper method returns an array of arrays, not an array of hashes. It is a two-dimensionnal array ("width" of 2, length of x where 0 ≤ x < +infinity).
The helper method returns an array of arrays containing DateTime objects, not String. You can easily manipulate the DateTime objects in order to format it in the way you want. "Why not directly transform the DateTime into nice-formatted strings?" you would ask, I would answer with "because you can do that in the view, at the last moment, and maybe someday you will want to do some calculation between the time_start and the time_end before rendering it.
Then in your view:
taken_times(#your_events).each do |taken_time|
"starts at: #{taken_time.first.strftime("%l:%M %P")}"
"ends at: #{taken_time.last.strftime("%l:%M %P")}"
end
You are asking for an array of hashes ([{}, {}, {}, ...]):
Array: []
Hash: {}
But you are expecting an array of array ([[], [], [] ...])
You should do something like this:
def taken_times()
#taken_times = []
#other_events.each do |e|
#taken_times << [e.time_start.strftime("%l:%M %P"), e.time_end.strftime("%l:%M %P")]
end
#taken_times
end

How to remove nested keys from a hash list in Rails

I am now trying for some hours to remove a nested hash key of a hash list.
I saw many solution non-nested hashs wich looks like this:
sample_hash = {"key1" => "value1", "key2" => "value2"}
sample_hash.except("key1")
This results in:
{"key2"=>"value2"}
But if I try to use the except method on a hash with nested key then it doesn't work.
Here my code:
nested_hash = {"key1"=>"value1", "key2"=>{
"nested_key1"=>"nestedvalue1",
"nested_key2"=>"nestedvalue2"
}
}
nested_hash.except("nested_key2")
The except() method returns the nested_hash without any changes. I have looked for a solution how I can pass nested hash-keys to the except method, but couldn't find anything. Is it even possible to pass nested keys to this method or should I use some other method which deletes a nested hash key from my hash list?
what about
Hash[nested_hash.map {|k,v| [k,(v.respond_to?(:except)?v.except("nested_key2"):v)] }]
=> {"key1"=>"value1", "key2"=>{"nested_key1"=>"nestedvalue1"}}
ugh.
The accepted solution is valid for the scenario given but if you're looking for something that will do this for arbitrarily nested hash tables then you're going to need a recursive solution. I couldn't find a suitable solution anywhere, so I wrote one here.
Reproduced here with annotations:
class Hash
def except_nested(key)
r = Marshal.load(Marshal.dump(self)) # deep copy the hashtable
r.except_nested!(key)
end
def except_nested!(key)
self.except!(key)
self.each do |_, v| # essentially dfs traversal calling except!
v.except_nested!(key) if v.is_a?(Hash)
end
end
end
adding it to the Hash class so that you can call it the same way you call except/except! anywhere else.
t = { a: '1', b: { c: '3', d: '4' } }
r = t.except_nested(:c)
# r => {:a=>"1", :b=>{:d=>"4"}}
# t => {:a=>"1", :b=>{:c=>"3", :d=>"4"}}
t.except_nested!(:c)
# t => {:a=>"1", :b=>{:d=>"4"}}
try
my_hash = Hash[nested_hash.map {|k,v| {k=>v.is_a? Array ? v.except("nested_key2") : v}}.map {|key, value| [key, value]}]
But this seems wrong, I wish I never started down this path, I'm willing to bet there is an easier way!
If you know that the nested key will always be there then you can just do
nested_hash['key2'].except!('nested_key2')
the whole nested_hash will now be lacking 'nested_key2'

How to assign an array of Hashes in a loop?

I'm attempting to convert MySQL timestamps in an ActiveRecord object to another timestamp format. My method takes an array of ActiveRecord records and returns an array of hashes with the timestamped fields with the formatted timestamp:
def convert_mysql_timestamps(records)
ary = []
hash = {}
records.each_with_index do |record, i|
record.attributes.each do |field, value|
if time_columns.include?(field) and value then
hash[field] = value.strftime("%Y-%m-%dT%H:%M:%S%z")
else
hash[field] = value
end
end
ary[i] = {}
ary[i] = hash
end
ary
end
However, when in the ary[i] = hash assignment, all ary elements get set to hash.
Is there a better way to convert a record's timestamp fields? (I don't need to save the records back to the database.) Also, how can I get the array to capture each individual hash representation of the record?
Input:
[#<Vehicle id: 15001, approved_at: "2011-03-28 10:16:31", entry_date: "2011-03-28 10:16:31">, #<Vehicle id: 15002, approved_at: "2011-03-28 10:16:31", entry_date: "2011-03-28 10:16:31">]
Desired output:
[{"id"=>15001, "approved_at"=>"2011-03-28T10:16:31-0700", "entry_date"=>"2011-03-28T10:16:31-0700"}, {"id"=>15002, "approved_at"=>"2011-03-28T10:16:31-0700", "entry_date"=>"2011-03-28T10:16:31-0700"}]
The problem is that you're creating one Hash:
def convert_mysql_timestamps(records)
ary = []
hash = {}
#...
and then trying to re-use for each record. You probably want a fresh Hash for each each_with_index iteration:
def convert_mysql_timestamps(records)
ary = []
records.each_with_index do |record, i|
hash = { }
record.attributes.each do |field, value|
#...
end
ary[i] = hash
end
end
You can use map for this - it's always a good option when you want to take an array in one format and produce a same-sized array in another. Here's how:
def convert_mysql_timestamps(records)
records.map do |record|
Hash[records.attributes.map{|k, v| [k, convert_mysql_timestamp(v)] }]
end
end
def convert_mysql_timestamp(field, value)
return value unless time_columns.include?(field) && value
value.strftime("%Y-%m-%dT%H:%M:%S%z")
end
It works like so:
Hash[array_of_pairs] turns an array of key-value pairs - like [["foo", 2], ["bar", 3], ...] - into a hash like {"foo" => 2, "bar" => 3, ...}.
map calls its block for each item in the collection, and collects each return value of the block into a new array, which it returns. The attributes.map inside the Hash[...] creates the array of key-value pairs, and the outer records.map collects up all the hashes into the returned array.
I'd suggest reading up on the methods in Enumerable because there are so many neat things like map in there. You will find that you almost never have to use indices in your loops, although if you're coming from another language with for loops everywhere it's a hard habit to break!
I am not sure what your time_columns are, but assuming they are Time class, you can simplify the part like value.is_a?(Time).
def convert_mysql_timestamps(records)
records.collect do |record|
# assuming records are from a Rails model, I am using #attributes
# to loop through all fields in a record
# then inject values in this hash -> ({}),
# which is in the block, named attributes
record.attributes.inject({}) do |attributes, (column_name, value)|
# if it is Time, convert it to iso8601 (slightly different from your format,
# but if this is also acceptable, your code can be simpler)
attributes[column_name] = (value.is_a?(Time) ? value.iso8601 : value)
attributes
end
end
end

Resources