Why is "box1" undefined after calling "box#{i} = ..."? - ruby

I'm trying to create a tic-tac-toe game. I listed only the code for the relevant class and methods I got so far:
class Box
attr_reader :name, :row, :column
attr_accessor :is_marked, :contents
def initialize(name, row, column, is_marked=false, contents)
#name = name
#row = row
#column = column
#is_marked = is_marked
#contents = contents
end
def self.display_box
print '|#{contents}|'
end
end
#generate box instances
(1..9).each do |i|
if i > 3
col = i % 3
else
col = i
end
box#{i} = Box.new('box#{i}', (i/3).ceil, col, false, '_')
end
board = [[box1, box2, box3], [box4, box5, box6], [box7, box8, box9]]
def display_board
box1.display_box; box2.display_box; box3.display_box; print '\n'
box4.display_box; box5.display_box; box6.display_box; print '\n'
box7.display_box; box8.display_box; box9.display_box; print '\n'
end
display_board
I can't figure out why creating an instance of my class throws an error. The error is:
undefined local variable or method `box1' for <Context:0x000000024df8a8>
(repl):44:in display_board'
(repl):61:in initialize'
I tried running it with and without the 'self' in the display_box method, same error.

The problem is this line.
box#{i} = Box.new('box#{i}', (i/3).ceil, col, false, '_')
It seems like you're trying to create a bunch of variables named box1, box2, ... There might be away way to do that in ruby, but that's not it. You could do it with eval, but you don't want to this. As you can see from the rest of your code, working with a big pile of variables like that is annoying.
Instead, make an Array of boxes.
boxes[i] = Box.new("box#{i}", (i/3).ceil, col, false, '_')
You need to declare boxes = [] before the loop.
Then create your board from that list using ranges.
board = [boxes[1..3], boxes[4..6], boxes[7..9]]
And display_board becomes less repetitive.
def display_board(board)
board.each { |row|
row.each { |box|
box.display_box
}
puts "\n"
}
end
Finally, if you want to interpolate variables in strings you have to use ". For example, in display_box.
def display_box
print "|#{contents}|"
end

This is an error:
box#{i}
You can interpolate into Strings(and regexes), but a variable name is not a
String. A String has quotes around it.
Even if you could interpolate into variable names, you would never do this:
box#{i} = Box.new('box#{i}', (i/3).ceil, col, false, '_')
Instead, you would create an Array and add instances to the Array:
boxes = []
(1..9).each do |i|
if i > 3
col = i % 3
else
col = i
end
boxes << Box.new('box#{i}', (i/3).ceil, col, false, '_')
end
Then the names of your instances are boxes[0], boxes[1], etc.
You also need to know that a def creates a new scope and variables outside the def cannot be seen inside the def. So any box1, box2, etc. variables created outside the def cannot be seen inside the def. What you need to do is pass the boxes Array to the display_board() method, like this:
def display_board(board_boxes)
...
end
display_board(boxes)
Ruby then lines up your method call and the method header like this:
display_board(boxes)
|
V
def display_board(board_boxes)
and the boxes Array gets assigned to a variable called board_boxes:
board_boxes = boxes
Then inside the def, board_boxes will be the Array containing the boxes.
Don't combine lines of code on one line using semi colons. When you use an Array to store your box instances, you can display your boxes like this:
board_boxes.each do |box|
box.display_box
end
If you want to print a newline after every three boxes, you can do this:
count = 1
board_boxes.each do |box|
box.display_box
print "\n" if count%3 == 0
count += 1
end

You get undefined local variable or method `box1' because box1 is not defined.
Apparently, you expected this line to create box1:
box#{i} = ...
but it is really just this:
box
because # starts a comment.
Double-quotes strings allow interpolation, but you can't interpolate variable names like this.
How to solve it?
It is almost always a bad idea to create variables dynamically. If you have a large or a varying number of values, you should store them in a collection - either an Array, a Hash, or a custom one.
The box object
Setting up the boxes is quite complex, because you are passing name, row and column to each box. Does a box really have to know its position within the board? Think about other objects that are part of a larger structure: an array element doesn't know its index (the array does), a character doesn't know its position (the string does) and a hash value doesn't know its key (the hash does).
Let's remove these extra attributes from Box and have it just store its content:
class Box
attr_accessor :content
def initialize
#content = '_'
end
def marked?
content != '_'
end
end
b = Box.new #=> #<Box:0x007fab7a9f58a8 #content="_">
b.content #=> "_"
b.marked? #=> false
b.content = 'X'
b.marked? #=> true
The board object
To create a 3×3 array of boxes, we can write: (I've removed the object ids from the output)
board = Array.new(3) { Array.new(3) { Box.new } }
#=> [[#<Box #content="_">, #<Box #content="_">, #<Box #content="_">],
# [#<Box #content="_">, #<Box #content="_">, #<Box #content="_">],
# [#<Box #content="_">, #<Box #content="_">, #<Box #content="_">]]>
Single boxes can be accessed via: (indices are zero-based)
board[0][0] #=> #<Box #content="_">
But since board is such an important object, I'd write a custom class:
class Board
def initialize
#boxes = Array.new(3) { Array.new(3) { Box.new } }
end
def box(x, y)
#boxes[x][y]
end
end
The class does not expose #boxes, because we don't want the array to be changed. It provides a method Board#box instead that returns the box for the given x and y coordinates.
Formatting
In my opinion neither Box nor Board should contain code that handles formatting, so let's write a small utility class to handle this part:
class BoardFormatter
def draw(board)
template.gsub(/[0-8]/) do |n|
x, y = n.to_i.divmod 3
board.box(x, y).content
end
end
def template
<<-STR
0 | 1 | 2
---+---+---
3 | 4 | 5
---+---+---
6 | 7 | 8
STR
end
end
BoardFormatter#template returns the string for the board. You might not have seen <<- before: it's called a heredoc and creates a multiline string between the given STR delimiters:
formatter = BoardFormatter.new
formatter.template #=> " 0 | 1 | 2\n---+---+---\n 3 | 4 | 5\n---+---+---\n 6 | 7 | 8\n"
BoardFormatter#draw uses String#gsub to replace each digit ([0-8]) with the respective box' content.
divmod is the key here, it returns both, a / b and a % b as an array. And because we're using zero-based indices, these are just the box coordinates for the nth box:
0.divmod(3) #=> [0, 0]
1.divmod(3) #=> [0, 1]
2.divmod(3) #=> [0, 2]
3.divmod(3) #=> [1, 0]
4.divmod(3) #=> [1, 1]
5.divmod(3) #=> [1, 2]
6.divmod(3) #=> [2, 0]
7.divmod(3) #=> [2, 1]
8.divmod(3) #=> [2, 2]
Complete example:
board = Board.new
board.box(0, 2).content = 'X'
board.box(0, 0).content = 'O'
board.box(2, 0).content = 'X'
board.box(1, 1).content = 'O'
formatter = BoardFormatter.new
puts formatter.draw(board)
Output:
O | _ | X
---+---+---
_ | O | _
---+---+---
X | _ | _
One-based indices
The players should probably enter one-based indices when playing your game. This conversion should be handled when processing the user input. Within your core code, you should always use zero-based indices, just like Ruby.

As other users mentioned you can create an array to hold the instances of Box.
If you are dead set on using variables you can use instance_variable_set although using an array is probably better. See here for more details.
With instance_variable_set:
class Box
attr_reader :name, :row, :column
attr_accessor :is_marked, :contents
def initialize(name, row, column, is_marked=false, contents)
#name = name
#row = row
#column = column
#is_marked = is_marked
#contents = contents
end
def display_box
print '|#{contents}|'
end
end
#generate box instances
(1..9).each do |i|
if i > 3
col = i % 3
else
col = i
end
instance_variable_set("#box#{i}", Box.new('box#{i}', (i/3).ceil, col, false, '_'))
end
board = [[#box1, #box2, #box3], [#box4, #box5, #box6], [#box7, #box8, #box9]]
def display_board
#box1.display_box; #box2.display_box; #box3.display_box; print '\n'
#box4.display_box; #box5.display_box; #box6.display_box; print '\n'
#box7.display_box; #box8.display_box; #box9.display_box; print '\n'
end
display_board
With an array:
class Box
attr_reader :name, :row, :column
attr_accessor :is_marked, :contents
def initialize(name, row, column, is_marked=false, contents)
#name = name
#row = row
#column = column
#is_marked = is_marked
#contents = contents
end
def display_box
print '|#{contents}|'
end
end
#generate box instances
arr = [];
(1..9).each do |i|
if i > 3
col = i % 3
else
col = i
end
name = 'box' + i.to_s
arr.push(Box.new(name, (i/3).ceil, col, false, '_'))
end
board = [[arr[0], arr[1], arr[2]], [arr[3], arr[4], arr[5]], [arr[6], arr[7], arr[8]]]
def display_board(arr)
arr[0].display_box; arr[1].display_box; arr[2].display_box; print '\n'
arr[3].display_box; arr[4].display_box; arr[5].display_box; print '\n'
arr[6].display_box; arr[7].display_box; arr[8].display_box; print '\n'
end
display_board(arr)

Related

Passing nill value to custom method

Can you please tell me why it is passing nil to check method? I am getting error main.rb:5:in `check': undefined method `%' for nil:NilClass (NoMethodError)
my_array = Array.new
$output = String.new
def check(n)
if n%3 == 0
$output = $output + 'Pop '
elsif n.even?
$output = $output + 'Crackle '
elsif n.odd?
$output = $output + 'Snap '
end
end
for x in 1..6
my_array[x] = gets.chomp.to_i
end
my_array.each { |x| check(x) }
puts $output.my_array
The reason you are getting a nil in the beginning of the array is that you are manually setting the keys in the array which creates a hole since arrays are 0 indexed in Ruby:
ary = Array.new
ary[1] = "a"
ary[2] = "b"
ary[3] = "c"
# => [nil, "a", "b", "c"]
While you could salvage this code with:
my_array = Array.new
$output = String.new
def check(n)
if n%3 == 0
$output = $output + 'Pop '
elsif n.even?
$output = $output + 'Crackle '
elsif n.odd?
$output = $output + 'Snap '
end
end
for x in 0..5
my_array[x] = gets.chomp.to_i
end
my_array.each { |x| check(x) }
puts $output.my_array
A more idiomatically correct way to write this in Ruby is:
str = 5.times.map do
n = gets.chomp.to_i
if n%3 == 0
'Pop'
elsif n.even?
'Crackle'
elsif n.odd?
'Snap'
end
end.join(" ")
puts str
for String.new and Array.new are rarely used if ever used. Use blocks instead of methods unless you're planning to reuse it later. In Ruby you can use the methods from Enumerable to both iterate over and transform arrays, hashes, ranges and other types of objects so there rarely is a reason to iterate and modify an external variable like in other languages.
With for x in 0..5 you would then have
t.rb:21:in `<main>': undefined method `my_array' for "":String (NoMethodError)
because my_array is not a method that you can send to $output.
There are many ways to do the same thing in Ruby.
my_array = []
def check(n)
case
when n % 3 == 0
'Pop'
when n.even?
'Crackle'
when n.odd?
'Snap'
else 'boom !' # not necessary in this particular case
end
end
(1..6).each do | i |
print "#{i} Enter a number > "
my_array << gets.to_i
end
puts my_array.collect { |e| check(e) }.join(' ')
Execution :
$ ruby t.rb
1 Enter a number > 44
2 Enter a number > 66
3 Enter a number > 30494
4 Enter a number > 383849
5 Enter a number > 2234
6 Enter a number > 4333
Crackle Pop Crackle Snap Crackle Snap
Don't use global variables, like $output. In the ancient (imperative programming style) languages, it was a common bug to inadvertently modify a variable accessible from anywhere.
The object oriented paradigm has been invented to isolate variables (encapsulated in an
object) to make it more difficult to modify them accidentally.
You could have use an instance variable :
#output = ''
if n%3 == 0
#output << 'Pop '
but beeing defined in the special 'main' object, it is not protected against unwanted access.
chomp is not necessary before to_i, see this post
Use iterators instead of loops. for is imperative style (C, Java), which imposes you to manage
the begin and end indexes. In an object oriented language, you simply send an iterate message to a
collection object, which takes cares of the internal details.
if and case are expressions which return the last computed value. check() returns that value.
Your my_array.each { |x| check(x) } mutates the variable $output and returns no result. In a big program, a later maintenance could insert some processing that modifies $output before you use it (bug).
The functional programming paradigm (Scala, Elixir, Kotlin) tends to use immutable variables and use functions to transform data.
The new my_array.collect { |e| check(e) }.join(' ') iterates over my_array, transforms each element calling the function check(), produces a new (immutable) collection with these transformed elements, which is then transformed by the function join() to produce the final result.
You have
for x in 1..6
my_array[x] = gets.chomp.to_i
end
Which populates the array from indexes 1 through 6, all arrays begin at index 0 so, in your method
my_array.each { |x| check(x) }
The .each method will iterate through each element of the array, starting at 0, which in this case would be nil because you never assigned a value to that index, you could change your range to
for x in 0..6
my_array[x] = gets.chomp.to_i
end
And that would work, remember to use 2 dots and not 3, as
0..6
0...6
are different, the first one is inclusive, the second one is exclusive.
You can check up more about ranges here

Trying to cull a set but the set keeps disappearing

I'm trying to program the AI for a Mastermind game in ruby using Donal Knuth's 5 guess algorithm. The game consists of a codemaker, who uses 8 different colored pegs to create a set of 4, and a codebreaker, who guesses at the code and receives feedback (a red square for a peg which is both the right color and in the right spot, and a white square for a peg which is the right color but in the wrong spot).
I've created a set for all possible codes. My goal is to compare feedback from the guess to feedback from all codes in the set, then delete the ones that don't match. It seems to delete the entire set though.
class ComputerPlayer < Player
def initialize(game)
super(game)
#all_possible_codes = create_codes
#turn = 1
end
def get_code
Array.new(4){rand(1..6)}
end
def get_guess
puts #all_possible_codes.length
if #turn == 0
#turn += 1
cull_set([1, 1, 2, 2])
#all_possible_codes.delete("1122")
return [1, 1, 2, 2]
else
random_sample = #all_possible_codes.to_a.sample.split('').map{|str| str.to_i}
#all_possible_codes.delete(random_sample.join(''))
cull_set(random_sample)
random_sample
end
end
def cull_set(guess)
feedback = #game.feedback_on_guess(guess)
puts feedback
#all_possible_codes.delete_if { |str| #game.feedback_on_guess(str.split.map{|num| num.to_i}) != feedback }
end
def create_codes
set = Set.new
(1..8).each do |i|
(1..8).each do |j|
(1..8).each do |k|
(1..8).each do |l|
set << [i, j, k, l].join('')
end
end
end
end
set
end
end
#this is the feedback_on_guess method used by the above class
def feedback_on_guess(code_guess)
code_duplicate = #code
feedback = []
code_duplicate.map.with_index do |entry, i|
if entry == code_guess[i]
feedback.push('r')
code_guess[i] = -1
-2
else
entry
end
end.each do |entry|
found_index = code_guess.find_index(entry)
if found_index
feedback.push('g')
code_guess[found_index] = -1
end
end
puts feedback
feedback
end
Try
copy = something.dup
because after just
copy = something
copy and something are pointing to the same object. You can confirm this by checking the object_id of the object referenced by the variable. If it is the same, then it is the same object.
When you dup an object, you will cretae a copy. Depending on what you want to dup you might need to implement/override the logic to create a copy. For built in Classes like String, Hash and so on it will work out of the box.
Be aware that nested constructs (eq. Hash containing other Hashes) are not duplicated.
h1 = {"a" => {"b" => 2}}
h2 = h1.dup
puts h1.object_id # 70199597610060
puts h2.object_id # 70199597627020
puts h1["a"].object_id # 70199597610080
puts h2["a"].object_id # 70199597610080

Ruby nested for loop to populate array of arrays

I'm trying to teach myself Ruby via coding Conway's Game of Life.
One of my preliminary steps to learn how arrays work is to create an array of arrays of Cell objects defined as follows:
class Cell
def initialize(status, xpos, ypos)
#status = status
#position = [xpos,ypos]
end
end
contents = Array.new(10, Array.new(10))
for i in 0..contents.length-1
for j in 0..9
contents.at(i).insert(j, Cell.new("DEAD", i, j))
end
end
I would expect that <code>contents</code> would be a size 10 array (which it is) where each of the inner arrays are also of size 10; however each of the inner arrays end up being size 110, why is that?
EDIT
So it seems my main issue was misunderstanding how insert works. I've since changed my code to as follows:
class Cell
def initialize(status, xpos, ypos)
#status = status
#position = [xpos,ypos]
end
def declare_state
puts "Position is [" + #position[0].to_s + ", " + #position[1].to_s + "] and status is " + #status
end
end
contents = Array.new(10, Array.new(10))
for i in 0..9
for j in 0..9
contents[i][j] = Cell.new("DEAD", i, j))
end
end
contents.each {
|subarray| subarray.each {
|cell| cell.declare_status
}
}
It looks like all the #xpos values for all my Cell objects is being set to 9, which is unexpected.
I know this doesn't directly relate, but one way to fix this that would be more idiomatic with ruby would be to use each_with_index instead of having nested for loops. It would look like this:
class Cell
def initialize(status, xpos, ypos)
#status = status
#position = [xpos,ypos]
end
end
contents = Array.new(10, Array.new(10))
contents.each_with_index do |row, row_index|
row.each_with_index do |cell, cell_index|
contents[row_index][cell_index] = Cell.new("DEAD", row_index, cell_index)
end
end
In the line:
contents = Array.new(10, Array.new(10))
an array with 10 positions is created. What you may not realize is that each of those positions is filled with the same array.
I think you wanted
contents = Array.new(10) { Array.new(10) }
There are two issues here. First is your use of insert, which is creating new elements in the sub-arrays rather than editing the values. Rather than contents.at(i).insert(j, cell) you could use contents[i][j] = cell where cell is your cell object.
The second issue is that your use of contents = Array.new(10, Array.new(10)) is creating one array with 10 elements referring to the same single array reference. If you run the object_id to each sub-array, you will see they all refer to the same object. Updating one of the sub-arrays will appear to update all of them as a result.

How to find count matching characters at the same indes and at an unmatching index

I have built a version of mastermind that checks a user's input and provides feedback based on how close the user's guess was to the winning sequence. If you're not familiar with the game, you get feedback indicating how many of your characters were guessed correctly at the same index and how many characters guessed are in the sequence, but at the wrong index. If there are duplicates in the guess, then you would not count the extra values unless they correspond to the same number of duplicates in the secret code.
Example: If the sequence is ["G","G","G","Y"] and the user guesses ["G", "Y","G","G"] then you'd want to return 2 for items at the same index and 2 for items at different indexes that are included in the secret sequence.
Another example: If the sequence is ["X","R","Y","T"] and the user guesses ["T","T","Y","Y"] then you'd return 1 for items at the same index 1 for the character guessed that is in the sequence but at the wrong index.
Anyway, to me this is not a simple problem to solve. Here's the code I used to get it to work, but it's not elegant. There must be a better way. I was hoping someone can tell me what I'm missing here?? New to Ruby...
def index_checker(input_array, sequence_array)
count = 0
leftover_input = []
leftover_sequence = []
input.each_with_index do |char, idx|
if char == sequence[idx]
count += 1
else
leftover_input << char
leftover_sequence << sequence[idx]
end
end
diff_index_checker(leftover_input, leftover_sequence, count)
end
def diff_index_checker(input, sequence, count)
count2 = 0
already_counted = []
input.each do |char|
if sequence.include?(char) && !already_counted.include?(char)
count2 += 1
already_counted << char
end
end
[count, count2]
end
Here's a clean Ruby solution, written in idiomatic Ruby object-oriented style:
class Mastermind
def initialize(input_array, sequence_array)
#input_array = input_array
#sequence_array = sequence_array
end
def matches
[index_matches, other_matches]
end
def results
[index_matches.size, other_matches.size]
end
private
attr_reader :input_array, :sequence_array
def index_matches
input_array.select.with_index { |e, i| e == sequence_array[i] }
end
def other_matches
non_exact_input & non_exact_sequence
end
def non_exact_input
array_difference(input_array, index_matches)
end
def non_exact_sequence
array_difference(sequence_array, index_matches)
end
# This method is based on https://stackoverflow.com/a/3852809/5961578
def array_difference(array_1, array_2)
counts = array_2.inject(Hash.new(0)) { |h, v| h[v] += 1; h }
array_1.reject { |e| counts[e] -= 1 unless counts[e].zero? }
end
end
You would use this class as follows:
>> input_array = ["G","G","G","Y"]
>> sequence_array = ["G", "Y","G","G"]
>> guess = Mastermind.new(input_array, sequence_array)
>> guess.results
#> [2, 2]
>> guess.matches
#> [["G", "G"], ["G", "Y"]]
Here's how it works. First everything goes into a class called Mastermind. We create a constructor for the class (which in Ruby is a method called initialize) and we have it accept two arguments: input array (the user guess), and sequence array (the answer).
We set each of these arguments to an instance variable, which is indicated by its beginning with #. Then we use attr_reader to create getter methods for #input_array and #sequence_array, which allows us to get the values by calling input_array and sequence_array from any instance method within the class.
We then define two public methods: matches (which returns an array of exact matches and an array of other matches (the ones that match but at the wrong index), and results (which returns a count of each of these two arrays).
Now, within the private portion of our class, we can define the guts of the logic. Each method has a specific job, and each is named to (hopefully) help a reader understand what it is doing.
index_matches returns a subset of the input_array whose elements match the sequence_array exactly.
other_matches returns a subset of the input_array whose elements do not match the sequence_array exactly, but do match at the wrong index.
other_matches relies on non_exact_input and non_exact_sequence, each of which is computed using the array_difference method, which I copied from another SO answer. (There is no convenient Ruby method that allows us to subtract one array from another without deleting duplicates).
Code
def matches(hidden, guess)
indices_wo_match = hidden.each_index.reject { |i| hidden[i] == guess[i] }
hidden_counts = counting_hash(hidden.values_at *indices_wo_match)
guess_counts = counting_hash(guess.values_at *indices_wo_match)
[hidden.size - indices_wo_match.size, guess_counts.reduce(0) { |tot, (k, cnt)|
tot + [hidden_counts[k], cnt].min }]
end
def counting_hash(arr)
arr.each_with_object(Hash.new(0)) { |s, h| h[s] += 1 }
end
Examples
matches ["G","G","G","Y"], ["G", "Y","G","G"]
#=> [2, 2]
matches ["X","R","Y","T"] , ["T","T","Y","Y"]
#=> [1, 1]
Explanation
The steps are as follows.
hidden = ["G","G","G","Y"]
guess = ["G", "Y","G","G"]
Save the indices i for which hidden[i] != guess[i].
indices_wo_match = hidden.each_index.reject { |i| hidden[i] == guess[i] }
#=> [1, 3]
Note that the number of indices for which the values are equal is as follows.
hidden.size - indices_wo_match.size
#=> 2
Now compute the numbers of remaining elements of guess that pair with one of the remaining values of hidden by having the same value. Begin by counting the numbers of instances of each unique element of hidden and then do the same for guess.
hidden_counts = counting_hash(hidden.values_at *indices_wo_match)
#=> {"G"=>1, "Y"=>1}
guess_counts = counting_hash(guess.values_at *indices_wo_match)
#=> {"Y"=>1, "G"=>1}
To understand how counting_hash works, see Hash::new, especially the explanation of the effect of providing a default value as an argument of new. In brief, if a hash is defined h = Hash.new(3), then if h does not have a key k, h[k] returns the default value, here 3 (the hash is not changed).
Now compute the numbers of matches of elements of guess that were not equal to the value of hidden at the same index and which pair with an element of hidden that have the same value.
val_matches = guess_counts.reduce(0) do |tot, (k, cnt)|
tot + [hidden_counts[k], cnt].min
end
#=> 2
Lastly, return the values of interest.
[hidden.size - indices_wo_match.size, val_matches]
#=> [2, 2]
In the code presented above I have substituted out the variable val_matches.
With Ruby 2.4+ one can use Enumerable#sum to replace
guess_counts.reduce(0) { |tot, (k, cnt)| tot + [hidden_counts[k], cnt].min }
with
guess_counts.sum { |k, cnt| [hidden_counts[k], cnt].min }
def judge(secret, guess)
full = secret.zip(guess).count { |s, g| s == g }
semi = secret.uniq.sum { |s| [secret.count(s), guess.count(s)].min } - full
[full, semi]
end
Demo:
> judge(["G","G","G","Y"], ["G","Y","G","G"])
=> [2, 2]
> judge(["X","R","Y","T"], ["T","T","Y","Y"])
=> [1, 1]
A shorter alternative, though I find it less clear:
full = secret.zip(guess).count(&:uniq!)
I prefer my other answer for its simplicity, but this one would be faster if someone wanted to use this for arrays larger than Mastermind's.
def judge(secret, guess)
full = secret.zip(guess).count { |s, g| s == g }
pool = secret.group_by(&:itself)
[full, guess.count { |g| pool[g]&.pop } - full]
end
Demo:
> judge(["G","G","G","Y"], ["G","Y","G","G"])
=> [2, 2]
> judge(["X","R","Y","T"], ["T","T","Y","Y"])
=> [1, 1]

Merge Ruby arrays

I have a few arrays of Ruby objects of class UserInfo:
class UserInfo
attr_accessor :name, :title, :age
end
How can I merge these arrays into one array? A user is identified by its name, so I want no duplicate names. If name, title, age, etc. are equal I'd like to have 1 entry in the new array. If names are the same, but any of the other details differ I probably want those 2 users in a different array to manually fix the errors.
Thanks in advance
Redefine equality comparison on your object, and you can get rid of actual duplicates quickly with Array#uniq
class UserInfo
attr_accessor :name, :title, :age
def == other
name==other.name and title==other.title and age==other.age
end
end
# assuming a and b are arrays of UserInfo objects
c = a | b
# c will only contain one of each UserInfo
Then you can sort by name and look for name-only duplicates
d = c.sort{ |p,q| p.name <=> q.name } #sort by name
name = ""
e = []
d.each do |item|
if item.name == name
e[-1] = [e[-1],item].flatten
else
e << item
end
end
A year ago I monkey patched a kind of cryptic instance_variables_compare on Object. I guess you could use that.
class Object
def instance_variables_compare(o)
Hash[*self.instance_variables.map {|v|
self.instance_variable_get(v)!=o.instance_variable_get(v) ?
[v,o.instance_variable_get(v)] : []}.flatten]
end
end
A cheesy example
require 'Date'
class Cheese
attr_accessor :name, :weight, :expire_date
def initialize(name, weight, expire_date)
#name, #weight, #expire_date = name, weight, expire_date
end
end
stilton=Cheese.new('Stilton', 250, Date.parse("2010-12-02"))
gorgonzola=Cheese.new('Gorgonzola', 250, Date.parse("2010-12-17"))
irb is my weapon of choice
>> stilton.instance_variables_compare(gorgonzola)
=> {"#name"=>"Gorgonzola", "#expire_date"=>#<Date: 4910305/2,0,2299161>}
>> gorgonzola.instance_variables_compare(stilton)
=> {"#name"=>"Stilton", "#expire_date"=>#<Date: 4910275/2,0,2299161>}
>> stilton.expire_date=gorgonzola.expire_date
=> #<Date: 4910305/2,0,2299161>
>> stilton.instance_variables_compare(gorgonzola)
=> {"#name"=>"Gorgonzola"}
>> stilton.instance_variables_compare(stilton)
=> {}
As you can see the instance_variables_compare returns an empty Hash if the two objects has the same content.
An array of cheese
stilton2=Cheese.new('Stilton', 210, Date.parse("2010-12-02"))
gorgonzola2=Cheese.new('Gorgonzola', 250, Date.parse("2010-12-17"))
arr=[]<<stilton<<stilton2<<gorgonzola<<gorgonzola2
One hash without problems and one with
h={}
problems=Hash.new([])
arr.each {|c|
if h.has_key?(c.name)
if problems.has_key?(c.name)
problems[c.name]=problems[c.name]<<c
elsif h[c.name].instance_variables_compare(c) != {}
problems[c.name]=problems[c.name]<<c<<h[c.name]
h.delete(c.name)
end
else
h[c.name]=c
end
}
Now the Hash h contains the objects without merging problems and the problems hash contains those that has instance variables that differs.
>> h
=> {"Gorgonzola"=>#<Cheese:0xb375e8 #name="Gorgonzola", #weight=250, #expire_date=#<Date: 2010-12-17 (4911095/2,0,2299161)>>}
>> problems
=> {"Stilton"=>[#<Cheese:0xf54c30 #name="Stilton", #weight=210, #expire_date=#<Date: 2010-12-02 (4911065/2,0,2299161)>>, #<Cheese:0xfdeca8 #name="Stilton", #weight=250,#expire_date=#<Date: 2010-12-02 (4911065/2,0,2299161)>>]}
As far as I can see you will not have to modify this code at all to support an array of UserInfo objects.
It would most probably be much faster to compare the properties directly or with a override of ==. This is how you override ==
def ==(other)
return self.weight == other.weight && self.expire_date == other.expire_date
end
and the loop changes into this
arr.each {|c|
if h.has_key?(c.name)
if problems.has_key?(c.name)
problems[c.name]=problems[c.name]<<c
elsif h[c.name] != c
problems[c.name]=problems[c.name]<<c<<h[c.name]
h.delete(c.name)
end
else
h[c.name]=c
end
}
Finally you might want to convert the Hash back to an Array
result = h.values
Here's another potential way. If you have a way of identifying each UserInfo, say a to_str method that prints out the values:
def to_str()
return "#{#name}:#{#title}:#{#age}"
end
You can use inject and a hash
all_users = a + b # collection of users to "merge"
res = all_users.inject({})do |h,v|
h[v.to_str] = v #save the value indexed on the string output
h # return h for the next iteration
end
merged = res.values #the unique users

Resources