Ruby: Interpolate strings in hash creation - ruby

I'm currently trying to write a small script which parses weekly reports(e-mails) and stores the data I want in variables, so I can handle them further.
The functionality is working already, but it's sort of hacked in at the moment and I'd like to learn how the code should look idealy.
This is my code at the moment - I shortened it a bit and made it more generic:
activated_accounts_rx = Regexp.new(/example/)
canceled_accounts_rx = Regexp.new(/example/)
converted_accounts_rx = Regexp.new(/example/)
File.open("weekly_report.txt") do |f|
input = f.read
activated_accounts = input.scan(activated_accounts_rx).join
canceled_accounts = input.scan(canceled_accounts_rx).join
converted_accounts = input.scan(converted_accounts_rx).join
end
I thought of something like this and I know that it can't work, but I don't know how I can get it to work:
var_names = ["activated_accounts",
"canceled_accounts",
"converted_accounts"]
regex = { "#{var_names[0]}": Regexp.new(/example/),
"#{var_names[1]}": Regexp.new(/example/) }
File.open("weekly_report.txt") do |f|
input = f.read
for name in var_names
name = input.scan(regex[:#{name}]).join
end
end
I would like to end up with variables like this:
activated_accounts = 13
canceled_accounts = 21
converted_accounts = 5
Can someone help me please?

You probably don't need to have a separate array for your variables. Just use them as the keys in the hash. Then you can access the values later from another hash. Better yet, if you don't need the regexes anymore, you can just replace that value with the scanned contents.
regexHash = {
activated_accounts: Regexp.new(/example/),
canceled_accounts : Regexp.new(/example/),
converted_accounts: Regexp.new(/example/)
}
values = {}
contents = File.open("weekly_report.txt").read
regexHash.each do |var, regex|
values[var] = contents.scan(regex).join
end
To access your values later just use
values[:var_name] # values[:activated_accounts] for example

If you want separate name's variable in an array, you can use to_sym:
regex = { var_names[0].to_sym => Regexp.new(/example/),
var_names[1].to_sym => Regexp.new(/example/) } #Rocket notation!
And:
for name in var_names
name = input.scan(regex[var_names[0].to_sym]).join
end
Anyway, i prefer the Rob Wagner advice.

Related

What's the correct way to read an array from a csv field in Ruby?

I am trying to save some class objects to a csv file, everything works fine. I can save and read back from the csv file, there is only a 'minor' problem with an attribute that is an Array of Strings.
When I save it to the file it appears like this: "[""Dan Brown""]"
CSV.open('documents.csv', "w") do |csv|
csv << %w[ISBN Titre Auteurs Type Disponibilité]
#docs.each { |doc|
csv << [doc.isbn, doc.titre, doc.auteurs, doc.type, doc.empruntable ? "Disponible" : "Emprunté"]
}
end
And when I try to extract the data from the file I end up with something like this: ["[\"Dan Brown\"]"].
table = CSV.parse(File.read("documents.csv"), headers: true)
table.each do |row|
doc = Document.new(row['Titre'], row['ISBN'], row['Type'])
doc.auteurs << row['Auteurs'] #This the array where there is a 'problem'
if row['Disponibilité'] == "Disponible"
doc.empruntable = true
else
doc.empruntable = false
end
#docs.push(doc) #this an array where I save my objects
end
I tried many things to solve this but without any luck. I would be thankful if you can help me find a solution.
Since a CSV file, by it's nature, contains in its fields only strings, not arrays or other data types, the CSV class is applying the to_s method of the objects to turn them into a string before putting them into the CSV.
When you later read them back, you just get this - the string representation of what once had been your array. The only one who knows that 'Auteurs' should end up as an array of strings, is the application, i.e. you.
Hence on reading the CSV, after having extracted the autheurs string, you need to convert it manually back to an Array, because there is no automatic "inverse method" to reverse the to_s.
A cheap, but dangerous way to do it, is to use eval, which indeed would reconstruct your array. However, you need to be sure that nobody had a chance to fiddle manually with the CSV data, because an eval allows sneaking in arbitrary code.
A safer way would be to either write your own conversion function to and from String representation, or use a format such as YAML or JSON for representing the Array as String, instead of using to_s.

How to parse two elements from a list to make a new one

I have this input repeated in 1850 files:
[
{
"id"=>66939,
"login"=>"XXX",
"url"=>"https://website.com/XX/users/XXX"
},
...
{}
]
And I wanted to make a list in a way that by looking for the login I can retrieve the ID using a syntax like:
users_list[XXX]
This is my desired output:
{"XXX"=>"66570", "XXX"=>"66570", "XXX"=>"66570", "XXX"=>"66570", ... }
My code is:
i2 = 1
while i2 != users_list_raw.parsed.count
temp_user = users_list_raw.parsed[i2]
temp_user_login = temp_user['login']
temp_user_id = temp_user['id']
user = {
temp_user_login => temp_user_id
}
users_list << user
i2 += 1
end
My output is:
[{"XXX":66570},{"XXX":66569},{"XXX":66568},{"XXX":66567},{"XXX":66566}, ... {}]
but this is not what I want.
What's wrong with my code?
hash[key] = value to add an entry in a hash. So I guess in your case users_list[temp_user_login] = temp_user_id
But I'm unsure why you'd want to do that. I think you could look up the id of a user by having the login with a statement like:
login = XXX
user = users_list.select {|user| user["login"] == login}.first
id = user["id"]
and maybe put that in a function get_id(login) which takes the login as its parameter?
Also, you might want to look into databases if you're going to manipulate large amounts of data like this. ORMs (Object Relational Mappers) are available in Ruby such as Data Mapper and Active Record (which comes bundled with Rails), they allow you to "model" the data and create Ruby objects from data stored in a database, without writing SQL queries manually.
If your goal is to lookup users_list[XXX] then a Hash would work well. We can construct that quite simply:
users_list = users_list_raw.parsed.each.with_object({}) do |user, list|
list[user['login']] = user['id']
end
Any time you find yourself writing a while loop in Ruby, there might be a more idiomatic solution.
If you want to keep track of a mapping from keys to values, the best data structure is a hash. Be aware that assignment via the array operator will replace existing values in the hash.
login_to_id = {}
Dir.glob("*.txt") { |filename| # Use Dir.glob to find all files that you want to process
data = eval(File.read(filename)) # Your data seems to be Ruby encoded hash/arrays. Eval is unsafe, I hope you know what you are doing.
data.each { |hash|
login_to_id[hash["login"]] = hash["id"]
}
}
puts login_to_id["XXX"] # => 66939

rails string substitution or similar solution in controller

I'm building a site with users in all 50 states. We need to display information for each user that is specific to their situation, e.g., the number of events they completed in that state. Each state's view (a partial) displays state-specific information and, therefore, relies upon state-specific calculations in a state-specific model. We'd like to do something similar to this:
##{user.state} = #{user.state.capitalize}.new(current_user)
in the users_controller instead of
#illinois = Illinois.new(current_user) if (#user.state == 'illinois')
.... [and the remaining 49 states]
#wisconsin = Wisconsin.new(current_user) if (#user.state == 'wisconsin')
to trigger the Illinois.rb model and, in turn, drive the view defined in the users_controller by
def user_state_view
#user = current_user
#events = Event.all
#illinois = Illinois.new(current_user) if (#user.state == 'illinois')
end
I'm struggling to find a better way to do this / refactor it. Thanks!
I would avoid dynamically defining instance variables if you can help it. It can be done with instance_variable_set but it's unnecessary. There's no reason you need to define the variable as #illinois instead of just #user_state or something like that. Here is one way to do it.
First make a static list of states:
def states
%{wisconsin arkansas new_york etc}
end
then make a dictionary which maps those states to their classes:
def state_classes
states.reduce({}) do |memo, state|
memo[state] = state.camelize.constantize
memo
end
end
# = { 'illinois' => Illinois, 'wisconsin' => Wisconsin, 'new_york' => NewYork, etc }
It's important that you hard-code a list of state identifiers somewhere, because it's not a good practice to pass arbitrary values to contantize.
Then instantiating the correct class is a breeze:
#user_state = state_classes[#user.state].new(current_user)
there are definitely other ways to do this (for example, it could be added on the model layer instead)

Get param value dynamically

My question model holds the prompt and the answer choices for questions that students can answer. It includes columns named :choice_0, :choice_1, :choice_2, :choice_3, :choice_4, and :choice_5.
In one section of my controller, I've used the following code:
correct_array.push(these_params[:choice_0]) if !these_params[:choice_0].blank?
correct_array.push(these_params[:choice_1]) if !these_params[:choice_1].blank?
correct_array.push(these_params[:choice_2]) if !these_params[:choice_2].blank?
correct_array.push(these_params[:choice_3]) if !these_params[:choice_3].blank?
correct_array.push(these_params[:choice_4]) if !these_params[:choice_4].blank?
correct_array.push(these_params[:choice_5]) if !these_params[:choice_5].blank?
In other areas of my app, I've used the #{} syntax, for example:
params[:choice_#{n}]
But that doesn't work within a params hash for some reason. I'm sure that there is a drier way to accomplish these five lines.
Thank you in advance for any insight.
A more Ruby way to do this is:
correct_array = (0..5).map { |i| these_params["choice_#{i}".to_sym] }.select(&:present?)
Or as a method:
def correct_array
(0..5).map { |i| these_params["choice_#{i}".to_sym] }.select(&:present?)
end
In either case, you have the added bonus of not having to initialize correct_array as it is created on the fly.
You may try this
(0..5).each do |i|
param_i = these_params["choice_#{i}".to_sym]
correct_array.push(param_i) if param_i.present?
end

Parse a string with multiple XML-like tags using Ruby

I have a string which looks like the following:
string = " <SET-TOPIC>INITIATE</SET-TOPIC>
<SETPROFILE>
<PROFILE-KEY>predicates_live</PROFILE-KEY>
<PROFILE-VALUE>yes</PROFILE-VALUE>
</SETPROFILE>
<think>
<set><name>first_time_initiate</name>yes</set>
</think>
<SETPROFILE>
<PROFILE-KEY>first_time_initiate</PROFILE-KEY>
<PROFILE-VALUE>YES</PROFILE-VALUE>
</SETPROFILE>"
My objective is to be able to read out each top level that is in caps with the parse. I use a case statement to evaluate what is the top level key, such as <SETPROFILE> but there can be lots of different values, and then run a method that does different things with the contnts of the tag.
What this means is I need to be able to know very easily:
top_level_keys = ['SET-TOPIC', 'SET-PROFILE', 'SET-PROFILE']
when I pass in the key know the full value
parsed[0].value = {:PROFILE-KEY => predicates_live, :PROFILE-VALUE => yes}
parsed[0].key = ['SET-TOPIC']
I currently parse the whole string as follows:
doc = Nokogiri::XML::DocumentFragment.parse(string)
parsed = doc.search('*').each_with_object({}){ |n, h|
h[n.name] = n.text
}
As a result, I only parse and know of the second tag. The values from the first tag do not show up in the parsed variable.
I have control over what the tags are, if that helps.
But I need to be able to parse and know the contents of both tag as a result of the parse because I need to apply a method for each instance of the node.
Note: the string also contains just regular text, both before, in between, and after the XML-like tags.
It depends on what you are going to achieve. The problem is that you are overriding hash keys by new values. The easiest way to collect values is to store them in array:
parsed = doc.search('*').each_with_object({}) do |n, h|
# h[n.name] = n.text :: removed because it overrides values
(h[n.name] ||= []) << n.text
end

Resources