patternrubyMinor
Parsing mathmatical functions
Viewed 0 times
functionsmathmaticalparsing
Problem
I have finally finished a Ruby calculator project, which is not based on eval. Instead it parses input char by char. The project is hosted on GitHub.
However, I find a specific part of the program very annoying since whenever I have to add something I have to rewrite a portion of it again.
```
def trig
case @look
when 'c'
match_all('cos')
case @look
when 'h'
match_all('h(')
value = cosh(calculate)
when '('
match('(')
value = cos(calculate)
else
expected('cos() or cosh()')
end
when 's'
match('s')
if @look == 'q'
match_all('qrt(')
value = sqrt(calculate)
else
match_all('in')
case @look
when 'h'
match_all('h(')
value = sinh(calculate)
when '('
match('(')
value = sin(calculate)
else
expected('sin() or sinh()')
end
end
when 'r'
match_all('root')
base = get_number
match('(')
value = calculate ** (1.0/base)
when 't'
match_all('tan')
case @look
when 'h'
match_all('h(')
value = tanh(calculate)
when '('
match('(')
value = tan(calculate)
else
expected('tan() or tanh()')
end
when 'l'
match('l')
case @look
when 'n'
match_all('n(')
value = log(calculate)
when 'o'
match_all('og')
if digit? @look
base = get_number
elsif @look == "("
base = 10
else
expected("integer or ( ")
end
match('(')
value = log(calculate, base)
else
expected('ln() or log()')
end
when 'e'
match_all('exp(')
value = exp(calculate)
when 'a'
match_all('arc')
case @look
when 'c'
match_all('cos')
case @look
when 'h'
match_all('h(')
value = acosh(calculate)
when '('
match('(')
value = acos(calculate)
else
expected('arccos() or arccosh()')
end
when 's'
match_all('sin'
However, I find a specific part of the program very annoying since whenever I have to add something I have to rewrite a portion of it again.
```
def trig
case @look
when 'c'
match_all('cos')
case @look
when 'h'
match_all('h(')
value = cosh(calculate)
when '('
match('(')
value = cos(calculate)
else
expected('cos() or cosh()')
end
when 's'
match('s')
if @look == 'q'
match_all('qrt(')
value = sqrt(calculate)
else
match_all('in')
case @look
when 'h'
match_all('h(')
value = sinh(calculate)
when '('
match('(')
value = sin(calculate)
else
expected('sin() or sinh()')
end
end
when 'r'
match_all('root')
base = get_number
match('(')
value = calculate ** (1.0/base)
when 't'
match_all('tan')
case @look
when 'h'
match_all('h(')
value = tanh(calculate)
when '('
match('(')
value = tan(calculate)
else
expected('tan() or tanh()')
end
when 'l'
match('l')
case @look
when 'n'
match_all('n(')
value = log(calculate)
when 'o'
match_all('og')
if digit? @look
base = get_number
elsif @look == "("
base = 10
else
expected("integer or ( ")
end
match('(')
value = log(calculate, base)
else
expected('ln() or log()')
end
when 'e'
match_all('exp(')
value = exp(calculate)
when 'a'
match_all('arc')
case @look
when 'c'
match_all('cos')
case @look
when 'h'
match_all('h(')
value = acosh(calculate)
when '('
match('(')
value = acos(calculate)
else
expected('arccos() or arccosh()')
end
when 's'
match_all('sin'
Solution
Here's a toy program to illustrate the separation of user input collection from the string matching that determines which calc to use, as we discussed in the comments. Note that the class
When I find a match, I just evaluate it with the argument pi. In your real application, you'd need to do further process to gather the user's input for the argument.
Also note how if we decide we want to change which calculations we support (one of the most likely changes for the application), it becomes a 1 line change, and the user input processing code doesn't need to be touched.
NamedCalculations has no knowledge of IO with the user. All the IO is encapsulated in CmdLineCalculator.When I find a match, I just evaluate it with the argument pi. In your real application, you'd need to do further process to gather the user's input for the argument.
Also note how if we decide we want to change which calculations we support (one of the most likely changes for the application), it becomes a 1 line change, and the user input processing code doesn't need to be touched.
class NamedCalculations
# a hash of named calcs
# eg, {cos: Math.method(:cos), sin: Math.method(:sin) }
def initialize(calc_definitions)
@calc_definitions = calc_definitions
@calc_names = @calc_definitions.keys.map(&:to_s)
end
def method_for(calc_name)
calc_name = calc_name.to_sym
raise "Invalid Calculation Name" unless @calc_definitions.key? calc_name
@calc_definitions[calc_name]
end
def exact_match?(input)
@calc_names.any? {|name| input == name}
end
def partial_match?(input)
@calc_names.any? {|name| name.include? input}
end
end
class CmdLineCalculator
def initialize(named_calcs)
@named_calcs = named_calcs
@input = ''
end
def start
begin
system("stty raw -echo")
collect_user_input while not calc_complete?
show_answer
ensure
system("stty -raw echo")
end
end
private
def collect_user_input
@input += next_input_char
validate_input
echo_last_char
end
def validate_input
raise "Invalid Calculation" unless @named_calcs.partial_match? @input
end
def next_input_char
STDIN.getc
end
def echo_last_char
STDOUT.putc @input[-1]
end
def calc_complete?
@named_calcs.exact_match? @input
end
def show_answer
selected_method = @named_calcs.method_for(@input)
value_at_pi = selected_method.call(Math::PI)
STDOUT.puts "\nThe #{@input}(pi) = #{value_at_pi}"
end
end
my_calcs = {cos: Math.method(:cos), sin: Math.method(:sin) }
client = CmdLineCalculator.new(NamedCalculations.new(my_calcs))
client.startCode Snippets
class NamedCalculations
# a hash of named calcs
# eg, {cos: Math.method(:cos), sin: Math.method(:sin) }
def initialize(calc_definitions)
@calc_definitions = calc_definitions
@calc_names = @calc_definitions.keys.map(&:to_s)
end
def method_for(calc_name)
calc_name = calc_name.to_sym
raise "Invalid Calculation Name" unless @calc_definitions.key? calc_name
@calc_definitions[calc_name]
end
def exact_match?(input)
@calc_names.any? {|name| input == name}
end
def partial_match?(input)
@calc_names.any? {|name| name.include? input}
end
end
class CmdLineCalculator
def initialize(named_calcs)
@named_calcs = named_calcs
@input = ''
end
def start
begin
system("stty raw -echo")
collect_user_input while not calc_complete?
show_answer
ensure
system("stty -raw echo")
end
end
private
def collect_user_input
@input += next_input_char
validate_input
echo_last_char
end
def validate_input
raise "Invalid Calculation" unless @named_calcs.partial_match? @input
end
def next_input_char
STDIN.getc
end
def echo_last_char
STDOUT.putc @input[-1]
end
def calc_complete?
@named_calcs.exact_match? @input
end
def show_answer
selected_method = @named_calcs.method_for(@input)
value_at_pi = selected_method.call(Math::PI)
STDOUT.puts "\nThe #{@input}(pi) = #{value_at_pi}"
end
end
my_calcs = {cos: Math.method(:cos), sin: Math.method(:sin) }
client = CmdLineCalculator.new(NamedCalculations.new(my_calcs))
client.startContext
StackExchange Code Review Q#105099, answer score: 2
Revisions (0)
No revisions yet.