Why does changing the order of conditionals break my code? - ruby

I have created a ping-pong test in Ruby. It monkey patches the Fixnum class with a new method, ping_pong, that loops over a range (0..self), checks some conditions on each element, and constructs an array of the results.
The resulting array will have Ping for numbers in the range divisible by 3, Pong for numbers divisible by 5, and Ping-Pong for numbers divisible by both.
My question now is, why does the code only work if the part:
elsif (num.%(3) == 0) && (num.%(5) == 0) array.push("Ping-Pong")
is ahead of the other elsif statements? I tried putting it after the other elsifs but it didn't work.
Here's my code:
class Fixnum
define_method(:ping_pong) do
array = [0]
total = (0..self)
total = total.to_a
total.each() do |num|
if (num == 0)
array.push(num)
elsif (num.%(3) == 0) && (num.%(5) == 0)
array.push("Ping-Pong")
elsif (num.%(3) == 0)
array.push("Ping")
elsif (num.%(5) == 0)
array.push("Pong")
else
array.push(num)
end
end
array
end
end

When you have multiple if/elsif blocks chained together, only one of them will run, and the first block to have a true condition will be the one to be run. So, the order of the blocks matters. For example:
if true
puts 'this code will run'
elsif true
puts 'this code will not run'
end
Even though the conditions for those blocks are both true, only the first one is run. If you want to have both run, use two separate if blocks, like this:
if true
puts 'this code will run'
end
if true
puts 'this code will also run'
end

Related

Combining multiple 'elsif' statements

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

How can I avoid error when setting elsif range condition?

def Summer
#summer = true
puts "Your fruit are ripe for the picking."
if #tree_age == 1..5 && #tree_age > 0
#oranges = 5
elsif #tree_age == 6..15
#oranges = 20
else
#oranges = 50
end
end
I'm trying to ensure a tree between a certain age range gives x oranges, however I'm stuck with the following error referring to my elsif statement:
Orange_tree.rb:14: warning: integer literal in conditional range
I have also tried using an if greater than && less than conditional statement, can somebody please explain what this error means, and how to reach my solution.
You have a few problems:
You'll want to put your ranges in parenthesis when other operators or methods are nearby. Your current error comes from Ruby parsing elsif #tree_age == 6..15 differently than you expect - it's treating it as (1 == 6)..15, and false..15 obviously doesn't make any sense.
To test a number is within a range, use (1..5) === num, not num == (1..5). Range#=== is defined to test that the Range includes the right hand side, while Fixnum#== and Fixnum#=== both just test that the right hand side is numerically equivalent.
You don't need to test #tree_age > 0. You're already testing that it's in 1..5.
You could also consider a case statement for this, which can be a bit easier to read. case does its comparisons using ===.
#oranges = case #tree_age
when 1..5 then 5
when 6..15 then 20
else 50
end
You should use include? instead of == to determine if the given number is within the range:
def Summer
#summer = true
puts "Your fruit are ripe for the picking."
if (1..5).include?(#tree_age) && #tree_age > 0
#oranges = 5
elsif (6..15).include? #tree_age
#oranges = 20
else
#oranges = 50
end
end
==:
Returns true only if obj is a Range, has equivalent begin and end
items (by comparing them with ==), and has the same exclude_end?
setting as the range.
Which is obviously not the case.
The problem is with the lines that say == with a range.
if ( 10 == 1..11) # throws integer literal in conditional range warning
puts "true"
end
If you did this instead
if ( 10.between?(1, 11))
puts "true"
end

Bowling calculator

I am writing a bowling score calculator in Ruby that is defined and tested using RSpec. It currently runs, but only passes 5 of the 8 input tests. Here is the code for my implementation:
class ScoreKeeper
def calculate(input)
unless input.is_a? String
raise argumentException, "Score Keeper will only except string types for score calculation."
end
# Thanksgiving Turkey Edge Case
return 300 if input == "xxxxxxxxxxxx"
# Calculate Score
throws = input.gsub(/-/, "0").split(//)
score = 0
throws.each_with_index do |ball, i|
current_throw = i
last_throw = throws[i - 1] || "0"
lastlast_throw = throws[i - 2] || "0"
next_throw = throws[i + 1] || "0"
if current_throw == 0
last_throw = 0
lastlast_throw = 0
end
if current_throw == 1
lastlast_throw = 0
end
working_value = 0
# Add numbers directly (unless part of a spare frame)
if ((1..9) === ball.to_i)
working_value = ball.to_i
end
# Add strike as 10 points
if ball == "x"
working_value = 10
end
# Add spare as number of remaining pins from last throw
if ball == "/"
if last_throw == "/" || last_throw == "x"
raise argumentException, "Invalid score string. A spare cannot immediately follow a strike or spare."
end
working_value = 10 - last_throw.to_i
end
# Strike / Spare Bonus
if last_throw == "x" || last_throw == "/" || lastlast_throw == "x"
score += working_value
end
# Add current throw value
score += working_value
end
if score > 300 || score < 0
raise argumentExcpetion, "Invalid score string. Impossible score detected."
end
score
end
end
I can't tell why my code is not calculating a proper score in every test case.
The RSpec:
require "./score_keeper"
describe ScoreKeeper do
describe "calculating score" do
let(:score_keeper) { described_class.new }
context "when rolls are valid" do
{
"xxxxxxxxxxxx" => 300,
"--------------------" => 0,
"9-9-9-9-9-9-9-9-9-9-" => 90,
"5/5/5/5/5/5/5/5/5/5/5" => 150,
"14456/5/---17/6/--2/6" => 82,
"9/3561368153258-7181" => 86,
"9-3/613/815/0/8-7/8-" => 121,
"x3/61xxx2/9-7/xxx" => 193
}.each do |bowling_stats, score|
it "returns #{score} for #{bowling_stats}" do
expect(score_keeper.calculate(bowling_stats)).to eq score
end
end
end
end
end
The failing inputs are:
"5/5/5/5/5/5/5/5/5/5/5" (expected: 150, got: 155),
"x3/61xxx2/9-7/xxx" (expected: 82, got: 88),
"14456/5/---17/6/--2/6" (expected: 193, got: 223)
The first thing I see is your use of gsub:
input.gsub(/-/, "0")
You're not assigning the string returned by gsub to anything, and instead you're throwing it away.
input = '#0#'
input.gsub('0', '-') # => "#-#"
input # => "#0#"
I suspect you're thinking of the mutating gsub! but instead I suggest simply passing the value to split:
_frames = input.gsub(/-/, "0").split(//)
Your code is not idiomatic Ruby; There are a number of things you need to do differently:
Instead of if !input.is_a? String use:
unless input.is_a? String
raise argumentException, "Score Keeper will only except string types for score calculation."
end
It's considered better to use unless than a negated test.
Instead of
if input == "xxxxxxxxxxxx"
return 300
end
use a "trailing if":
return 300 if input == "xxxxxxxxxxxx"
Don't name variables with a leading _. _frames should be frames.
Don't name variables like lastFrame, lastlastFrame and workingValue with mixed-case AKA "camelCase". We use "snake_case" for Ruby variables and methods and camelCase for classes and modules. It_is_a matterOfReadability.
Don't end lines with a trailing ;:
workingValue = 0;
The only time we use a trailing semicolon is when we're using multiple statements on a single line, which should be extremely rare. Just don't do that unless you know why and when you should.
Consider the potential problem you have here:
"12".include?('1') # => true
"12".include?('2') # => true
"12".include?('12') # => true
While your code might skirt that issue, don't write code like that and think about side-effects. Perhaps you want to really test to see if the value is an integer between 1 and 9?
((1 .. 9) === '1'.to_i) # => true
((1 .. 9) === '2'.to_i) # => true
((1 .. 9) === '12'.to_i) # => false
Instead of using
return score
you can simply use
score
Ruby will return the last value seen; You don't have to explicitly return it.
Indent your code properly. Your future self will appreciate it when you have to dive back into code to debug something. Consistenly use two space indents.
Use whitespace liberally to separate your code into readable blocks. It doesn't affect the run-time speed of your code and it makes it a lot easier to read. Again, your future self will appreciate it.
While it might seem nit-picking, those little things go a long way when coding in a team of developers, and failing to do those things can land you in the hot seat during a code-review.
You're problem appears to be that that for your first two frames you're adding the last two frames. Consider the following.
arr = [1,2,3,4,5,6,7,8,9]
arr.each_with_index do |num, i|
puts "current number #{num}"
puts arr[i-1]
puts arr[i-2]
end
I think you need an if statement to handle the first two frames because - index will loop back to the end of the array if you're at 0 index.
so you need something like
arr = [1,2,3,4,5,6,7,8,9]
arr.each_with_index do |num, i|
puts "current number #{num}"
if i <= 1
puts "no previous frame"
elsif i == 1
puts arr[i-1] + "can be added to frame 2"
else
puts arr[i-1] + "can be added to frame 1"
puts arr[i-2] + "can be added to frame 2"
end
end

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

binary alphabet sort ruby

i tried writing my own alphabet search for Chris Pine tutorial chapter 7, and i waned to implement a binary method. there is no validity for string input, so i don't know what will happen with integers mixed with strings, but the idea was to do it for a list of strings only.
#get list of strings
puts "type words to make a list. type 'exit' to leave program."
x = ()
list = []
while x.to_s.upcase != 'EXIT'
x = gets.chomp
list.push(x)
end
list.pop
#binary method
nano = list.length
half= list.each_slice(nano/2).to_a
left = half[0]
right = half[1]
nanol=left.length
nanor=right.length
#initialize results array
A = []
for i in 0..nano-1
smallest_left = left.min
smallest_right = right.min
#no difference with this commented out or not
#if nanol==0
# A<<smallest_right
#end
#if nanor==0
# A<<smallest_left
#end
#error message points to the line below (rb:44)
if smallest_left<smallest_right
A << smallest_left
print A
left.pop[i]
elsif smallest_left>smallest_right
A << smallest_right
print A
right.pop[i]
else
print A
end
end
for input = ['z','b','r','a'] i can see the list being sorted in the error:
["a"]["a", "b"]["a", "b", "r"] rb:44:in `<': comparison of String with nil failed (ArgumentError)
please help me see my error :) Thanks in advance!
The exception is occurring because you are trying to compare nil. You get a different exception when nil is on the left.
'1' < nil
#=> scratch.rb:1:in `<': comparison of String with nil failed (ArgumentError)
nil > '1'
scratch.rb:1:in `<main>': undefined method `>' for nil:NilClass (NoMethodError)
Your code gets into this situation when the left or right array is empty (ie all of its elements have been added to A already). Presumably, this is why you had originally added the if-statements for nanol == 0 and nanor == 0 (ie to handle when one of the arrays is empty).
Your if-statements have a couple of issues:
You do need the nanol == 0 and nanor == 0 statements
The three if-statements are always run, even though only one would apply in an iteration
nanol and nanor are never re-calculated (ie they will never get to zero)
When the left and right values are equal, you don't actually add anything to the A array
The inside of your iteration should be:
smallest_left = left.min
smallest_right = right.min
nanol=left.length
nanor=right.length
if nanol == 0 #Handles left no longer having values
A << right.delete_at(right.index(smallest_right) || right.length)
elsif nanor == 0 #Handles right no longer having values
A << left.delete_at(left.index(smallest_left) || left.length)
elsif smallest_left < smallest_right
A << left.delete_at(left.index(smallest_left) || left.length)
elsif smallest_left > smallest_right
A << right.delete_at(right.index(smallest_right) || right.length)
else #They are equal so take one
A << left.delete_at(left.index(smallest_left) || left.length)
end
You will still have an issue (no error, but unexpected results) when your list has an odd number of elements. But hopefully that answers your question.

Resources