Dynamically Create Arrays in Ruby - 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

Related

Combining data parsed from within the same hash in 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]}"}

Can't convert String onto integer (TypeError)

Following code return error "Can't convert String onto integer", please help
subject = ['eng','Math','Sci']
grade = ['grade 1','grade 2','grade 3']
subject.each do |sub|
puts ("some string")
grade[sub] .each do |grd|
puts ("some string")
end
end
grade[sub] .each do |grd| thats the problem.
Array elements are accessed by using a index of integer or a range of integers.
You are trying to access a array element by using the variable stored in sub. Since this is a ordinary .each loop it will loop all the elements in the array, in this case 'eng','Math','Sci'. If you want the position of for example 'eng' you could use a .each_with_index
it should probably just be
grade.each do |grd|
with each_with_index it would be
subject.each_with_index do |sub, index|
print sub
print grade[index]
end
If you want a subject -> grade collection it might be good to look into using a Hash like Dave Newton said.
{"eng" => "grade 1","Math" => "grade 2","Sci" => "grade 3"}.each do |subject, grade|
puts "#{subject| #{grade}"
end
When you do
grade[sub] .each do |grd|
Ruby expects sub to be using an integer to reference a position in the list of grades.
subject and grade are both arrays. They can only be accessed by their position. In your each loop, the block gets the actual element (in sub), not the position. So in line 5, you are trying to access grade['eng'], which will not work and produces the error. Note that unlike in e.g. PHP, an array and a hash (an associative array) are different things.
Guessing from your code, you might want to use each_index instead of each which will pass the index number to the block instead of the element.
I'm not sure I understand what you're trying to achieve; however, if you'd like to print subjects and grades and you're sure about the relative order of elements in the arrays, you could do this:
subject = ['eng','Math','Sci']
grade = ['grade 1','grade 2','grade 3']
subject.each_with_index do |sub, idx|
puts "#{sub} - #{grade[idx]}"
end
Output:
eng - grade 1
math - grade 2
sci - grade 3
An hash is however probably more suitable to your needs.

Populate an new array from another array of symbols

I have a question about Ruby arrays but it's kind of hard to describe so I could not find much from reading other questions. Here it is.
Currently I have the following code that works (part of Prawn's table)
Snippet A:
students = all_students.map do |student|
[
student[:first_name],
student[:last_name],
student[:email],
student[:given_name]
]
end
pdf.table students
This works fine, but now I'd like to omit some of the columns (e.g. don't show last_name).
Say I get an array of column names, let's say pickedColumns:
Snippet B:
pickedColumns = []
pickedColumns << :first_name << :email << :given_name #NOTE: no (:last_name) there!
students = all_students.map do |student|
studentCols = pickedColumns.each do |studentCol|
student[studentCol]
end
end
p.table students
I haven't been able to achieve the effect of snippet A, using the replaced code in snippet B. All I get in snippet B are not the actual values of "student[:first_name]" of just a string "first_name" for each row.
If my description is not 100% clear, please let me know.
Thanks for you help!
Regards
students = all_students.map do |student|
studentCols = pickedColumns.each do |studentCol|
student[studentCol]
end
end
Make that
students = all_students.map do |student|
pickedColumns.map do |studentCol|
student[studentCol]
end
end
and it will work.
PS: To adhere to ruby's naming convention you should change your variable names to use all lower case and underscores, rather than camelCase.

What's up with the way ruby alters variables this way?

Sorry about the vague question. I'm at a loss for words to describe this phenomenon, thus google wasn't much help. Please consider the following code:
array = [["name", "age"]]
a = []
x = ["Joe 32",
"Tom 45",
"Jim 36"]
x.each do |line|
name, age = line.split(/\s/)
a[0] = name
a[1] = age
array.push(a)
end
array.each do |x|
puts x.join(" ")
end
This produces:
name age
Jim 36
Jim 36
Jim 36
which is contrary to what I was expecting:
name age
Joe 32
Tom 45
Jim 36
Why is array affected after the fact by modifying a?
You want to set a to a new Array object inside the each. At the moment, you're pushing the same a object to the array, which is why it's returning the same value three times. Even better would be to not use a at all and instead convert the code into something like this:
x.each do |line|
name, age = line.split(/\s/)
array.push([name, age])
end
You could make it smaller than that even by moving the line.split to be within the push method, but I think that reduces readability and doesn't explain what information you're getting out of split.
This is slightly more advanced, but to build on Ryan's answer, rather than doing
x.each do |line|
name, age = line.split(/\s/)
array.push([name, age])
end
, you could use the map function, and have
people = x.map do |line|
name, age = line.split(/\s/)
[name, age]
end
desired_result = [["name", "age"]] + people
This is a slightly more "functional programming" approach. I'm sure this is a very rough summary, but in functional programming, you don't modify existing objects, you only create new objects instead.
As an aside, if you wanted to verify Ryan's answer, you could use object_id on each of the objects:
array.each_with_index do |object, index|
puts "Object #{index} (which is #{object.inspect}) has an object id of #{object.object_id}"
end
which gives
Object 0 (which is ["name", "age"]) has an object id of 10204144
Object 1 (which is ["Jim", "36"]) has an object id of 10248384
Object 2 (which is ["Jim", "36"]) has an object id of 10248384
Object 3 (which is ["Jim", "36"]) has an object id of 10248384

How do I remove unwanted from an array?

Ok so I have an array that looks like this.
["Enter Sandman", "One", "Nothing Else Matters", "Master of Puppets", "The Unforgiven", "The Day That Never Comes", "For Whom the Bell Tolls", "Fade to Black", "Sad But True", "Wherever I May Roam", "Turn the Page", "I Disappear", "Fuel", "Cyanide", "Seek & Destroy", "Whiskey In the Jar", "All Nightmare Long", "Battery", "Welcome Home (Sanitarium)", "The Unforgiven III", "The Unforgiven II", "King Nothing", "Ride the Lightning", "No Leaf Clover", "Until It Sleeps", "...And Justice for All", "Blackened", "The Memory Remains", "Hero of the Day", "The Four Horsemen", "Orion", "Creeping Death", "St. Anger", "Harvester of Sorrow", "Don't Tread on Me", "Broken, Beat & Scarred", "Disposable Heroes", "Fight Fire With Fire", "The End of the Line", "Trapped Under Ice", "Of Wolf and Man", "Whiplash", "My Apocalypse", "Suicide & Redemption", "The Shortest Straw", "Tuesday's Gone"]
This array is generated by this command
artists = search_object.map{|x| x["trackName"]}.uniq.delete_if {|x| x == nil}
this works well but I need to filter out some more elements. The user will type in the textfield and as they type i need to narrow the results. So for example if the user types the string "Matters" i need to take out the elements that dont have that in the name or like the name. So it narraws down to "Nothing Else Matters". If the user enters the letter "a" then all the others in the array that dont have an "a" get deleted.
they will come in with the params[:text]
I did this and it worked but maybe there is a cleaner way
query = params[:term]
artists = search_object.map{|x| x["trackName"]}.uniq.delete_if {|x| x == nil}
filtered = []
artists.each do |artist|
filtered << artist if artist.include?(query)
end
the fast ruby variant is:
albums = ["hello kitty", "bad day", "all is good", "day is okay"]
def filter_word_in(word,array)
array.delete_if { |data| !data.match(word) }
return array
end
result1 = filter_word_in("y", albums)
puts result1.inspect # => ["hello kitty", "bad day", "day is okay"]
result2 = filter_word_in("ay", result1)
puts result2.inspect # => ["bad day", "day is okay"]
result3 = filter_word_in("day", result2)
puts result3.inspect # => ["bad day", "day is okay"]
result4 = filter_word_in("day i",result3)
puts result4.inspect # => ["day is okay"]
How you can see in this code: we just save our result in variables.
So, where we can store our data in rails?
You can use user_model for this, or you can just store this data in memory.
Create something like this:
class UserSongMemory
attr_accessor :memory
def initialize
#memory = []
end
def push(id, data)
#memory << {id => data}
end
def pop(id)
#memory.delete_if {|obj| obj.id == id}
end
end
user_memory = UserSongMemory.new
user_memory.add(#user.id, params[:inputed_string])
# after our calculations
user.pop(#user.id)
I prefer to store state memory in Class, but don't forget to clean this class-data
I would do this:
term, fname = params[:term], "trackName"
filtered = search_object.map {|x| x[fname] if x[fname].match(term) }.compact.uniq
This approach eliminates the need for two loops, one for collection and other selection. The uniq and compact methods are there as per your requirement.
The Array class offers you the "reject" method to remove unwanted elements from your array.
Here's a slightly different approach, and my reasoning.
Reason:
I'm imagining a list on a webpage with a textbox that allows you to type a substring of your desired selection to filter down to that item. As such, my assumption is that your users will ONLY ever type a substring. These aren't power users who will be regex matching for their desired track. By just using select and include? you can limit that regex power, as people suggested, without the added complexity. Regex is a bit like using a machine gun to kill a fly in this example.
Code:
#I renamed the array, since it was a list of songs, not artists
songs.select {|s| s.upcase.include?(query.upcase)}
You could leave the upcase off if you want the query to be case sensitive

Resources