Class: Calculus::Expression

Inherits:
Object
  • Object
show all
Includes:
Latex
Defined in:
lib/calculus/expression.rb

Overview

This class represent some expression and optionaly transform it to the postfix notation for later analysis.

Expression can introduce variables which could be substituted later

exp = Expression.new("x + 2 * 4")
exp.to_s                            #=> "x + 2 * 4"
exp.calculate                       # raises UnboundVariableError
exp["x"] = 5
exp.to_s                            #=> "5 + 2 * 4"
exp.calculate                       #=> 40

Constant Summary

Constants included from Latex

Latex::TEMPLATE

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Latex

#missing_commands, #to_png

Constructor Details

#initialize(source, options = {}) ⇒ Expression

Initialize instance with given string expression.

It is possible to skip parser.

# raises Calculus::ParserError: Invalid character...
x = Expression.new("\sum_{i=1}^n \omega_i \x_i")
# just stores source string and allows rendering to PNG
x = Expression.new("\sum_{i=1}^n \omega_i \x_i", :parse => false)
x.parsed?   #=> false

It raises ArgumentError if there are more than one equal sign because if you need to represent the system of equations you should you two instances of Expression class and no equals sign for just calculation.

Also it initializes SHA1 fingerprint of particular expression

Raises:

  • (ArgumentError)


47
48
49
50
51
52
53
54
# File 'lib/calculus/expression.rb', line 47

def initialize(source, options = {})
  options = {:parse => true}.merge(options)
  @source = source
  @postfix_notation = options[:parse] ? Parser.new(source).parse : []
  raise ArgumentError, "Should be no more that one equals sign" if @postfix_notation.count(:eql) > 1
  @variables = extract_variables
  update_sha1
end

Instance Attribute Details

#postfix_notationObject (readonly) Also known as: rpn

Array with postfix notation of expression



28
29
30
# File 'lib/calculus/expression.rb', line 28

def postfix_notation
  @postfix_notation
end

#sha1Object (readonly)

Represents unique identifier of expression. Should be changed when some variables are binding



22
23
24
# File 'lib/calculus/expression.rb', line 22

def sha1
  @sha1
end

#sourceObject (readonly)

Source expression string



25
26
27
# File 'lib/calculus/expression.rb', line 25

def source
  @source
end

Instance Method Details

#[](name) ⇒ Object

Getter for given variable. Raises an Argument exception when there no such variable.

Raises:

  • (ArgumentError)


78
79
80
81
# File 'lib/calculus/expression.rb', line 78

def [](name)
  raise ArgumentError, "No such variable defined: #{name}" unless @variables.keys.include?(name)
  @variables[name]
end

#[]=(name, value) ⇒ Object

Setter for given variable. Raises an Argument exception when there no such variable.

Raises:

  • (ArgumentError)


85
86
87
88
89
# File 'lib/calculus/expression.rb', line 85

def []=(name, value)
  raise ArgumentError, "No such variable defined: #{name}" unless @variables.keys.include?(name)
  @variables[name] = value
  update_sha1
end

#abstract_syntax_treeObject Also known as: ast

Builds abstract syntax tree (AST) as alternative expression notation. Return nested array where first member is an operation and the other operands

Calculus::Expression.new("x + 2 * 4").ast   #=> [:plus, "x", [:mul, 2, 4]]


143
144
145
146
147
# File 'lib/calculus/expression.rb', line 143

def abstract_syntax_tree
  traverse do |operation, left, right, stack|
    [operation, left, right]
  end
end

#calculateObject

Traverse postfix notation and calculate the actual value of expression. Raises NotImplementedError when equation detected (currently it cannot solve equation) and UnboundVariableError if there unbound variables found.

Note that there some rounding errors here in root operation because of general approach to calculate it:

1000 ** (1.0 / 3)   #=>   9.999999999999998

Raises:

  • (NotImplementedError)


121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/calculus/expression.rb', line 121

def calculate
  raise NotImplementedError, "Equation detected. This class can't calculate equations yet." if equation?
  raise UnboundVariableError, "Can't calculate. Unbound variables found: #{unbound_variables.join(', ')}" unless unbound_variables.empty?

  traverse do |operation, left, right, stack|
    case operation
    when :sqrt  then left ** (1.0 / right) # could cause some rounding errors
    when :exp   then left ** right
    when :plus  then left + right
    when :minus then left - right
    when :mul   then left * right
    when :div   then left / right
    end
  end
end

#equation?Boolean

Returns true if there equals sign presented

Returns:

  • (Boolean)


62
63
64
# File 'lib/calculus/expression.rb', line 62

def equation?
  @postfix_notation.include?(:eql)
end

#inspectObject

:nodoc:



163
164
165
# File 'lib/calculus/expression.rb', line 163

def inspect # :nodoc:
  "#<Expression:#{@sha1} postfix_notation=#{@postfix_notation.inspect} variables=#{@variables.inspect}>"
end

#parsed?Boolean

Returns true when postfix notation has been built

Returns:

  • (Boolean)


57
58
59
# File 'lib/calculus/expression.rb', line 57

def parsed?
  !@postfix_notation.empty?
end

#to_sObject

Returns string representation of expression. Substitutes bound variables.



152
153
154
155
156
157
158
159
160
161
# File 'lib/calculus/expression.rb', line 152

def to_s
  result = source.dup
  (variables - unbound_variables).each do |var|
    result.gsub!(/(\W)#{var}(\W)/, "\\1#{@variables[var]}\\2")
    result.gsub!(/^#{var}(\W)/, "#{@variables[var]}\\1")
    result.gsub!(/(\W)#{var}$/, "\\1#{@variables[var]}")
    result.gsub!(/^#{var}$/, @variables[var].to_s)
  end
  result
end

#traverse(&block) ⇒ Object

Perform traverse along postfix notation. Yields operation with left and right operands and the latest argument is the current state of stack (note you can amend this stack from outside)



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/calculus/expression.rb', line 95

def traverse(&block) # :yields: operation, left, right, stack
  stack = []
  @postfix_notation.each do |node|
    case node
    when Symbol
      operation, right, left = node, stack.pop, stack.pop
      stack.push(yield(operation, left, right, stack))
    when Numeric
      stack.push(node)
    when String
      stack.push(@variables[node] || node)
    end
  end
  stack.pop
end

#unbound_variablesObject

Returns array of variables which have nil value



72
73
74
# File 'lib/calculus/expression.rb', line 72

def unbound_variables
  @variables.keys.select{|k| @variables[k].nil?}
end

#variablesObject

Returns array of strings with variable names



67
68
69
# File 'lib/calculus/expression.rb', line 67

def variables
  @variables.keys
end