Simple Calculator
Solution 1
class SimpleCalculator
ALLOWED_OPS = ['+', '/', '*'].freeze
OPS = {
'+' => proc { |a, b| a + b },
'*' => proc { |a, b| a * b },
'/' => proc { |a, b| a / b },
}
class UnsupportedOperation < StandardError; end
def self.calculate(a, b, op)
raise ArgumentError.new('Operands must be integers.') unless
[a, b].all? { |v| v.is_a?(Integer) }
raise UnsupportedOperation.new unless
OPS.member?(op)
begin
'%s %s %s = %s' % [a, op, b, OPS[op].call(a, b)]
rescue ZeroDivisionError => err
'Division by zero is not allowed.'
end
end
end
Solution 2
This solution uses more meaningful naming for parameters and variables.
It also creates a better error class for unsupported operations which extends from ArgumentError
, since the operation is an parameter to the method.
It also provides some aliases like OPS
and OP
but based on more lengthy, descriptive names.
OP
abbreviation is made private so consumers don’t use at will (but they can use the non-abbreviated name).
The String#%
format uses annotated specifiers for clarity.
module SimpleCalculatorExceptions
class UnsupportedOperation < ArgumentError
def initialize(msg = 'A valid operation must be provided.')
super
end
end
end
class SimpleCalculator
include SimpleCalculatorExceptions
OPS = ALLOWED_OPERATIONS = ['+', '/', '*'].freeze
OP = OPERATION = {
'+' => ->(a, b) { a + b },
'*' => ->(a, b) { a * b },
'/' => ->(a, b) { a / b },
}
private_constant :OP
def self.calculate(left_operand, right_operand, operator)
raise ArgumentError.new('Operands must be integers.') unless
[left_operand, right_operand].all? { |operand| operand.is_a?(Integer) }
raise UnsupportedOperation unless ALLOWED_OPERATIONS.member?(operator)
begin
'%{left_operand} %{operator} %{right_operand} = %{result}' %
{
left_operand: left_operand,
operator: operator,
right_operand: right_operand,
result: OP[operator].call(left_operand, right_operand),
}
rescue ZeroDivisionError => err
'Division by zero is not allowed.'
end
end
end
Solution 3
##
# Improvements made from code review and mentoring from the @kotp.
##
module CalculatorExceptions
class UnsupportedOperationError < ArgumentError
def initialize(message = 'A valid operation must be provided.')
super
end
end
UnsupportedOperation = UnsupportedOperationError
end
module SimpleCalculatorExceptions
class IntegerOperandError < ArgumentError
def initialize(message = 'Operands must be integers.')
super
end
end
end
class SimpleCalculator
include CalculatorExceptions, SimpleCalculatorExceptions
OPERATE = {
'+' => ->(operand1, operand2) { operand1 + operand2 },
'*' => ->(operand1, operand2) { operand1 * operand2 },
'/' => ->(dividend, divisor) { dividend / divisor },
}
REPORT = '%<operand1>i %<operator>s %<operand2>i = %<result>i'
def self.operation_allowed?(operator)
OPERATE.keys.member?(operator)
end
private_class_method :operation_allowed?
##
# Applies the operator to the operands and returns a string
# representing the entire expression with the answer or an
# error message if some invalid input is provided.
#
# @param operand1 [Integer]
# @param operand2 [Integer]
# @param operator ['+', '*', '/']
#
def self.calculate(operand1, operand2, operator)
raise UnsupportedOperation unless operation_allowed?(operator)
raise IntegerOperandError unless
[operand1, operand2].all? { |operand| operand.is_a?(Integer) }
new(operand1, operand2, operator).to_s
rescue ZeroDivisionError
'Division by zero is not allowed.'
end
private
attr_reader :operand1, :operand2, :operator, :report
def initialize(operand1, operand2, operator, report: REPORT)
@operand1 = operand1
@operand2 = operand2
@operator = operator
@report = report
end
def operate
OPERATE[operator].call(operand1, operand2)
end
public
def to_s
report % { operand1:, operator:, operand2:, result: operate }
end
end
if $PROGRAM_NAME == __FILE__
# expected normal use
puts SimpleCalculator.calculate(1, 2, '+')
puts SimpleCalculator.new(1, 2, '+')
puts
# examination as a debugging example
p SimpleCalculator.new(1, 2, '+').to_s
p SimpleCalculator.new(1, 2, '+')
puts
# additions from outside the class as a user of the library!
SimpleCalculator::OPERATE.merge!({'**' => ->(base, power) { base ** power }})
puts SimpleCalculator.calculate(2, 8, '**')
puts
# an example of a custom report string provided by user
# an example of "single quote heredoc"
my_report = <<~eos
%<operand1>4i
———— = %<result>s
%<operand2>4i
eos
puts SimpleCalculator.new(1024, 4, '/', report: my_report)
puts
# another example of adding operation from outside of the class as
# a user of the library
SimpleCalculator::OPERATE.merge!(
{'-' => ->(operand1, operand2) { operand1 - operand2 }}
)
puts SimpleCalculator.calculate(3, 2, '-')
end