Skip to content

Instantly share code, notes, and snippets.

@bbuck
Created April 22, 2016 05:32
Show Gist options
  • Save bbuck/09aea975bdd83efdc313a528cba15e51 to your computer and use it in GitHub Desktop.
Save bbuck/09aea975bdd83efdc313a528cba15e51 to your computer and use it in GitHub Desktop.
# This is the challenge -- you should figure out what you
# don't know here.
class Calculator
class << self
def exit_command(cmd = nil)
if cmd.nil?
@exit_command || "exit"
else
@exit_command = cmd
end
end
def tasks
@tasks ||= {}
end
def define_task(name, &block)
tasks[name.to_sym] = block
end
def perform(task_name, args)
tasks[task_name].call(*args)
end
def run
loop do
task = ask_user_for_task
break if task.nil?
numbers = ask_user_for_numbers
puts perform(task.to_sym, numbers)
end
end
def ask_user_for_task
loop do
print "Please select one of the following options (or '#{exit_command}' to quit)\n#{tasks.keys.join(", ")}\n >> "
task = gets.chomp.to_sym
if tasks.has_key?(task)
break task
elsif task.to_s == exit_command
break
else
puts "Invalid response\n"
end
end
end
def ask_user_for_numbers
[].tap do |numbers|
2.times do
print "Enter a number >> "
numbers << gets.to_i
end
end
end
end
end
Calculator.define_task("add") do |a, b|
"#{a} + #{b} = #{a + b}"
end
Calculator.define_task("sub") do |a, b|
"#{a} - #{b} = #{a - b}"
end
Calculator.define_task("mul") do |a, b|
"#{a} * #{b} = #{a * b}"
end
Calculator.run
# It's not _as_ magical as I intended... but it's still pretty neat.
# First and foremost, you probably want to use a class and or module for some
# of this. I imagine you probably have not learned much about these constructs
# here.
# Your naming needs work. The primary goal of Ruby code as I've seen it is
# readability. You want things to be named in such a way that they really just
# imply exactly what they're doing immediately. You're off to a good start,
# but maybe `ask_for_task`, `prompt_for_task` or something a bit more readable
# (as a sentence).
#
# Another note, this method does more than ask for a task. This is usually a
# bad sign. Now you can find me preaching _against_ strict adherence to the
# single responsiblity principle but in some cases it's a good idea. You don't
# want changes to your input fetching also working closely to your input
# processing.
def ask_task
# People have mentioned it, but this idiom is better with a `loop do`
# construct since continues _only_ purpose is to break out of the loop and
# (most langauges have it) there is a keyword to do this actual thing called
# `break`.
continue = true
until !continue
# This was the first "major" issue I had with your code. Ruby lets you
# blindly do things like this without realizing what you've done. And this
# is an issue I have with the language. You'll just have to learn to watch
# for moments like this -- so what's so wrong here? You're creating a new
# Array instance every loop iteration along with three new String instances
# as well, check this output:
#
# [70335426081500, 70335426081560, 70335426081540, 70335426081520]
# [70335426081120, 70335426081200, 70335426081180, 70335426081140]
# [70335426080880, 70335426080940, 70335426080920, 70335426080900]
# [70335426080600, 70335426080700, 70335426080660, 70335426080640]
# [70335426080300, 70335426080360, 70335426080340, 70335426080320]
# [70335426079980, 70335426080060, 70335426080020, 70335426080000]
# [70335426079700, 70335426079760, 70335426079740, 70335426079720]
# [70335426079380, 70335426079460, 70335426079440, 70335426079400]
# [70335426079040, 70335426079100, 70335426079080, 70335426079060]
# [70335426078540, 70335426078600, 70335426078580, 70335426078560]
#
# I ran this code to generate that:
#
# 10.times do
# arr = ["one", "two", "three"]
# ids = arr.map do |i|
# i.object_id
# end
# ids.unshift(arr)
# p ids
# end
#
# So you can see, from this list of object ID, the Array and all strings
# have different object IDs (which is auto assigned to instances in Ruby)
# so you can see you're creating these _every_ iteration. It's best to avoid
# this. You probably should move this outside the loop or a constant.
tasks = ["add", "subtract", "multiply"]
# If you're ending with \n probably best to use `puts` which will handle
# this for you.
print "\nWhat would you like to do? (type 'exit' to quit the program)\n"
# So the name `string` for a String means nothing. I literally had no idea
# what you were doing with this until I continued reading. Now, while this
# isn't always preventable -- in this case it could have been. `task_string`
# or `task_list` or something _other_ than it's type.
string = ""
# It's been driven in the ground already, but for the sake of providing my
# opinions for you I'm going to repeat it. This is better as #join which
# optionally takes a string ot join the values in the list on (by default
# it's `""`).
tasks.each do |task|
string += "#{task}, "
end
# If you have a string and you're not modifying then you shouldn't interpolate
# it into another string. This line is equivalen to `puts "" + string` which
# creates an instance of a string that isn't even necessary! Again, you're
# going to want to keep an eye on situations like these.
puts "#{string}"
# It's been pointed out before you don't need this variable, but I'd like
# to mention there are better names than `answer`. Answer to what? The user
# gave you a task and you asked for a task. This is probably a task, and
# naming it `task` would be much more clear.
answer = gets.chomp
# just to reiterate here, the best case for this is avoid the answer variable
# and go with `gets.chomp.downcase`
case answer.downcase
# I'm chalking this one up to lack of experience with Ruby, but this doesn't
# do what you expect. Inputting "e" doesn't fire this case (I'm actually
# surprised this even worked for "exit", to be honest). The way you should
# structure this so it works as you expect is with commas!
#
# when "exit", "e", "quit"
when "exit" || "e" || "quit"
continue = false
when "add"
# These methods are repetitive. Notice anything? All three of them fit
# this exact format: `calc_{task_name}(ask_numbers)`
#
# Anytime you find repition you probably have the ability to refactor to
# reduce code.
puts calc_add(ask_numbers)
when "subtract"
puts calc_subtract(ask_numbers)
when "multiply"
puts calc_multiply(ask_numbers)
else
puts "Sorry, try again"
end
end
end
# Naming again here. What about numbers are you asking? `ask_for_numbers`?
def ask_numbers
# So, again, already run into the ground, but this should be `2.times do`
i = 0
numbers = []
print "\nType two numbers\n\n"
while i < 2
print "Enter a number: "
# Again, don't need answer here, you an push this directly into the array.
answer = gets.chomp.to_i
# While using #push on an array is perfeclty legal and there is nothing
# wrong with it. The more common method to use here is `<<` so:
#
# numbers << gets.to_i
numbers.push(answer)
i += 1
end
# You can write this without need for this -- I'll not discuss it comments
# but in my "here's a rewrite" version.
return numbers
end
# The name `array` here is another "name it what it is" example. Instead of
# calling it `array` I'm really suprised you didn't stick with `numbers`. There
# are two approach here, the first as mentioned is using a splat operator and
# defining this as a method that takes two arguments.
def calc_add(array)
a = array.first
b = array.last
c = (a + b)
answer = "#{a} + #{b} = #{c}"
return answer
end
def calc_subtract(array)
a = array.first
b = array.last
c = (a - b)
answer = "#{a} - #{b} = #{c}"
return answer
end
def calc_multiply(array)
a = array.first
b = array.last
c = (a * b)
answer = "#{a} * #{b} = #{c}"
return answer
end
puts ask_task
# We start with a class that namespaces our work.
class Calculator
# Here I define this globally. I could have done the fancy %w() blah blah
# which I realize a lot of people prefer. But I prefer to keep things
# representative of what's going on and %w() never screams "I'm creating an
# array here."
TASKS = ["add", "sub", "mul"]
# This is magical. I'm going to way oversimplify this to explain it for now,
# I highly suggest you look it up to get a real idea of what is going on here.
# For all intents and purposes this is a way to define class methods in such
# a way you that you don't have to prefix everything with `self.` and you
# can use standard instance variables as class level values since Ruby's
# `@@` variables are extraordinarily odd (you'll find out eventually).
class << self
# Double parenthisis!! Actually, I'm deconstructing the array with this.
# The magic here is that calling this `Calculator.add([1, 2])` assigns
# 1 == a and 2 == b.
def add((a, b))
# Notice I didn't use `return`. In Ruby the best practice is to avoid
# using an explicity return as much as possible.
"#{a} + #{b} = #{a + b}"
end
def sub((a, b))
"#{a} - #{b} = #{a - b}"
end
def mul((a, b))
"#{a} * #{b} = #{a * b}"
end
def run
loop do
task = ask_user_for_task
# I used a naked return to return nil as the task here. So now I'm
# testing if the task was nil and breaking out of this loop and
# exiting the program.
break if task.nil?
numbers = ask_user_for_numbers
puts send(task, numbers)
end
end
def ask_user_for_task
loop do
print "Please select one of the following options (or 'exit' to quit)\n#{TASKS.join(", ")}\n >> "
task = gets.chomp
if TASKS.include?(task)
# Why break instead of return? In Ruby you can break with values.
# And all things in Ruby are expressions (meaning they return values)
# which includes a `loop` which will return the value that I break
# with. And since `loop` is the last expression in this method it's
# return value is automatically going to be returned from this method.
# Yay!
break task
elsif task == "exit"
break
else
puts "Invalid response\n"
end
end
end
def ask_user_for_numbers
# Enter #tap, just as I promised. This is a "magical" method that can be
# called on an object. It takes a block (the do thing after the method
# call) and the block takes the object as it's parameter. Inside you can
# transform the object as you see fit and the return from tap is -- again
# the return object.
[].tap do |numbers|
2.times do
print "Enter a number >> "
numbers << gets.to_i
end
end
end
end
end
Calculator.run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment