From a420ba3ce2beb1f27aac60faee579f34069e5303 Mon Sep 17 00:00:00 2001 From: Slavasil Date: Mon, 13 Apr 2026 13:23:12 +0300 Subject: [PATCH] replace expressions with polynomials, add interpreter command class, rewrite parser --- lib/ruby_algebra.rb | 5 +- lib/ruby_algebra/command.rb | 46 ++++ lib/ruby_algebra/expression.rb | 375 ----------------------------- lib/ruby_algebra/parser.rb | 427 +++++++++++++++++++++------------ lib/ruby_algebra/polynomial.rb | 22 +- 5 files changed, 343 insertions(+), 532 deletions(-) create mode 100644 lib/ruby_algebra/command.rb delete mode 100644 lib/ruby_algebra/expression.rb diff --git a/lib/ruby_algebra.rb b/lib/ruby_algebra.rb index d017ed9..2664ef9 100644 --- a/lib/ruby_algebra.rb +++ b/lib/ruby_algebra.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true require_relative 'ruby_algebra/version' -require_relative 'ruby_algebra/expression' +require_relative 'ruby_algebra/command' require_relative 'ruby_algebra/polynomial' require_relative 'ruby_algebra/parser' - -module RubyAlgebra -end diff --git a/lib/ruby_algebra/command.rb b/lib/ruby_algebra/command.rb new file mode 100644 index 0000000..167162d --- /dev/null +++ b/lib/ruby_algebra/command.rb @@ -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 diff --git a/lib/ruby_algebra/expression.rb b/lib/ruby_algebra/expression.rb deleted file mode 100644 index 4307a1f..0000000 --- a/lib/ruby_algebra/expression.rb +++ /dev/null @@ -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 diff --git a/lib/ruby_algebra/parser.rb b/lib/ruby_algebra/parser.rb index 77e4b8b..bc6ccbc 100644 --- a/lib/ruby_algebra/parser.rb +++ b/lib/ruby_algebra/parser.rb @@ -2,199 +2,332 @@ module RubyAlgebra module Parser + # Токен выражения или команды class Token attr_reader :type + + def initialize; end end - class NumberToken < Token - attr_reader :value - - def initialize(value) - @type = :num - @value = value + # Знак сложения + class PlusToken < Token + def initialize + super() + @type = :plus end end - class IdentifierToken < Token - attr_reader :name - - def initialize(name) - @type = :id - @name = name + # Знак вычитания + class MinusToken < Token + def initialize + super() + @type = :minus end end - class BinaryOperatorToken < Token - attr_reader :op - - def initialize(op) - @type = :op - @op = op + # Знак умножения + class MultiplyToken < Token + def initialize + super() + @type = :mult end end - class UnaryOperatorToken < Token - attr_reader :op - - def initialize(op) - @type = :unary_op - @op = op + # Знак деления + class DivideToken < Token + def initialize + super() + @type = :div end end + # Знак возведения в степень + class PowerToken < Token + def initialize + super() + @type = :pow + end + end + + # Скобка class ParenthesisToken < Token attr_reader :closing def initialize(closing) + super() @type = :paren @closing = closing 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 - def initialize() + def initialize + super() @type = :end end end - def self.tokenize(e) - i = 0 - add_implicit_mul = false - while i < e.length - c = e[i] - if c.match?(/\p{Alpha}/) - yield BinaryOperatorToken.new(:mult) if add_implicit_mul - j = i + 1 - j += 1 while j < e.length && e[j].match?(/\p{Alnum}/) - add_implicit_mul = true - yield IdentifierToken.new(e[i...j]) - i = j - elsif c.match?(/[+\-*\/^()]/) - yield BinaryOperatorToken.new(:mult) if c == "(" && add_implicit_mul - i += 1 - case c - when "+" - if add_implicit_mul - yield BinaryOperatorToken.new(:add) - else - yield UnaryOperatorToken.new(:positive) + # Класс для разбора выражений на токены + class Tokenizer + attr_reader :lookahead + + 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}/) + puts 'letter' + j = @i + 1 + j += 1 while j < @expr.length && @expr[j].match?(/\p{Alnum}/) + token = IdentifierToken.new(@expr[@i...j]) + @i = j + return token + elsif c.match(%r{[+\-*/^():]}) + puts 'operator' + token = nil + case c + when '+' + token = PlusToken.new + when '-' + token = MinusToken.new + when '*' + token = MultiplyToken.new + when '/' + token = DivideToken.new + when '^' + token = PowerToken.new + when '(' + token = ParenthesisToken.new(false) + when ')' + token = ParenthesisToken.new(true) end - when "-" - if add_implicit_mul - yield BinaryOperatorToken.new(:sub) - else - yield UnaryOperatorToken.new(:negative) + @i += 1 + if c == ':' && @expr[@i] == '=' + token = AssignmentToken.new + @i += 1 end - when "*" - 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}|\./) - yield BinaryOperatorToken.new(:mult) if add_implicit_mul - j = i + 1 - j += 1 while j < e.length && e[j].match?(/\p{Digit}|\./) - s = e[i...j] - fl = s.count "." - if fl > 1 - raise Exception.new - end - add_implicit_mul = true - if fl == 0 - yield NumberToken.new(s.to_i) + return token + elsif c.match?(/\p{Digit}|\./) + puts 'digit' + j = @i + 1 + j += 1 while j < @expr.length && @expr[j].match?(/\p{Digit}|\./) + s = @expr[@i...j] + @i = j + puts "ssss: #{s}" + fl = s.count '.' + raise StandardError, 'malformed number' if fl > 1 + return fl.zero? ? NumberToken.new(s.to_i) : NumberToken.new(s.to_f) + elsif c == ' ' + puts 'whitespace' + @i += 1 else - yield NumberToken.new(s.to_f) + puts 'unknown' + @i += 1 end - i = j - elsif c == " " - i += 1 - else - i += 1 end end - yield EndToken.new end - def self.parse(expr) - operators = [] - out_stack = [] - tokenize(expr) do |token| - if token.type == :num - 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 - operators.pop - end + def self.parse_command(expr) + tokenizer = Tokenizer.new(expr) + n = tokenizer.lookahead + if n.type == :id && !n.variable? + parse_assignment_command(tokenizer) + else + parse_display_command(tokenizer) + end + end + + def self.parse_assignment_command(tokenizer) + left_hand_side = tokenizer.next_token + n = tokenizer.next_token + 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 + tokenizer.next_token + operator = tokenizer.next_token + raise StandardError, "unexpected token #{operator.type}, expected +, -, * or /" unless [:plus, :minus, :mult, :div].include?(operator.type) + + operand2 = tokenizer.next_token + unless ([:plus, :minus, :mult].include?(operator.type) && operand2.type == :id && !operand2.variable?) || + ([:mult, :div].include?(operator.type) && operand2.type == :num) + raise StandardError, 'unsupported operation or invalid syntax' + end + if operand2.type == :num + if operator.type == :mult + return AssignmentCommand.new(left_hand_side.symbol, :scale, operand1.symbol, operand2.value) else - operators.push token + 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 + return AssignmentCommand.new(left_hand_side.symbol, :mult, operand1.symbol, operand2.symbol) end - elsif token.type == :id - out_stack.push(Variable.new(token.name)) end - end - out_stack[0] - end - - def self._parse_make_op(operators, out_stack) - oper = operators.pop - unless (oper.type == :op && out_stack.length >= 2) || (oper.type == :unary_op && out_stack.length >= 1) - raise Exception.new - end - b = out_stack.pop if oper.type == :op - a = out_stack.pop - - case oper.op - when :add - out_stack.push Addition.new(a, b) - when :sub - out_stack.push Subtraction.new(a, b) - when :mult - out_stack.push Multiplication.new(a, b) - 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) + 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 - def self._parse_op_prio(op_token) - case op_token.op - when :add, :sub - 1 - when :mult, :div - 2 - when :pow, :positive, :negative - 3 + def self.parse_display_command(tokenizer) + DisplayCommand.new(parse_polynomial(tokenizer)) + end + + def self.parse_polynomial(tokenizer) + puts "parse_polynomial " + 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 " + 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 "" + 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 " + 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 diff --git a/lib/ruby_algebra/polynomial.rb b/lib/ruby_algebra/polynomial.rb index 007b1e3..1ddf2a9 100644 --- a/lib/ruby_algebra/polynomial.rb +++ b/lib/ruby_algebra/polynomial.rb @@ -171,7 +171,9 @@ module RubyAlgebra end 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 other.variables.each do |symbol, power| @@ -205,11 +207,19 @@ module RubyAlgebra unless @variables.empty? first = true @variables.each do |var, power| - result += if first - "#{var}^#{power}" - else - " * #{var}^#{power}" - end + if first + unless power == 1 + result += "#{var}^#{power}" + else + result += "#{var}" + end + else + unless power == 1 + result += " * #{var}^#{power}" + else + result += " * #{var}" + end + end first = false end end