I recently wrote the following code for a codewars.com kata for evaluating mathematical expressions. I've been writing ruby for years, but almost everything I do is for personal use and isn't shared, so I've gotten essentially zero feedback on my ruby style. The following block of code (with more comments than usual) is fairly typical of my style, but it never feels as ruby-ish as the code snippets I see online.
What can be changed to make this code closer to ideal idiomatic ruby?
#parse the initial expression, break it up into an array
# of tokens (numbers, parens, operations, etc)
# this also detects negation and cheges the symbol from '-' to 'n'
# and finally, this converts numbers to floats
def tokenize_expression(expression)
#remove spaces
s = expression.gsub(/\s/,'')
#convert negations to 'n' character
while md = s.match(%r{(?<![\d\)])-}) do
s[md.begin(0)] = 'n'
end
#iterate through string
#if number, get full number and add to array
#otherwise grab just the first character which will be an operation or parenthesis
tokens = []
while not s.empty?
if not s.match(%r{^\d}).nil? #first char is digit
md = s.match %r{[\d\.]+}
s = md.post_match
tokens << md[0].to_f
else
tokens << s[0] #first char is parenthesis or operation
s = s[1..-1] #everything but first char
end
end
tokens
end
#take the array and make sub arrays based on the parentheses
# e.g. ['(', '1', '+', '(', '2', '+', '3', ')', ')'] -> ['1', '+', ['2', '+', '3']]
def nest_parens(tokens)
result = []
stack = []
first, *rest = *tokens
while not first.nil? do
case first #look at first token
when '('
stack.push result #store current partial result on stack if open parens
result = [] #start new result
when ')'
child = result #store result in temp var
result = stack.pop #get previous partial result
result << child #add temp result to current result
else
result << first #add this token to the current result
end
first, *rest = *rest
end
throw "Unclosed parenthesis" if not stack.empty?
result
end
#find all the neagtions and convert them to nested postfix
# e.g. '5-n6' becomes '5-[n 6]'
def postfix_negation(tokens)
return tokens if not tokens.is_a? Array
tokens = tokens.map{ |t| postfix_negation(t) } #recursively process everything below the current level
result = []
first, *rest = *tokens
while not first.nil?
case first
when 'n'
second, *rest = *rest
result << [first, second] #e.g. [n 6]
else
result << first
end
first, *rest = *rest
end
result
end
#find all operations (mult/div or plus/minus) and convert to nested postfix
# e.g. '1+2*3' becomes '[+ 1 [* 2 3]]'
def postfix_ops(tokens, ops=['/','*'])
return tokens if not tokens.is_a? Array
tokens = tokens.map{ |t| postfix_ops(t, ops) } #recursively process everything below the current level
result = []
first, *rest = *tokens
while not first.nil?
second = rest.first #if there is an operator, second will contain it
if ops.include? second
second, third, *rest = *rest
first = [second, first, third] #[op, arg1, arg2]. This now becomes first and is compared again to the following tokens,
next #which will handle cases like 1+2+3 --> [+ [+ 1 2] 3]
else
result << first
end
first, *rest = *rest
end
result
end
#take a fully processed, postfix tree and recursively evaluate the expressions
def eval(tree)
return tree if not tree.is_a? Array #if this isn't an array, return it
tree = tree.map {|n| eval(n) } #recursively process everything below the current level
return tree.first if tree.length == 1 #sometimes we end up with a single value as an array, e.g. [5], so just return the inner value
first, second, third = tree #process arguments
case first
when 'n' then return -second
when '+' then return second + third
when '-' then return second - third
when '*' then return second * third
when '/' then return second / third
else raise "Unkown Op: #{first}"
end
end
#wrapper to call all the needed steps for processing
def calc(expression)
tokens = tokenize_expression(expression)
tokens = nest_parens(tokens)
tokens = postfix_negation(tokens)
tokens = postfix_ops(tokens, ['/','*'])
tokens = postfix_ops(tokens, ['+','-'])
eval(tokens)
end
#test input
[
"2/(2+3)*4",
"2-(-(3*-2))",
"3*((4*5)*6*(7*8))",
"-(55--(-(1+2))--12)"
].each do |s|
puts "\nString: #{s}"
puts "Eval: #{calc(s)}"
end
.each
to iterate over lists instead of yourwhile
s... \$\endgroup\$