Time variable not being passed somewhere in these methods - ruby

There aren't any error messages showing up once the code is run, I've resolved all of those before posting here. This is just a simple greeting with a few methods, which takes the current time into account and asks for the user's name, but for some reason there's a hole where the time variable should be in the greeting. This is my Ruby code here:
def determine_current_hour
current_time = Time.new
current_hour = current_time.hour
end
def greeting(name)
current_hour = determine_current_hour
if(current_hour > 3 && current_hour < 12)
time = "morning"
elsif(current_hour > 12 && current_hour < 18)
time = "afternoon"
elsif(current_hour > 18 || current_hour < 2)
time = "evening"
end
puts "Good #{time}, #{name.capitalize}!"
end
puts "What is your name?"
name = gets
greeting(name)
And this is the resulting input and output:
What is your name?
Ruby
Good , Ruby
!
It seems airtight to me but I must be missing something. Any help would greatly be appreciated!

Your conditions do not account for the hour being 2, 3, 12 or 18 because they are all exclusive checks. In any of those hours you will see the output you describe.

As Greg Beech correctly points out, some hours are wrong because your conditions exclude them. For example, your first condition says current_hour < 12 and your second condition says current_hour > 12, but if current_hour is equal to 12 it meets neither of those conditions. To fix it, either replace the comparison operator in the first condition with <= or in the second with >=:
def greeting(name)
current_hour = determine_current_hour
if current_hour >= 3 && current_hour < 12
time = "morning"
elsif current_hour >= 12 && current_hour < 18
time = "afternoon"
elsif current_hour >= 18 || current_hour < 3
time = "evening"
end
puts "Good #{time}, #{name.capitalize}!"
end
This is pretty verbose, though. Might I suggest a more idiomatic approach using Ruby's versatile case construct?
def current_day_part
case Time.new.hour
when 3..11 then "morning"
when 12..17 then "afternoon"
else "evening"
end
end
def greeting(name)
puts "Good #{current_day_part}, #{name.capitalize}!"
end

Related

Ruby while loop keeps repeating regardless of input

I wrote a simple guess the number game. But it keeps looping even when I input the correct number. Please help, thanks!
puts "Pick a number between 0 - 1000."
user_guess = gets.chomp.to_i
my_num = rand(831)
guess_count = 0
until user_guess == my_num do
if user_guess == my_num
guess_count += 1
puts "you got it!"
elsif user_guess <= 830
guess_count += 1
puts "higher"
else user_guess >= 1000
guess_count += 1
puts "lower"
end
end
puts "You guessed my number in #{guess_count} attempts. Not bad"
The part of the code that asks for a number from the user is outside the loop, so it will not repeat after the answer is checked. If you want to ask the user to guess again when their guess is wrong, that code needs to be inside the loop.
my_num = rand(831)
guess_count = 0
keep_going = true
while keep_going do
puts "Pick a number between 0 - 1000."
user_guess = gets.chomp.to_i
if user_guess == my_num
guess_count += 1
puts "you got it!"
keep_going = false
elsif user_guess <= 830
guess_count += 1
puts "higher"
else user_guess >= 1000
guess_count += 1
puts "lower"
end
end
puts "You guessed my number in #{guess_count} attempts. Not bad"
This code still has some bugs in it that stops the game from working correctly though, see if you can spot what they are.
As #Tobias has answered your question I would like to take some time to suggest how you might make your code more Ruby-like.
Firstly, while you could use a while or until loop, I suggest you rely mainly on the method Kernel#loop for most loops you will write. This simply causes looping to continue within loop's block until the keyword break is encountered1. It is much like while true or until false (commonly used in some languages) but I think it reads better. More importantly, the use of loop protects computations within its block from prying eyes. (See the section Other considerations below for an example of this point.)
You can also exit loop's block by executing return or exit, but normally you will use break.
My second main suggestion is that for this type of problem you use a case statement rather than an if/elsif/else/end construct. Let's first do that using ranges.
Use a case statement with ranges
my_num = rand(831)
guess_count = 0
loop do
print "Pick a number between 0 and 830: "
guess_count += 1
case gets.chomp.to_i
when my_num
puts "you got it!"
break
when 0..my_num-1
puts "higher"
else
puts "lower"
end
end
There are a few things to note here.
I used print rather than puts so the user will enter their response on on the same line as the prompt.
guess_count is incremented regardless of the user's response so that can be done before the case statement is executed.
there is no need to assign the user's response (gets.chomp.to_i) to a variable.
case statements compare values with the appropriate case equality method ===.
With regard to the last point, here we are comparing an integer (gets.chomp.to_i) with another integer (my_num) and with a range (0..my_num-1). In the first instance, Integer#=== is used, which is equivalent to Integer#==. For ranges the method Range#=== is used.
Suppose, for example, that my_num = 100 and gets.chomp.to_i #=> 50 The case statement then reads as follows.
case 50
when 100
puts "you got it!"
break
when 0..99
puts "higher"
else
puts "lower"
end
Here we find that 100 == 50 #=> false and (0..99) === 50 #=> true, so puts "higher" is displayed. (0..99) === 50 returns true because the integer (on the right of ===) is covered by the range (on the left). That is not the same as 50 === (0..90), which loosely reads, "(0..99) is a member of 50", so false is returned.
Here are a couple more examples of how case statements can be used to advantage because of their reliance on the triple equality method.
case obj
when Integer
obj + 10
when String
obj.upcase
when Array
obj.reverse
...
end
case str
when /\A#/
puts "A comment"
when /\blaunch missiles\b/
big_red_button.push
...
end
Use a case statement with the spaceship operator <=>
The spaceship operator is used by Ruby's Array#sort and Enumerable#sort methods, but has other uses, as in case statements. Here we can use Integer#<=> to compare two integers.
my_num = rand(831)
guess_count = 0
loop do
print "Pick a number between 0 and 830: "
case gets.chomp.to_i <=> my_num
when 0
puts "you got it!"
break
when -1
puts "higher"
else # 1
puts "lower"
end
end
In other applications the spaceship operator might be used to compare strings (String#<=>), arrays (Array#<=>), Date objects (Date#<=>) and so on.
Use a hash
Hashes can often be used as an alternative to case statements. Here we could write the following.
response = { -1=>"higher", 0=>"you got it!", 1=>"lower" }
my_num = rand(831)
guess_count = 0
loop do
print "Pick a number between 0 and 830: "
guess = gets.chomp.to_i
puts response[guess <=> my_num]
break if guess == my_num
end
Here we need the value of gets.chomp.to_i twice, so I've saved it to a variable.
Other considerations
Suppose we write the following:
i = 0
while i < 5
i += 1
j = i
end
j #=> 5
j following the loop is found to equal 5.
If we instead use loop:
i = 0
loop do
i += 1
j = i
break if i == 5
end
j #=> NameError (undefined local variable or method 'j')
Although while and loop both have access to i, but loop confines the values of local variables created in its block to the block. That's because blocks create a new scope, which is good coding practice. while and until do not use blocks. We generally don't want code following the loop to have access to local variables created within the loop, which is one reason for favouring loop over while and until.
Lastly, the keyword break can also be used with an argument whose value is returned by loop. For example:
def m
i = 0
loop do
i += 1
break 5*i if i == 10
end
end
m #=> 50
or
i = 0
n = loop do
i += 1
break 5*i if i == 10
end
n #=> 50
1. If you examine the doc for Kernel#loop you will see that executing break from within loop's block is equivalent to raising a StopIteration exception.

Ruby OOP correct concept?

The exercise questions are below with my answers.
#Create a Tree class with a rings attribute and getter method.
#Trees create a ring for every winter that passes
#It should have a bear_fruit? method which should return true if the
#has fruit that year. the tree produces fruit when it has
#more than 7 rings but less than 15, but false otherwise.
#The class should also have an winter_season method that increases #rings attr by 1.
Can anyone give me constructive criticism on this code?
class Tree
attr_accessor :winters, :rings, :bear_fruit?
def initialize(winters, rings)
#winters = winters
#rings = rings
end
def rings_created
#winters = 0
#rings = 0
while #winters == #rings do
#winters +=1
#rings +=1
break if #winters == 100
end
end
end
def bear_fruit
if #rings > 6 || < 16
#bear_fruit? = true
else
#bear_fruit? = false
end
end
def winter_season
#winters = 0
#rings = 0
while #winters < #rings do
#winters +=1
#rings +=2
break if #winters == 100
end
end
end
end
According to the exercise, you are supposed to create a class Tree with a single attribute rings and two methods, bear_fruit? and winter_season:
Create a Tree class with
a rings attribute and getter method
a bear_fruit? method which
returns true if the tree has more than 7 rings but less than 15
returns false otherwise
a winter_season method that
increases rings by 1
That's all. It doesn't say that a tree should track winters and it doesn't mention any loops.
Here's how I would implement it:
class Tree
attr_reader :rings
def initialize
#rings = 0
end
def bear_fruit?
#rings > 7 && #rings < 15
end
def winter_season
#rings += 1
end
end
First, does it work? I'm guessing not. Run it and see what the error is.
Ruby provides a number of ways of looping which you can look up in the ruby docs. I prefer not to use while loops if I can avoid it, partly because it can lead to less readable code with the use of break. Look up the times method and other enumerables.

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

Timer RSpec Test Ruby

I am trying to solve the timer problem from TestFirst Ruby.
I got the first two criteria correctly but the third one when tested for time = 12 seconds does not work. It does not look like Ruby is reading the time = 12 secs.
The codes are pretty lazy and not optimized, apparently. Also I did tried out the padded method but the test never worked. I had my padded method defined as
def padded(num)
if num<=9
return "0"<<num.to_s
else
return num.to_s
end
end
It would be great if someone can show me how to set that up correctly since that might have been the problems.
Here are my complete codes:
class Timer
#initialization of seconds
def seconds
return 0
end
#seconds= method
def seconds=(time)
#seconds = time_string(time)
end
#time_string method
def time_string(time=0)
#format of hour:minute:second
#minute must be less than 59 (or 59*60 seconds), otherwise it will convert to hour
minute = time/60 #note that this is integer math, so it will take the minute and not the remainder
hour = minute/60
remainder_seconds = time%60
if time<=9
return "00:00:0" << time.to_s
elsif time>9 && time<=60
return "00:00:" << time.to_s
elsif time>60 && time<=9*60 #9 minutes and greater than 1 min
#ensuring double XX seconds or 0X seconds (this would be easier to use the padded method)
if remainder_seconds >9
remainder_seconds_sd = remainder_seconds.to_s
else
remainder_seconds_sd = "0" << remainder_seconds.to_s
end
return "00:0" << minute.to_s << ":" << remainder_seconds_sd
end
end
end
RSpec below:
require '09_timer'
describe "Timer" do
before(:each) do
#timer = Timer.new
end
it "should initialize to 0 seconds" do
#timer.seconds.should == 0
end
describe 'time_string' do
it "should display 0 seconds as 00:00:00" do
#timer.seconds = 0
#timer.time_string.should == "00:00:00"
end
it "should display 12 seconds as 00:00:12" do
#timer.seconds = 12
#timer.time_string.should == "00:00:12"
end
it "should display 66 seconds as 00:01:06" do
#timer.seconds = 66
#timer.time_string.should == "00:01:06"
end
it "should display 4000 seconds as 01:06:40" do
#timer.seconds = 4000
#timer.time_string.should == "01:06:40"
end
end
# One way to implement the Timer is with a helper method.
# Uncomment these specs if you want to test-drive that
# method, then call that method from inside of time_string.
#
=begin
describe 'padded' do
it 'pads zero' do
#timer.padded(0).should == '00'
end
it 'pads one' do
#timer.padded(1).should == '01'
end
it "doesn't pad a two-digit number" do
#timer.padded(12).should == '12'
end
end
=end
end
The problem with your tests and Timer is that, in your tests you are setting the value of #timer.seconds, but the Timer#time_string does not rely on the #seconds variable set. Your time_string method is implemented the way it accepts the amount of seconds as an argument, not an attribute of Timer.
Try changing your tests as follows:
describe "Timer" do
# rest of your code
describe 'time_string' do
it "should display 0 seconds as 00:00:00" do
#timer.time_string(0).should == "00:00:00"
end
it "should display 12 seconds as 00:00:12" do
#timer.time_string(12).should == "00:00:12"
end
it "should display 66 seconds as 00:01:06" do
#timer.time_string(66).should == "00:01:06"
end
it "should display 4000 seconds as 01:06:40" do
#timer.time_string(4000).should == "01:06:40"
end
end
end
You might be wondering okay, but why the first test - 00:00:00 - did work in first place?. Well, this is, because your time_string method argument defaults to 0:
def time_string(time=0)
# Rest of the code
end
and because you were not passing any other value, the 0 has been used.
If you have any questions - I'm happy to help!
Good luck!
Edit
If you want to make it the other way around - make the class to work for your tests, change your Timer class:
class Timer
def initialize
#seconds = 0
end
def seconds
#seconds
end
def seconds=(time)
#seconds = time
end
def time_string
#format of hour:minute:second
#minute must be less than 59 (or 59*60 seconds), otherwise it will convert to hour
minute = #seconds/60 #note that this is integer math, so it will take the minute and not the remainder
hour = minute/60
remainder_seconds = #seconds%60
if #seconds<=9
return "00:00:0" << #seconds.to_s
elsif #seconds>9 && #seconds<=60
return "00:00:" << #seconds.to_s
elsif #seconds>60 && #seconds<=9*60 #9 minutes and greater than 1 min
#ensuring double XX seconds or 0X seconds (this would be easier to use the padded method)
if remainder_seconds >9
remainder_seconds_sd = remainder_seconds.to_s
else
remainder_seconds_sd = "0" << remainder_seconds.to_s
end
return "00:0" << minute.to_s << ":" << remainder_seconds_sd
end
end
end
We have added initialize method, we have changed def seconds=(time) method, and we have changed all occurrences of time in your time_string method.
If that works for you, consider posting the code to https://codereview.stackexchange.com/. There is a lot in the code to improve, and codereview is a great place to ask for help!
A cleaner version:
class Timer
attr_accessor :seconds
def initialize
#seconds = 0
end
def time_string
seconds = #seconds % 60
minutes = (#seconds / 60) % 60
hours = #seconds / (60**2)
"#{padded(hours)}:#{padded(minutes)}:#{padded(seconds)}"
end
def padded(num)
return '0' + num.to_s if num < 10
return num.to_s if num >= 10
end
end

Ruby: intersection between two ranges

In ruby, given two date ranges, I want the range that represents the intersection of the two date ranges, or nil if no intersection. For example:
(Date.new(2011,1,1)..Date.new(2011,1,15)) & (Date.new(2011,1,10)..Date.new(2011,2,15))
=> Mon, 10 Jan 2011..Sat, 15 Jan 2011
Edit: Should have said that I want it to work for DateTime as well, so interval can be down to mins and secs:
(DateTime.new(2011,1,1,22,45)..Date.new(2011,2,15)) & (Date.new(2011,1,1)..Date.new(2011,2,15))
=> Sat, 01 Jan 2011 22:45:00 +0000..Tue, 15 Feb 2011
require 'date'
class Range
def intersection(other)
return nil if (self.max < other.begin or other.max < self.begin)
[self.begin, other.begin].max..[self.max, other.max].min
end
alias_method :&, :intersection
end
p (Date.new(2011,1,1)..Date.new(2011,1,15)) & (Date.new(2011,1,10)..Date.new(2011,2,15))
#<Date: 2011-01-10 ((2455572j,0s,0n),+0s,2299161j)>..#<Date: 2011-01-15 ((2455577j,0s,0n),+0s,2299161j)>
You can try this to get a range representing intersection
range1 = Date.new(2011,12,1)..Date.new(2011,12,10)
range2 = Date.new(2011,12,4)..Date.new(2011,12,12)
inters = range1.to_a & range2.to_a
intersected_range = inters.min..inters.max
Converting your example:
class Range
def intersection(other)
raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range)
inters = self.to_a & other.to_a
inters.empty? ? nil : inters.min..inters.max
end
alias_method :&, :intersection
end
I found this: http://www.postal-code.com/binarycode/2009/06/06/better-range-intersection-in-ruby/ which is a pretty good start, but does not work for dates. I've tweaked a bit into this:
class Range
def intersection(other)
raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range)
new_min = self.cover?(other.min) ? other.min : other.cover?(min) ? min : nil
new_max = self.cover?(other.max) ? other.max : other.cover?(max) ? max : nil
new_min && new_max ? new_min..new_max : nil
end
alias_method :&, :intersection
end
I've omitted the tests, but they are basically the tests from the post above changed for dates. This works for ruby 1.9.2.
Anyone got a better solution?
I baked this solution for ascending ranges, also taking care of the exclude end situations:
intersect_ranges = ->(r1, r2) do
new_end = [r1.end, r2.end].min
new_begin = [r1.begin, r2.begin].max
exclude_end = (r2.exclude_end? && new_end == r2.end) || (r1.exclude_end? && new_end == r1.end)
valid = (new_begin <= new_end && !exclude_end)
valid ||= (new_begin < new_end && exclude_end))
valid ? Range.new(new_begin, new_end, exclude_end) : nil
end
I'm also a bit worried by you guys adding it to the Range class itself, since the behavior of intersecting ranges is not uniformly defined.
(How about intersecting 1...4 and 4...1? Why nil when there is no intersection; we could also say this is an empty range: 1...1 )
You can use overlaps? with Range starting with Rails v3
# For dates, make sure you have the correct format
first_range = first_start.to_date..first_end.to_date
second_range = second_start.to_date..second_end.to_date
intersection = first_range.overlaps?(second_range) # => Boolean
# Example with numbers
(1..7).overlaps?(3..5) # => true
More details in the docs
Try something like this
require 'date'
sample = Date.parse('2011-01-01')
sample1 = Date.parse('2011-01-15')
sample2 = Date.parse('2010-12-19')
sample3 = Date.parse('2011-01-11')
puts (sample..sample1).to_a & (sample2..sample3).to_a
What this will give you is a array of intersection dates!!
I have times as [[start, end], ...] and I want to remove the some time ranges from a each initial time range, here is what I did:
def exclude_intersecting_time_ranges(initial_times, other_times)
initial_times.map { |initial_time|
other_times.each do |other_time|
next unless initial_time
# Other started after initial ended
next if other_time.first >= initial_time.last
# Other ended before initial started
next if other_time.last <= initial_time.first
# if other time started before and ended after after, no hour is counted
if other_time.first <= initial_time.first && other_time.last >= initial_time.last
initial_time = nil
# if other time range is inside initial time range, split in two time ranges
elsif initial_time.first < other_time.first && initial_time.last > other_time.last
initial_times.push([other_time.last, initial_time.last])
initial_time = [initial_time.first, other_time.first]
# if start time of other time range is before initial time range
elsif other_time.first <= initial_time.first
initial_time = [other_time.last, initial_time.last]
# if end time of other time range if after initial time range
elsif other_time.last >= initial_time.last
initial_time = [initial_time.first, other_time.first]
end
end
initial_time
}.compact
end
Since this question is related to How to combine overlapping time ranges (time ranges union), I also wanted to post my finding of the gem range_operators here, because if helped me in the same situation.
I'd transfer them into an array, since arrays know the intersection-operation:
(Date.new(2011,1,1)..Date.new(2011,1,15)).to_a & (Date.new(2011,1,10)..Date.new(2011,2,15)).to_a
Of course this returns an Array. So if you want an Enumerator (Range doesn't seem to be possible since these are not consecutive values anymore) just throw to_enum at the end.

Resources