replace expressions with polynomials, add interpreter command class, rewrite parser

This commit is contained in:
2026-04-13 13:23:12 +03:00
parent afb81b475a
commit a420ba3ce2
5 changed files with 343 additions and 532 deletions

View File

@@ -1,9 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'ruby_algebra/version' require_relative 'ruby_algebra/version'
require_relative 'ruby_algebra/expression' require_relative 'ruby_algebra/command'
require_relative 'ruby_algebra/polynomial' require_relative 'ruby_algebra/polynomial'
require_relative 'ruby_algebra/parser' require_relative 'ruby_algebra/parser'
module RubyAlgebra
end

View File

@@ -0,0 +1,46 @@
# frozen_string_literal: true
module RubyAlgebra
# Команда интерпретатора
class Command
end
# Команда присвоения
class AssignmentCommand < Command
attr_reader :lhs, :operation, :operand1, :operand2
def initialize(lhs, operation, operand1, operand2)
@lhs = lhs
@operation = operation
@operand1 = operand1
@operand2 = operand2
end
def to_s
result = @lhs.is_a?(Array) ? @lhs.join(', ') : @lhs.to_s
result += ' := '
result += @operand1.to_s
case @operation
when :add
result += ' + '
when :sub
result += ' - '
when :mult, :scale
result += ' * '
when :div
result += ' / '
end
result += @operand2.to_s if @operation != :assign
result
end
end
# Команда вывода на экран
class DisplayCommand < Command
attr_reader :polynomial
def initialize(polynomial)
@polynomial = polynomial
end
end
end

View File

@@ -1,375 +0,0 @@
# frozen_string_literal: true
module RubyAlgebra
class Expression
def to_s
raise NotImplementedError
end
def type
raise NotImplementedError
end
def op_priority
raise NotImplementedError
end
def op_assoc_type
raise NotImplementedError
end
def diff(v)
raise NotImplementedError, "#{self.class}#diff not implemented"
end
def evaluate
raise NotImplementedError, "#{self.class}#evaluate not implemented"
end
def constant?
raise NotImplementedError, "#{self.class}#constant? not implemented"
end
end
class Addition < Expression
attr_reader :lhs, :rhs
def initialize(lhs, rhs)
raise ArgumentError unless lhs.is_a?(Expression) && rhs.is_a?(Expression)
@lhs = lhs
@rhs = rhs
end
def to_s
need_parentheses_left = lhs.op_priority < op_priority || (lhs.op_priority == op_priority && op_assoc_type == :right)
need_parentheses_right = rhs.op_priority < op_priority || (rhs.op_priority == op_priority && op_assoc_type == :left)
result = need_parentheses_left ? "(#{lhs}) + " : "#{lhs} + "
result + (need_parentheses_right ? "(#{rhs})" : rhs.to_s)
end
def type
:add
end
def op_priority
10
end
def op_assoc_type
:left
end
def ==(other)
return false if other.type != type
lhs == other.lhs && rhs == other.rhs
end
def diff(v)
Addition.new(@lhs.diff(v), @rhs.diff(v))
end
def evaluate
lhs_val = @lhs.evaluate
rhs_val = @rhs.evaluate
return nil if lhs_val.nil? || rhs_val.nil?
lhs_val + rhs_val
end
def constant?
@lhs.constant? && @rhs.constant?
end
end
class Subtraction < Addition
attr_reader :lhs, :rhs
def to_s
need_parentheses_left = lhs.op_priority < op_priority || (lhs.op_priority == op_priority && op_assoc_type == :right)
need_parentheses_right = rhs.op_priority < op_priority || (rhs.op_priority == op_priority && op_assoc_type == :left)
result = need_parentheses_left ? "(#{lhs}) - " : "#{lhs} - "
result + (need_parentheses_right ? "(#{rhs})" : rhs.to_s)
end
def type
:sub
end
def diff(v)
Subtraction.new(@lhs.diff(v), @rhs.diff(v))
end
def evaluate
lhs_val = @lhs.evaluate
rhs_val = @rhs.evaluate
return nil if lhs_val.nil? || rhs_val.nil?
lhs_val - rhs_val
end
def constant?
@lhs.constant? && @rhs.constant?
end
end
class Multiplication < Expression
attr_reader :lhs, :rhs
def initialize(lhs, rhs)
raise ArgumentError unless lhs.is_a?(Expression) && rhs.is_a?(Expression)
@lhs = lhs
@rhs = rhs
end
def to_s
need_parentheses_left = lhs.op_priority < op_priority || (lhs.op_priority == op_priority && op_assoc_type == :right)
need_parentheses_right = rhs.op_priority < op_priority || (rhs.op_priority == op_priority && op_assoc_type == :left)
if lhs.type == :constant && lhs.value == 1
need_parentheses_right ? "(#{rhs})" : rhs.to_s
elsif lhs.type == :constant && lhs.value == -1
need_parentheses_right ? "-(#{rhs})" : "-#{rhs}"
elsif (need_parentheses_right || rhs.type == :variable) && (need_parentheses_left || lhs.type == :mult || lhs.type == :constant)
result = need_parentheses_left ? "(#{lhs})" : lhs.to_s
result += ' ' if lhs.type == :variable && rhs.type == :variable && lhs.single_letter? && rhs.single_letter?
result + (need_parentheses_right ? "(#{rhs})" : rhs.to_s)
else
result = need_parentheses_left ? "(#{lhs}) * " : "#{lhs} * "
result + (need_parentheses_right ? "(#{rhs})" : rhs.to_s)
end
end
def type
:mult
end
def op_priority
20
end
def op_assoc_type
:left
end
def ==(other)
return false if other.type != type
lhs == other.lhs && rhs == other.rhs
end
def diff(v)
u_prime = @lhs.diff(v)
v_prime = @rhs.diff(v)
term1 = Multiplication.new(u_prime, @rhs)
term2 = Multiplication.new(@lhs, v_prime)
Addition.new(term1, term2)
end
def evaluate
lhs_val = @lhs.evaluate
rhs_val = @rhs.evaluate
return nil if lhs_val.nil? || rhs_val.nil?
lhs_val * rhs_val
end
def constant?
@lhs.constant? && @rhs.constant?
end
end
class Division < Multiplication
def to_s
need_parentheses_left = lhs.op_priority < op_priority || (lhs.op_priority == op_priority && op_assoc_type == :right)
need_parentheses_right = rhs.op_priority < op_priority || (rhs.op_priority == op_priority && op_assoc_type == :left)
result = need_parentheses_left ? "(#{lhs}) / " : "#{lhs} / "
result + (need_parentheses_right ? "(#{rhs})" : rhs.to_s)
end
def type
:div
end
def diff(v)
u_prime = @lhs.diff(v)
v_prime = @rhs.diff(v)
numerator = Subtraction.new(
Multiplication.new(u_prime, @rhs),
Multiplication.new(@lhs, v_prime)
)
denominator = Power.new(@rhs, Constant.new(2))
Division.new(numerator, denominator)
end
def evaluate
lhs_val = @lhs.evaluate
rhs_val = @rhs.evaluate
return nil if lhs_val.nil? || rhs_val.nil?
return nil if rhs_val == 0
lhs_val.to_f / rhs_val
end
def constant?
@lhs.constant? && @rhs.constant?
end
end
class Power < Expression
attr_reader :base, :exponent
def initialize(base, exponent)
raise ArgumentError unless base.is_a?(Expression) && exponent.is_a?(Expression)
@base = base
@exponent = exponent
end
def to_s
need_parentheses_left = base.op_priority < op_priority || (base.op_priority == op_priority && op_assoc_type == :right)
need_parentheses_right = exponent.op_priority < op_priority || (exponent.op_priority == op_priority && op_assoc_type == :left)
result = need_parentheses_left ? "(#{base}) ^ " : "#{base} ^ "
result + (need_parentheses_right ? "(#{exponent})" : exponent.to_s)
end
def type
:pow
end
def op_priority
30
end
def op_assoc_type
:right
end
def ==(other)
return false if other.type != type
base == other.base && exponent == other.exponent
end
def diff(v)
unless @exponent.is_a?(Constant)
raise NotImplementedError, 'Дифференцирование степени с неконстантным показателем не реализовано'
end
n = @exponent.value
# (u^n)' = n * u^(n-1) * u'
base_diff = @base.diff(v)
new_power = Power.new(@base, Constant.new(n - 1))
coeff = Constant.new(n)
Multiplication.new(coeff, Multiplication.new(new_power, base_diff))
end
def evaluate
base_val = @base.evaluate
exp_val = @exponent.evaluate
return nil if base_val.nil? || exp_val.nil?
base_val**exp_val
end
def constant?
@base.constant? && @exponent.constant?
end
end
class Constant < Expression
attr_reader :value
def initialize(value)
@value = value
end
def to_s
@value.to_s
end
def type
:constant
end
def op_priority
1000
end
def op_assoc_type
nil
end
def ==(other)
return false if other.type != type
value == other.value
end
def diff(v)
Constant.new(0)
end
def evaluate
@value
end
def constant?
true
end
end
class Variable < Expression
attr_reader :symbol
def initialize(symbol)
@symbol = symbol
@is_single_letter = symbol.length == 1
end
def to_s
@symbol
end
def type
:variable
end
def op_priority
1000
end
def op_assoc_type
nil
end
def single_letter?
@is_single_letter
end
def ==(other)
return false if other.type != type
symbol == other.symbol
end
def diff(v)
@symbol == v ? Constant.new(1) : Constant.new(0)
end
def evaluate
nil
end
def constant?
false
end
end
end

View File

@@ -2,199 +2,332 @@
module RubyAlgebra module RubyAlgebra
module Parser module Parser
# Токен выражения или команды
class Token class Token
attr_reader :type attr_reader :type
def initialize; end
end end
class NumberToken < Token # Знак сложения
attr_reader :value class PlusToken < Token
def initialize
def initialize(value) super()
@type = :num @type = :plus
@value = value
end end
end end
class IdentifierToken < Token # Знак вычитания
attr_reader :name class MinusToken < Token
def initialize
def initialize(name) super()
@type = :id @type = :minus
@name = name
end end
end end
class BinaryOperatorToken < Token # Знак умножения
attr_reader :op class MultiplyToken < Token
def initialize
def initialize(op) super()
@type = :op @type = :mult
@op = op
end end
end end
class UnaryOperatorToken < Token # Знак деления
attr_reader :op class DivideToken < Token
def initialize
def initialize(op) super()
@type = :unary_op @type = :div
@op = op
end end
end end
# Знак возведения в степень
class PowerToken < Token
def initialize
super()
@type = :pow
end
end
# Скобка
class ParenthesisToken < Token class ParenthesisToken < Token
attr_reader :closing attr_reader :closing
def initialize(closing) def initialize(closing)
super()
@type = :paren @type = :paren
@closing = closing @closing = closing
end end
end end
# Знак присвоения :=
class AssignmentToken < Token
def initialize
super()
@type = :assign
end
end
# Запятая
class CommaToken < Token
def initialize
super()
@type = :comma
end
end
# Имя переменной или функции
class IdentifierToken < Token
attr_reader :symbol
def initialize(symbol)
super()
@type = :id
@symbol = symbol
end
def variable?
@symbol.match?(/^\p{Ll}/)
end
end
# Целое или дробное число
class NumberToken < Token
attr_reader :value
def initialize(value)
super()
@type = :num
@value = value
end
end
# Специальный токен, означающий конец ввода
class EndToken < Token class EndToken < Token
def initialize() def initialize
super()
@type = :end @type = :end
end end
end end
def self.tokenize(e) # Класс для разбора выражений на токены
i = 0 class Tokenizer
add_implicit_mul = false attr_reader :lookahead
while i < e.length
c = e[i] def initialize(expr)
@expr = expr
@i = 0
@lookahead = _next_token
end
def next_token
token = @lookahead
@lookahead = _next_token unless token.type == :end
token
end
def _next_token
puts '_next_token'
loop do
return EndToken.new unless @i < @expr.length
puts "i = #{@i}"
c = @expr[@i]
puts "c = #{c}"
if c.match?(/\p{Alpha}/) if c.match?(/\p{Alpha}/)
yield BinaryOperatorToken.new(:mult) if add_implicit_mul puts 'letter'
j = i + 1 j = @i + 1
j += 1 while j < e.length && e[j].match?(/\p{Alnum}/) j += 1 while j < @expr.length && @expr[j].match?(/\p{Alnum}/)
add_implicit_mul = true token = IdentifierToken.new(@expr[@i...j])
yield IdentifierToken.new(e[i...j]) @i = j
i = j return token
elsif c.match?(/[+\-*\/^()]/) elsif c.match(%r{[+\-*/^():]})
yield BinaryOperatorToken.new(:mult) if c == "(" && add_implicit_mul puts 'operator'
i += 1 token = nil
case c case c
when "+" when '+'
if add_implicit_mul token = PlusToken.new
yield BinaryOperatorToken.new(:add) when '-'
else token = MinusToken.new
yield UnaryOperatorToken.new(:positive) when '*'
token = MultiplyToken.new
when '/'
token = DivideToken.new
when '^'
token = PowerToken.new
when '('
token = ParenthesisToken.new(false)
when ')'
token = ParenthesisToken.new(true)
end end
when "-" @i += 1
if add_implicit_mul if c == ':' && @expr[@i] == '='
yield BinaryOperatorToken.new(:sub) token = AssignmentToken.new
else @i += 1
yield UnaryOperatorToken.new(:negative)
end end
when "*" return token
yield BinaryOperatorToken.new(:mult)
when "/"
yield BinaryOperatorToken.new(:div)
when "^"
yield BinaryOperatorToken.new(:pow)
when "("
yield ParenthesisToken.new(false)
when ")"
yield ParenthesisToken.new(true)
end
add_implicit_mul = c == ")"
elsif c.match?(/\p{Digit}|\./) elsif c.match?(/\p{Digit}|\./)
yield BinaryOperatorToken.new(:mult) if add_implicit_mul puts 'digit'
j = i + 1 j = @i + 1
j += 1 while j < e.length && e[j].match?(/\p{Digit}|\./) j += 1 while j < @expr.length && @expr[j].match?(/\p{Digit}|\./)
s = e[i...j] s = @expr[@i...j]
fl = s.count "." @i = j
if fl > 1 puts "ssss: #{s}"
raise Exception.new fl = s.count '.'
end raise StandardError, 'malformed number' if fl > 1
add_implicit_mul = true return fl.zero? ? NumberToken.new(s.to_i) : NumberToken.new(s.to_f)
if fl == 0 elsif c == ' '
yield NumberToken.new(s.to_i) puts 'whitespace'
@i += 1
else else
yield NumberToken.new(s.to_f) puts 'unknown'
end @i += 1
i = j end
elsif c == " "
i += 1
else
i += 1
end end
end end
yield EndToken.new
end end
def self.parse(expr) def self.parse_command(expr)
operators = [] tokenizer = Tokenizer.new(expr)
out_stack = [] n = tokenizer.lookahead
tokenize(expr) do |token| if n.type == :id && !n.variable?
if token.type == :num parse_assignment_command(tokenizer)
out_stack.push(Constant.new(token.value))
elsif token.type == :end
_parse_make_op(operators, out_stack) until operators.empty?
elsif token.type == :op || token.type == :unary_op
p = _parse_op_prio token
while operators.length != 0 && (operators.last.type == :op || operators.last.type == :unary_op) && (_parse_op_prio(operators.last) > p || (_parse_op_prio(operators.last) == p && token.op != :pow && token.op != :positive && token.op != :negative))
_parse_make_op(operators, out_stack)
end
operators.push(token)
elsif token.type == :paren
if token.closing
until operators.empty? || (operators.last.type == :paren && !operators.last.closing)
_parse_make_op(operators, out_stack)
end
if operators.empty? || operators.last.type != :paren || operators.last.closing
raise Exception.new
else else
operators.pop parse_display_command(tokenizer)
end end
else
operators.push token
end
elsif token.type == :id
out_stack.push(Variable.new(token.name))
end
end
out_stack[0]
end end
def self._parse_make_op(operators, out_stack) def self.parse_assignment_command(tokenizer)
oper = operators.pop left_hand_side = tokenizer.next_token
unless (oper.type == :op && out_stack.length >= 2) || (oper.type == :unary_op && out_stack.length >= 1) n = tokenizer.next_token
raise Exception.new if n.type == :assign
operand1 = tokenizer.lookahead
unless operand1.type == :id && !operand1.variable?
return AssignmentCommand.new(left_hand_side.symbol, :assign, parse_polynomial(tokenizer), nil)
end end
b = out_stack.pop if oper.type == :op tokenizer.next_token
a = out_stack.pop operator = tokenizer.next_token
raise StandardError, "unexpected token #{operator.type}, expected +, -, * or /" unless [:plus, :minus, :mult, :div].include?(operator.type)
case oper.op operand2 = tokenizer.next_token
when :add unless ([:plus, :minus, :mult].include?(operator.type) && operand2.type == :id && !operand2.variable?) ||
out_stack.push Addition.new(a, b) ([:mult, :div].include?(operator.type) && operand2.type == :num)
when :sub raise StandardError, 'unsupported operation or invalid syntax'
out_stack.push Subtraction.new(a, b) end
if operand2.type == :num
if operator.type == :mult
return AssignmentCommand.new(left_hand_side.symbol, :scale, operand1.symbol, operand2.value)
else
return AssignmentCommand.new(left_hand_side.symbol, :scale, operand1.symbol, 1.0 / operand2.value)
end
elsif operand2.type == :id
case operator.type
when :plus
return AssignmentCommand.new(left_hand_side.symbol, :add, operand1.symbol, operand2.symbol)
when :minus
return AssignmentCommand.new(left_hand_side.symbol, :sub, operand1.symbol, operand2.symbol)
when :mult when :mult
out_stack.push Multiplication.new(a, b) return AssignmentCommand.new(left_hand_side.symbol, :mult, operand1.symbol, operand2.symbol)
when :div
out_stack.push Division.new(a, b)
when :pow
out_stack.push Power.new(a, b)
when :positive
out_stack.push a
when :negative
if a.type == :constant
out_stack.push Constant.new(-a.value)
else
out_stack.push Multiplication.new(Constant.new(-1), a)
end end
end end
elsif n.type == :comma
second_lhs = tokenizer.next_token
unless second_lhs.type == :id && !second_lhs.variable?
raise StandardError, "unexpected token #{second_lhs.type}, expected polynomial variable"
end
unless tokenizer.next_token.type == :assign
raise StandardError, "unexpected token, expected :="
end
operand1 = tokenizer.next_token
unless operand1.type == :id && !operand1.variable?
raise StandardError, "unexpected token #{operand1.type}, expected polynomial variable"
end
unless tokenizer.next_token.type == :div
raise StandardError, "unexpected token, expected /"
end
operand2 = tokenizer.next_token
unless operand2.type == :id && !operand2.variable?
raise StandardError, "unexpected token #{operand2.type}, expected polynomial variable"
end
return AssignmentCommand.new([left_hand_side.symbol, second_lhs.symbol], :div, operand1.symbol)
else
raise StandardError, "unexpected token #{n.type}, expected ':=' or ','"
end
end end
def self._parse_op_prio(op_token) def self.parse_display_command(tokenizer)
case op_token.op DisplayCommand.new(parse_polynomial(tokenizer))
when :add, :sub
1
when :mult, :div
2
when :pow, :positive, :negative
3
end end
def self.parse_polynomial(tokenizer)
puts "parse_polynomial <lookahead #{tokenizer.lookahead.type}>"
terms = [parse_term(tokenizer)]
while [:plus, :minus].include?(tokenizer.lookahead.type)
sign = tokenizer.next_token.type == :plus ? +1 : -1
terms << parse_term(tokenizer) * sign
end
puts 'parse_polynomial end'
Polynomial.new(terms)
end
def self.parse_term(tokenizer)
puts "parse_term <lookahead #{tokenizer.lookahead.type}>"
n = tokenizer.lookahead
unless n.type == :id && n.variable? || n.type == :num
raise StandardError, "unexpected token #{n.type}, expected number or variable"
end
if n.type == :num
puts "eat coeff"
tokenizer.next_token
coeff = n.value
variables = {}
else
coeff = 1
v = parse_variable_factor(tokenizer)
variables = {v[0] => v[1]}
end
loop do
n = tokenizer.lookahead
puts "<lookahead #{n.type}>"
unless n.type == :mult || n.type == :id && n.variable?
break
end
v = parse_variable_factor(tokenizer)
variables[v[0]] = v[1]
end
puts 'parse_term end'
pp [coeff, variables]
Term.new(coeff, variables)
end
def self.parse_variable_factor(tokenizer)
puts "parse_term <lookahead #{tokenizer.lookahead.type}>"
n = tokenizer.lookahead
tokenizer.next_token
puts 'eat variable'
#n = tokenizer.next_token if n.type == :mult
if n.type == :mult
n = tokenizer.next_token
puts 'eat *'
end
unless n.type == :id && n.variable?
raise StandardError, "unexpected token #{n.type}, expected variable"
end
variable = n.symbol
if tokenizer.lookahead.type == :pow
tokenizer.next_token
puts 'eat ^'
unless tokenizer.lookahead.type == :num
raise StandardError, "unexpected token #{n.type}, expected number"
end
power = tokenizer.next_token.value
puts 'eat exponent'
else
power = 1
end
puts 'parse_variable_factor end'
pp [variable, power]
[variable, power]
end end
end end
end end

View File

@@ -171,7 +171,9 @@ module RubyAlgebra
end end
def *(other) def *(other)
return Term.new if @coeff.zero? || other.coeff.zero? return Term.new if zero? || other.zero?
return Term.new(coeff * other, variables) unless other.is_a? Term
new_variables = @variables.clone new_variables = @variables.clone
other.variables.each do |symbol, power| other.variables.each do |symbol, power|
@@ -205,10 +207,18 @@ module RubyAlgebra
unless @variables.empty? unless @variables.empty?
first = true first = true
@variables.each do |var, power| @variables.each do |var, power|
result += if first if first
"#{var}^#{power}" unless power == 1
result += "#{var}^#{power}"
else else
" * #{var}^#{power}" result += "#{var}"
end
else
unless power == 1
result += " * #{var}^#{power}"
else
result += " * #{var}"
end
end end
first = false first = false
end end