Combining data parsed from within the same hash in Ruby - ruby

I'm trying to combine large data sets that I've filtered out from a single hash. I've tried various things such as merge, but don't seem to be able to get the data to combine the way I'm envisioning. Here are the things I'm trying to combine:
puts '','=========GET INFO'
print_data = targetprocess.comments_with_ids #get the hash
puts print_data #show the hash for verification
puts '','=========GET IDs'
story_ids = print_data['Comments']['Comment'].map {|entry| entry['General']} #filter for story ids and story name
puts story_ids
puts '','=========GET COMMENTS'
comment_description = print_data['Comments']['Comment'].map {|words| words['Description']} #get all comments, these are in the same order as the story ids
puts comment_description
Ultimately what I would like it to look like is:
story_id 1 + comment_description 1
story_id 2 + comment_description 2
etc.
Any help would be greatly appreciated.

I ended up realizing that the hash had some other nested structures I could use. In this example I use a nested hash, then store it as an array (I ultimately need this for other work) and then output.
puts '','=========GET INFO'
print_data = targetprocess.comments_with_ids #get the hash
puts print_data #show the hash for verification
puts '=========COMPLETE', ''
#=========HASH OF USEFUL DATA
results = {}
print_data['Comments']['Comment'].each{|entry|
results[entry['Id'].chomp] = {:parent_id => entry['General']['Id'].chomp, :description => entry['Description'].chomp}}
#=========STORE HASH AS AN ARRAY
csv_array = []
results.each{|key,value|
csv_array << [key, value[:parent_id], value[:description]]
#=======FRIENDLY OUTPUT
puts "Story_Id #{value[:parent_id]}, Comment_Id #{key}, Comment #{value[:description]}"}

Related

How do I get this block of ruby to add each individual hash into an array instead of just adding one hash multiple times?

#session is formatted as
[
['time','action','user'],
['time','action','user'],
...
]
and I'm trying to create an array that has those array elements but as hashes of {:time=>"time, :action=>"action", :user=>"user"}. The puts sessions line outputs each line as I desire, but when I try to capture those hashes into sessions_array I receive an array of only one hash repeated many times and not the unique hashes that puts is outputting.
sessions = Hash.new
sessions_array = Array.new
#session.each_with_index { |element, index|
next_element = #session[index+1]
sessions[:time] = element[0]
sessions[:action] = element[1]
sessions[:user] = element[2]
sessions_array << sessions
puts sessions
}
puts sessions_array
Create sessions inside of the each_with_index block instead of outside:
sessions_array = []
#session.each do |element|
sessions = {
time: element[0],
action: element[1],
user: element[2],
}
sessions_array << sessions
end
puts sessions_array
However, this can be done much more succinctly. When you're turning an array into another array with the same number of elements you almost always want to use map. Also, in a Ruby block you can extract the elements from an array by specifying multiple names in its arguments (|foo, bar, ...|).
This code is equivalent to the above:
sessions_array = #session.map do |time, action, user|
{ time: time, action: action, user: user }
end
You can see both of these snippets in action on repl.it here: https://repl.it/#jrunning/NavyImmaculateShockwave
Perhaps you are looking for something like the following.
Code
def hashify(data, keys)
data.map { |row| keys.zip(row).to_h }
end
Example
data = [
%w| 11:00 pummel Billy-Bob |,
%w| 02:00 maim Trixie |,
%w| 19:00 kill Bill |
]
#=> [["11:00", "pummel", "Billy-Bob"],
# ["02:00", "maim", "Trixie"],
# ["19:00", "kill", "Bill"]]
keys = [:time, :action, :user]
hashify(data, keys)
#=> [{:time=>"11:00", :action=>"pummel", :user=>"Billy-Bob"},
# {:time=>"02:00", :action=>"maim", :user=>"Trixie"},
# {:time=>"19:00", :action=>"kill", :user=>"Bill"}]
I have chosen to make data and keys arguments of the method so that those parameters can be modified without affecting the method itself.
Note that each of the three elements of:
data.map { |row| keys.zip(row) }
#=> [[[:time, "11:00"], [:action, "pummel"], [:user, "Billy-Bob"]],
# [[:time, "02:00"], [:action, "maim"], [:user, "Trixie"]],
# [[:time, "19:00"], [:action, "kill"], [:user, "Bill"]]]
is converted to a hash using the method Array#to_h. See also Array#zip.

How to "split and group" an array of objects based on one of their properties

Context and Code Examples
I have an Array with instances of a class called TimesheetEntry.
Here is the constructor for TimesheetEntry:
def initialize(parameters = {})
#date = parameters.fetch(:date)
#project_id = parameters.fetch(:project_id)
#article_id = parameters.fetch(:article_id)
#hours = parameters.fetch(:hours)
#comment = parameters.fetch(:comment)
end
I create an array of TimesheetEntry objects with data from a .csv file:
timesheet_entries = []
CSV.parse(source_file, csv_parse_options).each do |row|
timesheet_entries.push(TimesheetEntry.new(
:date => Date.parse(row['Date']),
:project_id => row['Project'].to_i,
:article_id => row['Article'].to_i,
:hours => row['Hours'].gsub(',', '.').to_f,
:comment => row['Comment'].to_s.empty? ? "N/A" : row['Comment']
))
end
I also have a Set of Hash containing two elements, created like this:
all_timesheets = Set.new []
timesheet_entries.each do |entry|
all_timesheets << { 'date' => entry.date, 'entries' => [] }
end
Now, I want to populate the Array inside of that Hash with TimesheetEntries.
Each Hash array must contain only TimesheetEntries of one specific date.
I have done that like this:
timesheet_entries.each do |entry|
all_timesheets.each do |timesheet|
if entry.date == timesheet['date']
timesheet['entries'].push entry
end
end
end
While this approach gets the job done, it's not very efficient (I'm fairly new to this).
Question
What would be a more efficient way of achieving the same end result? In essence, I want to "split" the Array of TimesheetEntry objects, "grouping" objects with the same date.
You can fix the performance problem by replacing the Set with a Hash, which is a dictionary-like data structure.
This means that your inner loop all_timesheets.each do |timesheet| ... if entry.date ... will simply be replaced by a more efficient hash lookup: all_timesheets[entry.date].
Also, there's no need to create the keys in advance and then populate the date groups. These can both be done in one go:
all_timesheets = {}
timesheet_entries.each do |entry|
all_timesheets[entry.date] ||= [] # create the key if it's not already there
all_timesheets[entry.date] << entry
end
A nice thing about hashes is that you can customize their behavior when a non-existing key is encountered. You can use the constructor that takes a block to specify what happens in this case. Let's tell our hash to automatically add new keys and initialize them with an empty array. This allows us to drop the all_timesheets[entry.date] ||= [] line from the above code:
all_timesheets = Hash.new { |hash, key| hash[key] = [] }
timesheet_entries.each do |entry|
all_timesheets[entry.date] << entry
end
There is, however, an even more concise way of achieving this grouping, using the Enumerable#group_by method:
all_timesheets = timesheet_entries.group_by { |e| e.date }
And, of course, there's a way to make this even more concise, using yet another trick:
all_timesheets = timesheet_entries.group_by(&:date)

Ruby-How to build a multivalued hash?

Here is my code snippet:
something_1.each do |i|
something_2.each do |j|
Data.each do |data|
date = data.attribute('TIME_PERIOD').text
value = data.attribute('OBS_VALUE').text
date_value_hash[date] = value
end
end
end
I want to capture all the values in a single date. date is the key of my hash and it may have multiple values for a single date. How can I accomplish that here? When I am using this line:
date_value_hash[date] = value
values are getting replaced each time the loop iterates. But, I want to accumulate all the values in my date_value_hash for each dates i.e. I want to build the values dynamically.
Currently I am getting this:
{"1990"=>"1", "1994"=>"2", "1998"=>"0"}
But, I want something like this:
{"1990"=>"1,2,3,4,5,6", "1994"=>"1,2,3,4,5,6", "1998"=>"1,2,3,4,5,6"}
Anyone have any idea how can I accomplish that?
Like this
magic = Hash.new{|h,k|h[k]=[]}
magic["1990"] << "A"
magic["1990"] << "B"
magic["1994"] << "C"
magic["1998"] << "D"
magic["1994"] << "F"
after which magic is
{"1998"=>["D"], "1994"=>["C", "F"], "1990"=>["A", "B"]}
and if you need the values as comma separated string (as indicated by your sample data), you'll just access them as
magic['1990'].join(',')
which yields
"A,B"
if later you want to pass magic around and preventing it from automagically creating keys, just wrap it as follows
hash = Hash.new.update(magic)
Hope that helps!
Another approach of building multi-valued hash in Ruby:
h = {}
(h[:key] ||= []) << "value 1"
(h[:key] ||= []) << "value 2"
puts h

string to object?

Consider the following code:
I receive an object from datamapper which contains Values from my select:
user = User.first()
puts user.name
# John
puts user.surname
# Doe
puts user.age
# 42
In a user defined Array I have an Order for these Values to be displayed
dataordering = ["age", "surname", "name"]
So how do I get my values ordered as in my Array?
dataordering.each do |sequence|
puts user.sequence
# this, of course, fails
end
I don't want to use eval(). nope.
Maybe there's even a better way to store an ordering of values?
You can pick values from record this way:
user_attributes = user.attributes
dataordering.each do |attribute|
puts user_attributes[attribute.to_sym]
end
Or use send method:
dataordering.each do |attribute|
puts user.send attribute.to_sym
end
As an ordering solution, I can offer you this code:
dataordering.map { |attr| user.send attribute.to_sym }

Dynamically Create Arrays in Ruby

Is there a way to dynamically create arrays in Ruby? For example, let's say I wanted to loop through an array of books as input by a user:
books = gets.chomp
The user inputs:
"The Great Gatsby, Crime and Punishment, Dracula, Fahrenheit 451,
Pride and Prejudice, Sense and Sensibility, Slaughterhouse-Five,
The Adventures of Huckleberry Finn"
I turn this into an array:
books_array = books.split(", ")
Now, for each book the user input, I'd like to Ruby to create an array. Pseudo-code to do that:
x = 0
books_array.count.times do
x += 1
puts "Please input weekly sales of #{books_array[x]} separated by a comma."
weekly_sales = gets.chomp.split(",")
end
Obviously this doesn't work. It would just re-define weekly_sales over and over again. Is there a way to achieve what I'm after, and with each loop of the .times method create a new array?
weekly_sales = {}
puts 'Please enter a list of books'
book_list = gets.chomp
books = book_list.split(',')
books.each do |book|
puts "Please input weekly sales of #{book} separated by a comma."
weekly_sales[book] = gets.chomp.split(',')
end
In ruby, there is a concept of a hash, which is a key/value pair. In this case, weekly_sales is the hash, we are using the book name as the key, and the array as the value.
A small change I made to your code is instead of doing books.count.times to define the loop and then dereference array elements with the counter, each is a much nicer way to iterate through a collection.
The "push" command will append items to the end of an array.
Ruby Docs->Array->push
result = "The Great Gatsby, Crime and Punishment, Dracula, Fahrenheit 451,
Pride and Prejudice, Sense and Sensibility, Slaughterhouse-Five,
The Adventures of Huckleberry Finn".split(/,\s*/).map do |b|
puts "Please input weekly sales of #{b} separated by a comma."
gets.chomp.split(',') # .map { |e| e.to_i }
end
p result
Remove the comment if you would like the input strings converted to numbers
One way or another you need a more powerful data structure.
Your post gravitates toward the idea that weekly_sales would be an array paralleling the books array. The drawback of this approach is that you have to maintain the parallelism of these two arrays yourself.
A somewhat better solution is to use the book title as a key to hash of arrays, as several answers have suggested. For example: weekly_sales['Fahrenheit 451'] would hold an array of sales data for that book. This approach hinges on the uniqueness of the book titles and has other drawbacks.
A more robust approach, which you might want to consider, is to bundle together each book's info into one package.
At the simplest end of the spectrum would be a list of hashes. Each book would be a self-contained unit along these lines:
books = [
{
'title' => 'Fahrenheit 451',
'sales' => [1,2,3],
},
{
'title' => 'Slaughterhouse-Five',
'sales' => [123,456],
},
]
puts books[1]['title']
At the other end of the spectrum would be to create a proper Book class.
And an intermediate approach would be to use a Struct (or an OpenStruct), which occupies a middle ground between hashes and full-blown objects. For example:
# Define the attributes that a Book will have.
Book = Struct.new(:title, :weekly_sales)
books = []
# Simulate some user input.
books_raw_input = "Fahrenheit 451,Slaughterhouse-Five\n"
sales_raw_input = ['1,2,3', '44,55,66,77']
books_raw_input.chomp.split(',').each do |t|
ws = sales_raw_input.shift.split(",")
# Create a new Book.
books.push Book.new(t, ws)
end
# Now each book is a handy bundle of information.
books.each do |b|
puts b.title
puts b.weekly_sales.join(', ')
end
Are you happy to end up with an array of arrays? In which this might be useful:
book_sales = books_array.collect do |book|
puts "Please input weekly sales of #{books_array[0]} separated by a comma."
gets.chomp.split(",").collect{ |s| s.to_i }
end
Looking at it, you might prefer a hash, keyed by book. Something like this:
book_sales = books_array.inject({}) do |hash, book|
puts "Please input weekly sales of #{books_array[0]} separated by a comma."
weekly_sales = gets.chomp.split(",").collect{ |s| s.to_i }
hash[book] = weekly_sales
end
This solution assumes that there will never be a duplicate book title. I figure that is pretty safe, yes?
input = "A list of words"
hash = {}
input.split(/\s+/).collect { |word| hash[word] = [] }
# Now do whatever with each entry
hash.each do |word,ary|
ary << ...
end

Resources