How to reference a part of a 3D array? - ruby

So say I have a 3D array and I push values like so:
#h = [[[]]]
#h.push(["cat", "mammal", 100.0])
#h.push(["lizard", "reptile", 300.0])
#h.push(["dog", "mammal", 200.0])
How would I reference the max index by the 3rd element (the float) and then reference each individual elements of the index to output in this example just the value "reptile"?
I've tried:
#h.each do |(x,y,z)|
if ( [x,y,z] == #h.max_by{|(x,y,z)| z} )
puts y
end
end
But it doesn't give me only the "y" value.
Thanks!

#h = [] # not [[[]]]
#h.push(["cat", "mammal", 100.0])
#h.push(["lizard", "reptile", 300.0])
#h.push(["dog", "mammal", 200.0])
max_ar = #h.max_by{|ar| ar[2]}
p max_ar[1] #=> "reptile"
# Your way is less usual. It works fine, but you're working too hard:
max_ar = #h.max_by{|(x,y,z)| z}
p max_ar[1] #=> "reptile"

Related

Syncing a random variable in ruby

I am making a simple Tic tac toe game where the rows are represented by three arrays and columns are items in those arrays. For my computer choice I use two separate functions to get the row and column.
$first = ["-","-","-"]
$secound = ["-","-","-"]
$third = ["-","-","-"]
def get_row
row = nil
case rand(3)
when 0
row = $first
when 1
row = $secound
else
row = $third
end
row
end
def get_col
col = nil
case rand(3)
when 0
col = 0
when 1
col = 1
else
col = 2
end
col
end
I then use a third function that keeps generating a new "guess" until it finds an empty spot, at which point it marks an 0.
def computer_guess
temp = false
try = get_row[get_col]
while temp == false
if try == "-"
get_row[get_col] = "O"
temp = true
else
try = get_row[get_col]
end
end
end
My problem is that i cant return the guess to the same position that i check for validity. Is the there a way to sync these or do i need a different approach?
The problem with your approach is that get_row[get_col] returns a random element every time it is called, so likewise get_row[get_col] = "O" will set a random element to "O". You inspect one element and then set another.
You could fix this quickly by modifying the retrieved element in-place:
if try == "-"
try.replace("O")
# ...
But semantically, I don't like that fix very much. Thinking of a tic-tac-toe board, I'd rather assign an "O" to the free spot than transforming the existing placeholder from "-" into "O".
Did you notice that your get_row method returns an array whereas get_col returns an index? I think this mixture of arrays and indices makes your code a bit convoluted.
It's much easier (in my opinion) to access both, row and column via an index.
To do so, you could put your three rows into another array:
$board = [
['-', '-', '-'],
['-', '-', '-'],
['-', '-', '-']
]
The first row can be accessed via $board[0] and its first element via $board[0][0]. To set an element you'd use: $board[1][2] = 'O' (this sets the middle row's right-most element to "O"). Of course, you can also use variables, e.g. $board[row][col].
With this two-dimensional array, your computer_guess could be rewritten using just two random indices: (get_row and get_col aren't needed anymore)
def computer_guess
loop do
row = rand(3) # random row index
col = rand(3) # random column index
if $board[row][col] == '-' # if corresponding spot is "-"
$board[row][col] = 'O' # set that spot to "O"
break # and break out of the loop
end
end
end
Note however that "blindly" guessing spots until you find a free one might not be the best approach.
Instead, you could generate a list of "free" spots. This can be done by first generating an array of all coordinates1 and then select-ing those row/col pairs whose spot is "-":
def free_spots
coordinates = [0, 1, 2].product([0, 1, 2])
coordinates.select { |row, col| $board[row][col] == '-' }
end
Now you just have to chose a random pair (via sample) and set the corresponding spot to "O":
def computer_guess
row, col = free_spots.sample
$board[row][col] = 'O'
end
1 Array#product returns the Cartesian product of the given arrays. It's an easy way to get all pairs:
[0, 1, 2].product([0, 1, 2])
#=> [[0, 0], [0, 1], [0, 2],
# [1, 0], [1, 1], [1, 2],
# [2, 0], [2, 1], [2, 2]]
Here is another approach. This code imitates game computer against itself. You can easily replace one computer's move with user input.
ROWS = 3
COLUMNS = 3
MAX_TURNS = ROWS * COLUMNS
def random_cell
{ row: rand(ROWS), column: rand(COLUMNS) }
end
def empty_random_cell(board)
while true
cell = random_cell
return cell if board_value(board, cell).nil?
end
end
def board_value(board, cell)
board[cell[:row]][cell[:column]]
end
def turn(board, cell, value)
row = cell[:row]
column = cell[:column]
board[row][column] = value
end
def print_board(board)
ROWS.times do |row|
COLUMNS.times do |column|
value = board[row][column]
print value.nil? ? '_' : value
print ' '
end
puts
end
end
board = Array.new(ROWS) { Array.new(COLUMNS) }
print_board(board)
turns_counter = 0
while true
cell = empty_random_cell(board)
turn(board, cell, 'X')
turns_counter += 1
break if turns_counter == MAX_TURNS
cell = empty_random_cell(board)
turn(board, cell, '0')
turns_counter += 1
break if turns_counter == MAX_TURNS
end
print_board(board)
when there is only one available spot, it might take time to guess it.
So it might be better to collate the available spots and get a random value from there:
def computer_guess
free_spots=[]
free_spots.concat($first.each_with_index.map{|x,i|x=='-' ? ['$first',i] : nil}.compact)
free_spots.concat($secound.each_with_index.map{|x,i|x=='-' ? ['$secound',i] : nil}.compact)
free_spots.concat($third.each_with_index.map{|x,i|x=='-' ? ['$third',i] : nil}.compact)
try=free_spots.sample
eval"#{try[0]}[#{try[1]}]='O'" unless try.nil?
end

Identifying Triangle with if/else

Question is a user gives 3 sides and identifies triangles, like equilateral, isosceles and scalene. Here is my coding, I don't know why gives any sides that always show up "invalid". I think it's logic wrong, but I can't figure out.
puts "please input the length of 3 sides:"
a = gets.chomp.to_i
b = gets.chomp.to_i
c = gets.chomp.to_i
if a + b <= c
puts "invalid"
elsif a <= 0 || b <= 0 || c <= 0
puts "invalid"
else
if a == b && b == c
puts"equilateral triangle"
elsif a == b
puts"isosceles triangle"
else
puts"scalene triangle"
end
end
The fact that your code always prints "invalid" makes me think that input is passed in on one line instead of being on separate lines. For example, when the input is:
50 50 50
instead of getting 50 in all three variables you would get 50 in a and 0 in b, c. This is because gets takes in an entire line instead of taking one value.
In such an event, this is what you need:
a, b, c = gets.split.map{ |value| value.to_i }
A better more effective way to do this is to store the values of the triangle sides into a hash first, the value of of each triangle side will be the keys, and the value of each key can be the repeats. This will work with strings too.
Here is an Example.
# First you get an array, you can use gets.chomp as string and split to array, whichever way you choose, but in the end we end up with an array, and we pass the array to the method.
def triangle_type(arr)
# Create new empty hash
repeated_sides = Hash.new(0)
# First make sure the array is only a length of three. (this is optional)
if arr.length == 3
# Iterate through each value in the array and store it to to a hash to find duplicates
arr.each do |x|
repeated_sides[x] += 1
end
# sort the hash by it's values in descending order, for logic to work later.
repeated_sides = repeated_sides.sort_by {|k,v| v}.reverse.to_h
# uncomment this below to see the duplicate sides hash
#puts "#{repeated_sides}"
# Iterate through the sorted hash, apply logic starting from highest and first value the iterator will find.
repeated_sides.each do |k,v|
return v == 3 ? 'Equilateral Triangle' : v == 2 ? 'Isosceles Triangle' : 'Scalene Triangle'
end
end
# Return Not a triangle if the condition fails
return 'Not a triangle'
end
# Test with integers
puts triangle_type([4,1,2,5]) # output: Not a triangle
puts triangle_type([3,3,3]) # output: Equilateral Triangle
puts triangle_type([4,3,3]) # output: Isosceles Triangle
puts triangle_type([4,2,3]) # output: Scalene Triangle
# Test with strings
puts triangle_type(['4','1','2','5']) # output: Not a triangle
puts triangle_type(['3','3','3']) # output: Equilateral Triangle
puts triangle_type(['4','3','3']) # output: Isosceles Triangle
puts triangle_type(['4','2','3']) # output: Scalene Triangle
puts triangle_type(['a','a','a']) # output: Equilateral Triangle
puts triangle_type(['a','c','c']) # output: Isosceles Triangle
puts triangle_type(['a','b','c']) # output: Scalene Triangle
Skipping user inputs, since I can not reproduce the error (even if Unihedron found a fix) there is still a problem with the logic.
When the input is a = 1000, b = 1, c = 1, the result is "scalene triangle", but it should return "invalid". Below a fix I suggest.
Let's store the input in an array (already converted into integer or float):
sides = [a, b, c]
First you need to check that all sides are positive:
sides.all? { |x| x > 0 }
Then, check that the sum of two sides is greater than the other:
sides.combination(2).map{ |x| x.sum }.zip(sides.reverse).all? { |xy, z| xy > z }
Finally (I'm missing something?), to pick the triangle denomination you can use an hash accessing it by sides.uniq result:
triangle_kinds = {1 => 'equilateral', 2 => 'isosceles', 3 => 'scalene'}
triangle_kinds[sides.uniq.size]
Used the following methods over array (enumerable):
https://ruby-doc.org/core-2.5.1/Enumerable.html#method-i-all-3F
https://ruby-doc.org/core-2.5.1/Array.html#method-i-combination
https://ruby-doc.org/core-2.5.1/Array.html#method-i-map
https://ruby-doc.org/core-2.5.1/Array.html#method-i-zip
https://ruby-doc.org/core-2.5.1/Array.html#method-i-reverse
https://ruby-doc.org/core-2.5.1/Array.html#method-i-sum
https://ruby-doc.org/core-2.5.1/Array.html#method-i-uniq

Pick item in array by percentage

I have an array which contains names and percentages.
Example:
[["JAMES", 3.318], ["JOHN", 3.271], ["ROBERT", 3.143]].
Now I have about a thousand of these names, and I'm trying to figure out how to choose a name randomly based on the percentage of the name (like how James as 3.318% and John as 3.271%), so that name will have that percentage of being picked (Robert will have a 3.143% of being picked). Help would be appreciated.
You can use max_by: (the docs contain a similar example)
array.max_by { |_, weight| rand ** 1.fdiv(weight) }
This assumes that your weights are actual percentages, i.e. 3.1% has to be expressed as 0.031. Or, if you don't want to adjust your weights:
array.max_by { |_, weight| rand ** 100.fdiv(weight) }
I'm using fdiv here to account for possible integer values. If your weights are always floats, you can also use /.
Even though I like #Stefan answer more than mine, I will contribute with a possible solution: I would distribute all my percentages along 100.0 so that they start from 0.0 and end to 100.0.
Imagine I have an array with the following percentages:
a = [10.5, 20.5, 17.8, 51.2]
where
a.sum = 100.0
We could write the following to distribute them along 100.0:
sum = 0.0
b = a.map { |el| sum += el }
and the result would be
b = [10.5, 31.0, 48.8, 100.0]
now I can generate a random number between 0.0 and 100.0:
r = rand(0.0..100.0) # or r = rand * 100.0
imagine r is 45.32.
I select the first element of b that is >= r`
idx = b.index { |el| el >= r }
which in our case would return 2.
Now you can select a[idx].
But I would go with #Stefan answer as well :)
I assume you will be drawing multiple random values, in which case efficiency is important. Moreover, I assume that all names are unique and all percentages are positive (i.e., that pairs with percentages of 0.0 have been removed).
You are given what amounts to a (discrete) probability density function (PDF). The first step is to convert that to a cumulative density function (CDF).
Suppose we are given the following array (whose percentages sum to 100).
arr = [["LOIS", 28.16], ["JAMES", 22.11], ["JOHN", 32.71], ["ROBERT", 17.02]]
First, separate the names from the percentages.
names, probs = arr.transpose
#=> [["LOIS", "JAMES", "JOHN", "ROBERT"],
# [28.16, 22.11, 32.71, 17.02]]
Next compute the CDF.
cdf = probs.drop(1).
each_with_object([0.01 * probs.first]) { |pdf, cdf|
cdf << 0.01 * pdf + cdf.last }
#=> [0.2816, 0.5027, 0.8298, 1.0]
The idea is that we will generate a (pseudo) random number between zero and one, r and find the first value c of the CDF for which r <= c.1 To do this in an efficient way we will perform an intelligent search of the CDF. This is possible because the CDF is an increasing function.
I will do a binary search, using Array#bsearch_index. This method is essentially the same as Array#bseach (whose doc is the relevant one), except the index of cdf is returned rather than the element of cdf is randomly selected. It will shortly be evident why we want the index.
r = rand
#=> 0.6257547400776025
idx = cdf.bsearch_index { |c| r <= c }
#=> 2
Note that we cannot write cdf.bsearch_index { |c| rand <= c } as rand would be executed each time the block is evaluated.
The randomly-selected name is therefore2
names[idx]
#=> "JOHN"
Now let's put all this together.
def setup(arr)
#names, probs = arr.transpose
#cdf = probs.drop(1).
each_with_object([0.01*probs.first]) { |pdf, cdf| cdf << 0.01 * pdf + cdf.last }
end
def random_name
r = rand
#names[#cdf.bsearch_index { |c| r <= c }]
end
Let's try it. Execute setup to compute the instance variables #names and #cdf.
setup(arr)
#names
#=> ["LOIS", "JAMES", "JOHN", "ROBERT"]
#cdf
#=> [0.2816, 0.5027, 0.8298, 1.0]
and then call random_name each time a random name is wanted.
5.times.map { random_name }
#=> ["JOHN", "LOIS", "JAMES", "LOIS", "JAMES"]
1. This is how most discrete random variates are generated in simulation models.
2. Had I used bsearch rather than bsearch_index I would have had to earlier create a hash with cdf=>name key-value pairs in order to retrieve a name for a given randomly-selected CDF value.
This is my solution to the problem:
array = [["name1", 33],["name2", 20],["name3",10],["name4",7],["name5", 30]]
def random_name(array)
random_number = rand(0.000..100.000)
sum = 0
array.each do |x|
if random_number.between?(sum, sum + x[1])
return x[0]
else
sum += x[1]
end
end
end
puts random_name(array)

Find the largest value for an array of hashes with common keys?

I have two arrays, each containing any number of hashes with identical keys but differing values:
ArrayA = [{value: "abcd", value_length: 4, type: 0},{value: "abcdefgh", value_length: 8, type: 1}]
ArrayB = [{value: "ab", value_length: 2, type: 0},{value: "abc", value_length: 3, type: 1}]
Despite having any number, the number of hashes will always be equal.
How could I find the largest :value_length for every hash whose value is of a certain type?
For instance, the largest :value_length for a hash with a :type of 0 would be 4. The largest :value_length for a hash with a :type of 1 would be 8.
I just can't get my head around this problem.
A simple way:
all = ArrayA + ArrayB # Add them together if you want to search both arrays.
all.select{|x| x[:type] == 0}
.max_by{|x| x[:value_length]}
And if you wanna reuse it just create a function:
def find_max_of_my_array(arr,type)
arr.select{|x| x[:type] == type}
.max_by{|x| x[:value_length]}
end
p find_max_of_my_array(ArrayA, 0) # => {:value=>"abcd", :value_length=>4, :type=>0}
I'm not totally sure I know what the output you want is, but try this. I assume the arrays are ordered so that ArrayA[x][:type] == ArrayB[x][:type] and that you are looking for the max between (ArrayA[x], ArrayB[x]) not the whole array. If that is not the case, then the other solutions that concat the two array first will work great.
filtered_by_type = ArrayA.zip(ArrayB).select{|x| x[0][:type] == type }
filtered_by_type.map {|a| a.max_by {|x| x[:value_length] } }
Here's how I approached it: You're looking for the maximum of something, so the Array#max method will probably be useful. You want the actual value itself, not the containing hash, so that gives us some flexibility. Getting comfortable with the functional programming style helps here. In my mind, I can see how select, map, and max fit together. Here's my solution which, as specified, returns the number itself, the maximum value:
def largest_value_length(type, hashes)
# Taking it slowly
right_type_hashes = hashes.select{|h| h[:type] == type}
value_lengths = right_type_hashes.map{|h| h[:value_length]}
maximum = value_lengths.max
# Or, in one line
#hashes.select{|h| h[:type] == type}.map{|h| h[:value_length]}.max
end
puts largest_value_length(1, ArrayA + ArrayB)
=> 8
You can also sort after filtering by type. That way you can get smallest, second largest etc.
all = ArrayA + ArrayB
all = all.select { |element| element[:type] == 1 }
.sort_by { |k| k[:value_length] }.reverse
puts all[0][:value_length]
#8
puts all[all.length-1][:value_length]
#3

Ruby: Using the "index" method with "OR"

Using the index method, I'm trying to find if a value exists using a certain variable, and if that variable doesn't exist, then try another variable; a little something like this (3rd line below):
a = [ "a", "b", "c" ]
a.index("b") #=> 1
a.index("z" or "y" or "x" or "b") #=> 1
..meaning that if "z" is not found in the array, then try "y"; if y is not found, then try x; if x is not found then try b
How would I do that correctly?
TIMTOWTDI. But I prefer using Array#inject.
%w(x y z b).inject(nil) { |i, e| i or a.index(e) } #=> 1
And there is an another way to do this with more similar to your pseudo-code.
class String
def | other
->(e) { self==e || other==e }
end
end
class Proc
def | other
->(e) { self.call(e) || other==e }
end
end
a.index(&('x' | 'y' | 'z' | 'b')) #=> 1
Depends on your end goal. If you just want to see if a contains a z, y, x or b, you can do this:
(a & %w{z y x b}).length > 0 # gives true if a contains z, y, x and/or b.
what we're doing is seeing if there is a set intersection where a contains some shared elements with the set of desired quantities, then testing to see if there were any of those elements.

Resources