HiveBrain v1.2.0
Get Started
← Back to all entries
patternrubyMinor

CodeWars mathematical expresion evaluator

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
expresionevaluatorcodewarsmathematical

Problem

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{(? ['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 [+ [+ 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

Solution

Single responsibility principle

#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)


Your function tokenize_expression does:

  • break [the initial expression] up into an array



  • detects negation and changes the symbol from '-' to 'n'



  • converts numbers to floats



This should be three functions:

def negation_to_n(expression)
    s = expression.gsub(/\s/,'')
    while md = s.match(%r{(?<![\d\)])-}) do
        s[md.begin(0)] = 'n'
    end
    s
end

def split_tokens(expression)
    ### Implement
end

def numbers_to_floats(tokens)
    tokens.map{|t| t.match(%r{^\d}) ? t.to_f : t}
end


Use functional programming when practical

You should not feel forced to stick to imperative programming even when it becomes so hard to follow, for example negation_to_n can be written just like (please note that the string should be passed in already without spaces):

def matching_indexes(s, regex)
  s
   .enum_for(:scan, regex)
   .map { Regexp.last_match.begin(0) }
end

def negation_to_n(s)
  (0...s.length)
    .map{|i| matching_indexes(s, %r{(?<![\d\)])-}).include?(i) ? 'n' : s[i]}
end


Another example is split_tokens:

def split_tokens(s)
  s
    .chars
    .chunk {|d| is_digit?(d)}
    .map{|_, xs| xs}
    .map {|g| g.all? {|ch| is_digit?(ch)} ? g.join : g}
    .flatten
end


Surely other parts can be similarly simplified.

Code Snippets

#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)
def negation_to_n(expression)
    s = expression.gsub(/\s/,'')
    while md = s.match(%r{(?<![\d\)])-}) do
        s[md.begin(0)] = 'n'
    end
    s
end


def split_tokens(expression)
    ### Implement
end

def numbers_to_floats(tokens)
    tokens.map{|t| t.match(%r{^\d}) ? t.to_f : t}
end
def matching_indexes(s, regex)
  s
   .enum_for(:scan, regex)
   .map { Regexp.last_match.begin(0) }
end

def negation_to_n(s)
  (0...s.length)
    .map{|i| matching_indexes(s, %r{(?<![\d\)])-}).include?(i) ? 'n' : s[i]}
end
def split_tokens(s)
  s
    .chars
    .chunk {|d| is_digit?(d)}
    .map{|_, xs| xs}
    .map {|g| g.all? {|ch| is_digit?(ch)} ? g.join : g}
    .flatten
end

Context

StackExchange Code Review Q#119530, answer score: 5

Revisions (0)

No revisions yet.