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

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

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]}"}

Type Error - No implicit conversion from nil to integer - Creating a array

I'm new to Ruby. I'm trying to learn the dos and don'ts by making little programs. This program is a little redundant, but I wanna better myself with the syntax of the language.
Anyways, so I'm trying to create a little program that will ask the user for an amount of people. This amount of people will then reference the size of the array, then the program will ask the user to enter the names for each element in the array (which ALL be "nil" values since the new array's elements will be empty). Finally, I would want to print back the array to the console to see the completed elements.
However, I'm getting an error saying "Line 10: TypeError occured. No implicit conversion from nil to integer". My code is not 100% done. I'm still trying to add more stuff to it, but I want to troubleshoot this error first before I go about doing that. Anyone out there willing to help me out ?
Here's the code
def Array_Maker
puts "How many people would you like to enter? : "
num = gets.chomp.to_i
nameArray = Array.new(num)
puts "\nEnter the names of the people you wish to add: "
nameArray.each do |x|
nameArray[x] = gets.chomp.to_s
end
nameArray.each do |x|
puts x
end
end
Array_Maker()
I'm probably doing this all wrong, but I'm trying...
The line nameArray.each do |x| iterates over the array and x is set to the value of the array at each index.
A better way might be to build the array using a map method. Something like this:
def array_maker
puts "How many people would you like to enter? : "
num = gets.chomp.to_i
puts "\nEnter the names of the people you wish to add: "
nameArray = num.times.map do
gets.chomp.to_s
end
nameArray.each do |x|
puts x
end
end
array_maker()
nameArray.each do |x| In this loop, x is given the values of the Array which is nil
Try this.
for i in 0..num do
nameArray[i] = gets.chomp.to_s
end

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.

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