Combining multiple 'elsif' statements - ruby

I try to programm snakes in Ruby. In order to get myself more familiar with Ruby. I define the position of every part of the snake through saving its X and Y value in two 1D arrays one for a X value and one for a Y value.
$x = [2,...]
$y = [2,...]
(What I forgot to tell is that the head of the Snake moves through user input while the rest just inherits its position from the part before like this.)
def length(f)
if $length >= f
$y[f] = $y[f-1]
$x[f] = $x[f-1]
end
end
In order to get a field for the Snake to move around I programmed this.
for a in (1..20)
for b in (1..20)
print " X "
end
puts" "
end
Which gives me a 20*20 field.
I then tried to display every part of the snake like on the field like this.(While also drawing a boarder around the field.)
for a in (1..20)
for b in (1..20)
if a == 1 || a == 20
if b == 1 || b == 20
print " + "
else
print " - "
end
elsif b == 1 || b == 20
print " | "
elsif a == $x[0] && b == $y[0]
body
elsif a == $x[1] && b == $y[1]
body
elsif a == $x[2] && b == $y[2]
body
elsif a == $x[3] && b == $y[3]
body
elsif a == $x[4] && b == $y[4]
body
else
print " "
end
end
puts""
end
This works but if the user is really good/ has a lot of spare time I need to make allot of elsif for every one represents a part of the snake if the snake should have as a limit a length of 100 I would need to make 100 elsif statements.(The body is just:
def body
print " # ".green
end
)
I tried fixing it with a for loop like this:
for c in (1..100)
if a == $x[c] && b == $y[c]
body
end
end
and this
loop do
$x.size.times do |index|
if $x[index] == a && $y[index] == b
body
end
end
break
end
But sadly this didn't gave the desired outcome for this interfered with the ifthat draws the boarders of the field.
Is there a way to combine these multiple elsif statements?
Every help would be highly appreciated. ( Sorry for being to vague in the first draft.)

Recommended Refactorings
NB: You included no sample data in your original post, so your mileage with answers will vary.
You have a number of issues, not just one. Besides not being DRY, your code is also not very testable because it's not broken out into discrete operations. There are a number of things you can (and probably should) do:
Break your "body" stuff into discrete methods.
Use Array or Enumerator methods to simplify the data.
Use dynamic methods to loop over your arrays, rather than fixed ranges or for-loops.
Use case/when statements inside your loop to handle multiple conditionals for the same variable.
In short, you need to refactor your code to be more modular, and to leverage the language to iterate over your data objects rather than using one conditional per element as you're currently doing.
Simplify Your Data Set and Handle Procedurally
As an example, consider the following:
def handle_matched_values array
end
def handle_mismatched_values array
end
paired_array = a.zip b
matched_pairs = paired_array.select { |subarray| subarray[0] == subarray[1] }
unmatched_pairs = paired_array.reject { |subarray| subarray[0] == subarray[1] }
matched_pairs.each { |pair| handle_matched_values pair }
matched_pairs.each { |pair| handle_mismatched_values pair }
In this example, you may not even need an if statement. Instead, you could use Array#select or Array#reject to find indices that match whatever criteria you want, and then call the relevant handler for the results. This has the advantage of being very procedural, and makes it quite clear what data set and handler are being paired. It's also quite readable, which is extremely important.
Dynamic Looping and Case Statements
If you truly need to handle your data within a single loop, use a case statement to clean up your conditions. For example:
# Extract methods to handle each case.
def do_something_with data; end
def do_something_else_with data; end
def handle_borders data; end
# Construct your data any way you want.
paired_array = a.zip b
# Loop over your data to evaluate each pair of values.
paired_array.each do |pair|
case pair
when a == b
do_something_with pair
when a == paired_array.first || paired_array.last
handle_borders pair
else
do_something_else_with pair
end
end
There are certainly plenty of other ways to work pairwise with a large data set. The goal is to give you a basic structure for refactoring your code. The rest is up to you!

I would start with something like this:
(1..20).each do |a|
(1..20).each do |b|
if [1, 20].include?(a)
print([1, 20].include?(b) ? ' + ' : ' - ')
elsif (1..100).any? { |i| a == $x[i] && b == $y[i] }
body
else
print(' ')
end
puts('')
end
end

I suppose this would work as a solution even if it is not that advanced?
loop do
$x.size.times do |index|
if $x[index] == a && $y[index] == b
body
end
end
break
end

Related

How to read an array of strings and return strings without a new array

I am trying to write code that takes in an array of strings and returns the strings that are either less than 6 or end in "y", but not both.
The problem does not want a new array returned. I wrote code that works if I were to return a new array of strings that fit the conditions, but if I try to return just the strings and not an array, it doesn't work.
# frozen_string_literal: true
def select_long_words(words)
str = []
i = 0
while i < words.length
word = words[i]
if (word.length < 6 || word[-1] == 'y') && !(word.length < 6 && word[-1] == 'y')
str << word
end
i += 1
end
str
end
print select_long_words(%w[whatever are butterfly wit foreward funny])
puts
print select_long_words(%w[keepers cody])
This is the code that returns the new array of strings that fit the conditions.
Your question is a bit unclear but I'll try to anwser.
To select words that are less than 6 caracters or (exclusively) end with "y" you can use this function:
def select_long_words(words)
words.select do |word|
is_less_than_6 = word.size < 6
ends_with_y = word.end_with?('y')
# Select only if one of the two condition is respected, not both or none
next is_less_than_6 ^ ends_with_y
end
end
In this function I used the select function from Array that stands for "select every element that corresponds to the given criteria" and I set the criteria to "less than 6 characters or ends with 'y' but not both" using ^ which mean xor for booleans.
If you want only one word you can call your function like this:
select_long_words(["whatever", "are", "butterfly", "wit", "foreward", "funny"]).first
It'll return nil if there's no correspondancies, or the first word that corresponds. You can replace select with find in the method to get the first result directly.

How to get my method to return it's resulting values to a 3rd method

I'm not sure if the topic title is specific enough but here goes. I have two methods - one that iterates through some arrays along with conditionals in the block to push the correct data out.
Here is that code
def iterate_lines
WIN_COMBINATIONS.each_with_index do |line,index|
lines = #board[line[0]] + #board[line[1]] + #board[line[2]]
if lines.include?("X") && !lines.include?("O")
scores = tally_scores(lines.count("X"),"X",index)
elsif lines.include?("O") && !lines.include?("X")
scores = tally_scores(lines.count("O"),"O",index)
elsif lines.include?("X") && lines.include?("O")
scores = tally_scores(0,"",index)
elsif !lines.include?("X") && !lines.include?("O")
scores = tally_scores(0,"",index)
end
p scores
end
end
The other method is the one calculating those scores according to my chosen heuristics.
def tally_scores(score,player,index)
score = 1 if score == 1 && player == "X"
score = -1 if score == 1 && player == "O"
score = 10 if score == 2 && player == "X"
score = -10 if score == 2 && player == "O"
score = 100 if score == 3 && player == "X"
score = -100 if score == 3 && player == "O"
score
end
Calling 'iterate_lines I can print the correct values out from either 'tally_scores', or as I have shown here, by setting the variables 'scores' to the calls in 'iterate_lines', which allows me to just print them out from 'iterate_lines'.
Naturally the return values from 'iterate_lines' is the array (WIN_COMBINATIONS). Hard-coding a return scores obviously would give me just the last value.
My problem is I have a 3rd method that needs to get what comes out of 'tally_scores' yet I can't pass it over as a regular argument, aka my_method(scores). The reason being is that the 3rd method has it's own list of arguments it gets passed for other reasons. Plus it would be nil until the method was called.
def get_scores
# other code
#: something like this:
score = iterate_lines
# or
score = tally_scores
# or
# ?
end
So I feel like maybe I backed myself into a corner and should trash what I have and restart. I will say I tried taking 'tally_scores' and putting the scores into a instance variable array. I found though when I passed it, all but the last value remained.
There are a couple issues here. First of all, as you've seen when you use each_with_index nothing that happens in that block has an effect outside, unless you use side effects. If you a set a variable in that block it will be reset each iteration.
You can change it to map.with_index so that the result is an array of the results produced by the iterations.
Also it seems like scores should be score here and in lines similar to it, since tally_scores returns a single score:
scores = tally_scores(lines.count("X"),"X",index)
If you're using map.with_index, then the return value of the block should be score, that way the result will be an array of scores. However you can't use return score from the block, which will return from the parent method and not the single iteration of the block. You can use next score instead or simply score as the last line.
After making these changes, you can say scores = iterate_lines.
It would look something like this:
def iterate_lines
WIN_COMBINATIONS.map.with_index do |line, index|
# set score according to your conditional logic
score # or alternatively, "next score"
end
end
It's better to extract the printing logic to elsewhere, for example:
scores = iterate_lines
scores.each { |score| p score }

Does anyone see the error in this simple Ruby function?

This function is supposed to take a string and return the characters in reverse order.
def reverse(string)
reversedString = "";
i = string.length - 1
while i >= 0
reversedString = reversedString + string[i]
i -= 1
end
puts reversedString
end
however all the tests return false:
puts(
'reverse("abc") == "cba": ' + (reverse("abc") == "cba").to_s
)
puts(
'reverse("a") == "a": ' + (reverse("a") == "a").to_s
)
puts(
'reverse("") == "": ' + (reverse("") == "").to_s
)
Does anyone see what the problem is?
Try to use the default String class reverse method like this:
"Hello World".reverse
"Hello World".reverse!
Check Ruby's String class API at https://ruby-doc.org/core-2.4.0/String.html
If you want to make your custom method, you could use a map like this:
string = String.new
"Hello World".chars.each { | c | string.prepend c }
The problem is your function isn't returning its result, it's printing it. It needs to return reversedString.
As a rule of thumb, functions should return their result. Another function should format and print it.
def reverse(string)
reversedString = "";
i = string.length - 1
while i >= 0
reversedString = reversedString + string[i]
i -= 1
end
return reversedString
end
Note: This was probably an exercise, but Ruby already has String#reverse.
It's good that you're writing tests, but the way you're writing them it's hard to tell what went wrong. Look into a Ruby testing framework like MiniTest.
require "minitest/autorun"
class TestReverse < Minitest::Test
def test_reverse
assert_equal "cba", reverse("abc")
assert_equal "a", reverse("a")
assert_equal "", reverse("")
end
end
That would have told you that your function is returning nil.
1) Failure:
TestReverse#test_reverse [test.rb:16]:
Expected: "cba"
Actual: nil
To make this more Ruby-like yet avoid using the built-in String#reverse method you'd do this:
def reverse(string)
string.chars.reverse.join('')
end
Remember that in Ruby the result of the last operation is automatically the return value of the method. In your case the last operation is puts which always returns nil, eating your value. You want to pass it through.
Try to design methods with a simple mandate, that is, this function should focus on doing one job and one job only: reversing a string. Displaying it is beyond that mandate, so that's a job for another method, like perhaps the caller.
To avoid calling any sort of reverse method at all:
def reverse(string)
result = ''
length = string.length
length.times do |i|
result << string[length - 1 - i]
end
result
end
You can often avoid for almost completely and while frequently if you use things like times or ranges (0..n) to iterate over.
puts prints and returns nil, so the whole method returns nil. If, for debugging reasons , you want to inspect what your method is returning, use p which returns it's argument (reversedString in this case).
def reverse(string)
reversedString = ""
i = string.length - 1
while i >= 0
reversedString = reversedString + string[i]
i -= 1
end
p reversedString # !!!
end
And all 3 tests return true
If I was going to do this, I'd probably take advantage of an array:
ary = 'foo bar baz'.chars
reversed_ary = []
ary.size.times do
reversed_ary << ary.pop
end
reversed_ary.join # => "zab rab oof"
pop removes the last character from the array and returns it, so basically it's walking backwards through ary, nibbling at the end and pushing each character onto the end of reversed_ary, effectively reversing the array.
Alternately it could be done using a string:
ary = 'foo bar baz'.chars
reversed_str = ''
ary.size.times do
reversed_str << ary.pop
end
reversed_str # => "zab rab oof"
or:
reversed_str += ary.pop
I just saw that #tadman did a similar thing with the string. His would run more quickly but this is more readable, at least to my eyes.

Using random number generator and percentages?

Is there a better way to write this code?
I want 47.37 percent of the time the number should be "odd". 47.37 percent of the time the number should be "even".
random_no = rand*100
if (random_no <= 47.37)
number = "odd"
elsif (random_no <= 94.74)
number = "even"
else
number = "other"
end
This may be a solution:
{'odd'=> 47.37, 'even'=> 94.74, 'other'=> 100}
.find{|key, value| rand* 100 <= value}.first
I like Vidaica's answer if you're going for a one liner. But why? I think your code is easier to understand. Alternatively you could save three lines by using a case statement:
number = case rand*100
when 0...47.37 then 'odd'
when 47.37...94.74 then 'even'
else 'other'
end
I also like Cary's comment, about using integers for performance. You could combine that with any of the above solutions.
Now that I know it's for a roulette game, I'd go about it differently, using rand(38) to pick a number and then seeing whether it's odd, even or 'other'. To illustrate an OO approach to this:
class Roulette
def initialize
spin
end
def spin
#number_showing = rand(38)
end
def odd_even?
if #number_showing == 0 || #number_showing == 37
return 'other'
elsif #number_showing.odd?
return 'odd'
else
return 'even'
end
end
def number_showing
if #number_showing == 37
return '00'
else
return #number_showing.to_s
end
end
end
wheel = Roulette.new
10.times do
wheel.spin
puts wheel.number_showing
puts wheel.odd_even?
puts
end

Trouble with combined logic in ruby

writing a simple monte carlo simulation of a neutron beam. Having trouble with the geometry logic (whether something is in one environment or another). My issue is that Ruby seems to be processing the conditions sequentially and keeping the first value it comes to.
The code below illustrates this quite nicely:
def checkPosition(*args)
polyCylRad = 2.5
polyCylFr = 15
polyCylB = -2.0
borPolyBoxL = 9.0 / 2
pbCylRad = 3.0
pbBoxL = 10.0 / 2
cdBoxL = 9.5 / 2
position = Array.new
material = String.new
args.each do |item|
position << item.inspect.to_f
end
xSquared = position.at(0) ** 2
ySquared = position.at(1) ** 2
zSquared = position.at(2) ** 2
modX = Math.sqrt(xSquared)
modY = Math.sqrt(ySquared)
modZ = Math.sqrt(zSquared)
puts xSquared
puts Math.sqrt(ySquared + zSquared) <= polyCylRad
puts (position.at(0) >= polyCylB)
puts (position.at(0) <= polyCylFr)
puts (position.at(0) >= polyCylB)and(position.at(0) <= polyCylFr)
puts (position.at(0) <= polyCylFr)and(position.at(0) >= polyCylB)
puts zSquared
polyCylinder = (Math.sqrt(ySquared + zSquared) <= polyCylRad)and((position.at(0) >= polyCylB)and(position.at(0) <= polyCylFr) )
puts polyCylinder
borPolyBox = ((modX <= borPolyBoxL)or(modY < borPolyBoxL)or(modZ <= borPolyBoxL)) and not((modX >= cdBoxL)or(modY >= cdBoxL)or(modZ >= cdBoxL)) and not(Math.sqrt(ySquared + zSquared) <= polyCylRad)
puts borPolyBox
cadmiumShield = ((modX <= cdBoxL)or(modY < cdBoxL)or(modZ <= cdBoxL)) and not((modX >= pbBoxL)or(modY >= pbBoxL)or(modZ >= pbBoxL)) and not(Math.sqrt(ySquared + zSquared) <= polyCylRad)
puts cadmiumShield
leadShield = ( ((modX <= pbBoxL)or(modY <= pbBoxL)or(modZ <= pbBoxL)) or ((position.at(0) <= ployCylFr)and(Math.sqrt(ySquared + zSquared) <= pbCylRad)) ) and not(Math.sqrt(ySquared + zSquared) <= polyCylRad)
puts leadShield
if (polyCylinder) : material = "poly"
elsif(borPolyBox) : material = "borPoly"
elsif(cadmiumSheild) : material = "cd"
elsif(leadSheild) : material = "pb"
elsif(material == nil) : position = Array.new
end
thisEnvironment = Array.new
thisEnvironment << position << material
puts thisEnvironment.at(0)
puts thisEnvironment.at(1)
end
checkPosition(40, 0, 0)
call the code whatever you want, but give it *args as an argument (I am lazy and may want to add more args in the future) then call it with 3 floats, wrt the geometry set up in the logic and you'll see what I mean.
My question is: how do I get it to work like it should (ie evaluating the logic correctly) without a whole bunch of nested if's? (which is what I am about to remake, however it is a nightmare to read and memory is cheap.)
You've typed "Sheild" a few times where you probably meant "Shield"
In the context you're using them, you should be using && instead of and, || instead of or, and ! instead of not. The reason is that or and and have such a low precedence that they will cause your assignment operators to not work the way you want. For example,
a = b and c
evaluates as
(a = b) and c
Such that a is always assigned the value b, and then in the result is truthy, c is evaluated (and discarded). On the other hand,
a = b && c
evaluates as
a = (b && c)
Which is what you want in this code.
Beyond that, I would move all of this code into a class, so that I can create lots of little methods for things:
class PositionChecker
def initialize(*args)
#x, #y, #z = *args
end
def checkPosition
...
end
end
Look for opportunities to replace local variables in checkPosition with method calls. For example, you could move borPolyBox into its own method (once all of the values it uses are methods of their own):
class PositionChecker
...
def borPolyBox
((modX <= borPolyBoxL)||(modY < borPolyBoxL)||(modZ <= borPolyBoxL)) && !((modX >= cdBoxL)||(modY >= cdBoxL)||(modZ >= cdBoxL)) && !(Math.sqrt(ySquared + zSquared) <= polyCylRad)
end
...
end
Once you've got all of these predicates as their own method, you can create a method to determine the material, like so:
def material
[
[:polyCylinder, 'poly'],
[:borPolyBox, 'borPoly'],
[:cadmiumShield, 'cd'],
[:leadShield, 'pb'],
].each do |method, name|
return name if send(method)
end
nil
end
And one for the position:
def position
[#x, #y, #z] if material
end
Continue along this line until nothing is left but a bag of teeny, focused methods.
Change all and and or to && and ||.
Never seen anyone actually use array.at(index) instead of array[index] before.
I also recommend against *args in favor of a Hash parameter as a kind of named parameters
def test(params)
x = params[:x] || raise("You have to provide x!")
y = params[:y] || raise("You have to provide y!")
z = params[:z] || raise("You have to provide z!")
puts x, y, z
end
and call it with (Ruby 1.9+ syntax)
test({x: 42, y: 4711, z: 93})
42
4711
93
Try using && instead of and, || instead of or and ! instead of not.
Your issue is probably a precedence one - read this article for more information.
Looking at your code, the way you are setting this up is to have the four "material" variables as booleans. Then you feed those bools into an if-elsif-else block.
The problem with this is that the very first if that returns true is going to exit the if-elsif-else block. If that's what you mean by keeping the first value it comes to, than that's an extremely predictable outcome.

Resources