Ruby OOP beginner here. Trying to build a simple Vending machine code.
class VendingMachine
# TODO: add relevant getter/setter to this class to make the scenarios work properly.
attr_reader :snack_price_cents, :user_balance_cents
attr_accessor :snack_count
def initialize(snack_price_cents, snack_count)
#user_balance_cents = 0
#snack_count = snack_count
#snack_price_cents = snack_price_cents
end
def insert_coin(input_cents)
#user_balance_cents = user_balance_cents + input_cents if input_cents
end
def buy_snack
if snack_count.zero? || user_balance_cents.zero?
#snack_count = snack_count
else
#snack_count = snack_count - 1
#user_balance_cents = user_balance_cents - snack_price_cents
end
end
end
I am trying to understand what happens to snack_count, user_balance_cents and snack_price_cents when the user pushes a button to buy a snack?
It seems like all is working okay except for the user_balance_cents but I am getting:
should not let you buy a snack if you didn't insert enough money (error path) (FAILED - 1)"
error. Any help?
I would guess that your error here is that you are checking that user_balance_cents is not zero, but you are not checking that it as least snack_price_cents.
i.e. if I put 10c in and try to buy a 50c snack, it will give me it.
Related
I have the following Ruby code:
module BigTime
FOO1_MONEY_PIT = 500
FOO2_MONEY_PIT = 501
class LoseMoney
##SiteName = 'FOO1'
#site_num = ##SiteName_MONEY_PIT
def other_unimportant_stuff
whatever
end
end
end
So, what I'm trying to do here is set the SiteName and then use SiteName and combine it with the string _MONEY_PIT so I can access FOO1_MONEY_PIT and store its contents (500 in this case) in #site_num. Of course, the above code doesn't work, but there must be a way I can do this?
Thanks!!
If you want to dynamically get the value of a constant, you can use Module#const_get:
module BigTime
FOO1_MONEY_PIT = 500
FOO2_MONEY_PIT = 501
class LoseMoney
##SiteName = 'FOO1'
#site_num = BigTime.const_get(:"#{##SiteName}_MONEY_PIT")
end
end
Do not, under any circumstance, use Kernel#eval for this. Kernel#eval is extremely dangerous in any context where there is even the slightest possibility that an attacker may be able to control parts of the argument.
For example, if a user can choose the name of the site, and they name their site require 'fileutils'; FileUtils.rm_rf('/'), then Ruby will happily evaluate that code, just like you told it to!
Kernel#eval is very dangerous and you should not get into the habit of just throwing an eval at a problem. It is a very specialized tool that should only be employed when there is no other option (spoiler alert: there almost always is another option), and only after a thorough security review.
Please note that dynamically constructing variable names is already a code smell by itself, regardless of whether you use eval or not. It pretty much always points to a design flaw somewhere. In general, you can almost guaranteed replace the multiple variables with a data structure. E.g. in this case something like this:
module BigTime
MONEY_PITS = {
'FOO1' => 500,
'FOO2' => 501,
}.freeze
class LoseMoney
##SiteName = 'FOO1'
#site_num = MONEY_PITS[##SiteName]
end
end
You can refactor this as to use a Hash for your name lookups, and a getter method to retrieve it for easy testing/validation. For example:
module BigTime
MONEY_PITS = { FOO1: 500, FOO2: 501 }
MONEY_PIT_SUFFIX = '_MONEY_PIT'
class LoseMoney
##site = :FOO1
def initialize
site_name
end
def site_name
#site_name ||= '%d%s' % [MONEY_PITS[##site], MONEY_PIT_SUFFIX]
end
end
end
BigTime::LoseMoney.new.site_name
#=> "500_MONEY_PIT"
I'm new to programming in Ruby.
How do I make the output show Revenue and Profit or Loss?
How can I refactor the following code to look neater? I know it's wrong but I have no idea how to take my if profit out of the initialize method.
class Theater
attr_accessor :ticket_price, :number_of_attendees, :revenue, :cost
def initialize
puts "What is your selling price of the ticket?"
#ticket_price = gets.chomp.to_i
puts "How many audience are there?"
#number_of_attendees = gets.chomp.to_i
#revenue = (#number_of_attendees * #ticket_price)
#cost = (#number_of_attendees * 3) + 180
#profit = (#revenue - #cost)
if #profit > 0
puts "Profit made: $#{#profit}"
else
puts "Loss incurred: $#{#profit.abs}"
end
end
end
theater = Theater.new
# theater.profit
# puts "Revenue for the theater is RM#{theater.revenue}."
# I hope to put my Profit/Loss here
#
# puts theater.revenue
Thanks guys.
Do not initialize the object with input from the user, make your object accept the needed values. Make a method to read the needed input and return you new Theater. Last of all put the if in separate method like #report_profit.
Remember constructors are for setting up the initial state of the object, making sure it is in a valid state. The constructor should not have side effects(in your case system input/output). This is something to be aware for all programming languages, not just ruby.
Try this:
class Theatre
COST = { running: 3, fixed: 180 }
attr_accessor :number_of_audience, :ticket_price
def revenue
#number_of_audience * #ticket_price
end
def total_cost
COST[:fixed] + (#number_of_audience * COST[:running])
end
def net
revenue - total_cost
end
def profit?
net > 0
end
end
class TheatreCLI
def initialize
#theatre = Theatre.new
end
def seek_number_of_attendes
print 'Number of audience: '
#theatre.number_of_audience = gets.chomp.to_i
end
def seek_ticket_price
print 'Ticket price: '
#theatre.ticket_price = gets.chomp.to_i
end
def print_revenue
puts "Revenue for the theatre is RM #{#theatre.revenue}."
end
def print_profit
message_prefix = #theatre.profit? ? 'Profit made' : 'Loss incurred'
puts "#{message_prefix} #{#theatre.net.abs}."
end
def self.run
TheatreCLI.new.instance_eval do
seek_ticket_price
seek_number_of_attendes
print_revenue
print_profit
end
end
end
TheatreCLI.run
Notes:
Never use your constructor (initialize method) for anything other than initial setup.
Try to keep all methods under 5 lines.
Always try to keep each class handle a single responsibility; for instance, printing and formatting output is not something the Theatre class needs to care.
Try extracting all hard coded values; eg see the COST hash.
Use apt variables consistent to the domain. Eg: net instead of profit makes the intent clear.
I'm saving an object as such:
#rentalrequest = RentalRequest.new do |rr|
rr.delivery_start = Time.zone.parse(request_params[:deliverydate] + " " + request_params[:deliverytime_start]).utc
...
end
Every once in a while, my front end validation fails, and somehow, the form is posted even though deliverydate and deliverytime_start are blank. In this case, the controller breaks with a NoMethodError because this statement doesn't make sense:
Time.zone.parse("")
However, rather than having to write a rescue for when this happens, I feel like it's so much easier if I can just say rr.delivery_start = nil if Time.zone.parse doesn't work. That way, the back end validation on the #rentalrequest object kicks in and serves as a rescue.
But I'm not sure how to write the rr.delivery_start = nil if Time.zone.parse doesn't work (like... if any part of it doesn't work)
Thoughts?
How about checking if those params exist instead?
#rentalrequest = RentalRequest.new do |rr|
rr.delivery_start = nil
if request_params[:deliverydate].present? && request_params[:deliverytime_start].present?
rr.delivery_start = Time.zone.parse(request_params[:deliverydate] + " " + request_params[:deliverytime_start]).utc
end
...
end
Your calculation is in the wrong place, your model should be maintaining its own state by itself. Something like this perhaps:
class RentalRequest < ActiveRecord::Base
before_validation :set_delivery_start
private
def set_delivery_start
# Presumably validations will catch these conditions...
return if(!deliverydate || !deliverytime_start)
self.delivery_start = ... # whatever calculation matches the deliverydate and delivertime_start types goes here
end
end
and then you'd have validations to ensure that all three delivery values made sense.
You can require those parameters if new block is in controller.
def request_params
params
.require(:rental_request)
.permit(..., :deliverydate, :deliverytime_start)
.require(:deliverydate, :deliverytime_start)
end
Forgive me if my question isn't completely clear. I have been awake for way too long and I'm feeling a little brain dead.
I'm doing a Ruby exercise and I can't figure out why my rspec test isn't passing for something I thought would work.
require 'date'
class Product
attr_accessor :photo_src, :promotion, :initial_date
attr_reader :default_photo, :default_price, :current_price
def initialize(name, photo, price)
#name = name
#default_photo = photo
#photo_src = photo
#default_price = price
#current_price = price
#initial_date = Date.today.yday
#promotion = false
end
def price_change(sale_price)
calculator = RedPencilCalculator.new(self)
if promotion
if sale_price > #current_price
calculator.end_promotion!
elsif sale_price < (#default_price - (#default_price * 0.3))
calculator.end_promotion!
end
else
calculator.start_promotion!
end
#current_price = sale_price
end
end
class RedPencilCalculator
attr_accessor :promotion_start, :product
def initialize(product)
#product = product
end
def start_promotion!
if start_promotion?
product.promotion = true
product.photo_src = "redX.png"
#promotion_start = Date.today.yday
end
end
#would need to run daily
def end_promotion?
promotion_duration
if #duration == 30 || #duration == 335
end_promotion!
end
end
def end_promotion!
product.promotion = false
product.photo_src = product.default_photo
product.initial_date = Date.today.yday
end
private
def calculate_range
#min_discount = product.default_price - (product.default_price * 0.05)
#max_discount = product.default_price - (product.default_price * 0.3)
end
def start_promotion?
calculate_range
#max_discount <= product.current_price && product.current_price <= #min_discount && Date.today.yday - product.initial_date >= 30
end
def promotion_duration
current_date = Date.today.yday
#duration = current_date - #promotion_start
end
end
Rspec
This doesn't work:
describe Product do
let(:shoes) { Product.new("shoes", "item.png", 100) }
it 'should change the photo_src and promotion attribute if applicable' do
allow(shoes).to receive(:initial_date) { 100 }
shoes.price_change(75)
expect(shoes.promotion).to eq(true)
expect(shoes.photo_src).to eq("redX.png")
end
end
This does:
describe Product do
let(:shoes) { Product.new("shoes", "item.png", 100) }
let(:calculator) { RedPencilCalculator.new(shoes) }
it 'should change the photo_src and promotion attribute if applicable' do
allow(shoes).to receive(:initial_date) { 100 }
shoes.price_change(75)
calculator.start_promotion!
expect(shoes.promotion).to eq(true)
expect(shoes.photo_src).to eq("redX.png")
end
end
So it seems to me that the start_promotion! method call in the price_change method just isn't working.
I don't have a specific answer to your bug but some suggestions on how to pinpoint the problem.
You're testing too much in one unit test. There's so much that can go wrong it's hard (as you've found) to track down where the bug lies. Even if you work it out now, when something changes down the track (as it inevitably will) it will be at least as difficult as it is now to debug.
Simplify the initializer. It should only set #name, #photo, #price. The other instance variables should be methods (write tests unless they're private).
You suspect RedPencilCalculator#start_promotion! has a bug. Write a test to eliminate that possibility.
With more tests in place, the bug will eventually be cornered and crushed!
Lastly - this is easier said than done - but try writing tests first. It is hard but gets easier and even enjoyable!
ok, I put a puts inside of start_promotion? like this:
p "got past calc range: #{#max_discount.inspect} and #{#min_discount.inspect} and #{product.current_price}"
and got:
"got past calc range: 70.0 and 95.0 and 100"
given that the following line checks that current-price is less than the min-discount...
that's the line you've gotta check/fix to make things work
im trying to optimize my code as much as possible and i've reached a dead end.
my code looks like this:
class Person
attr_accessor :age
def initialize(age)
#age = age
end
end
people = [Person.new(10), Person.new(20), Person.new(30)]
newperson1 = [Person.new(10)]
newperson2 = [Person.new(20)]
newperson3 = [Person.new(30)]
Is there a way where i can get ruby to automatically pull data out from the people array and name them as following newperson1 and so on..
Best regards
That is definitely a code smell. You should refer to them as [people[0]], [people[1]], ... .
But if you insist on doing so, and if you can wait until December 25 (Ruby 2.1), then you can do:
people.each.with_index(1) do |person, i|
binding.local_variable_set("newperson#{i}", [person])
end
I think this is what you're trying to do...
newperson1 = people[0]
puts newperson1.age
The output of this 10 as expected.