From afb81b475af2fe4fe5d243cf38315ec6e542ac98 Mon Sep 17 00:00:00 2001 From: Slavasil Date: Tue, 31 Mar 2026 04:03:44 +0300 Subject: [PATCH 1/8] polynomials --- lib/ruby_algebra.rb | 7 +- lib/ruby_algebra/expression.rb | 43 +++++- lib/ruby_algebra/polynomial.rb | 232 +++++++++++++++++++++++++++++++++ 3 files changed, 272 insertions(+), 10 deletions(-) create mode 100644 lib/ruby_algebra/polynomial.rb diff --git a/lib/ruby_algebra.rb b/lib/ruby_algebra.rb index b9b54c4..d017ed9 100644 --- a/lib/ruby_algebra.rb +++ b/lib/ruby_algebra.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true -require_relative "ruby_algebra/version" -require_relative "ruby_algebra/expression" -require_relative "ruby_algebra/parser" +require_relative 'ruby_algebra/version' +require_relative 'ruby_algebra/expression' +require_relative 'ruby_algebra/polynomial' +require_relative 'ruby_algebra/parser' module RubyAlgebra end diff --git a/lib/ruby_algebra/expression.rb b/lib/ruby_algebra/expression.rb index 1871ccd..4307a1f 100644 --- a/lib/ruby_algebra/expression.rb +++ b/lib/ruby_algebra/expression.rb @@ -17,12 +17,15 @@ module RubyAlgebra 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 @@ -33,6 +36,7 @@ module RubyAlgebra def initialize(lhs, rhs) raise ArgumentError unless lhs.is_a?(Expression) && rhs.is_a?(Expression) + @lhs = lhs @rhs = rhs end @@ -59,18 +63,22 @@ module RubyAlgebra 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 @@ -94,12 +102,15 @@ module RubyAlgebra 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 @@ -110,6 +121,7 @@ module RubyAlgebra def initialize(lhs, rhs) raise ArgumentError unless lhs.is_a?(Expression) && rhs.is_a?(Expression) + @lhs = lhs @rhs = rhs end @@ -121,12 +133,10 @@ module RubyAlgebra 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.to_s}" + 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 - if lhs.type == :variable && rhs.type == :variable && lhs.single_letter? && rhs.single_letter? - result += " " - end + 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} * " @@ -148,6 +158,7 @@ module RubyAlgebra def ==(other) return false if other.type != type + lhs == other.lhs && rhs == other.rhs end @@ -158,12 +169,15 @@ module RubyAlgebra 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 @@ -181,6 +195,7 @@ module RubyAlgebra def type :div end + def diff(v) u_prime = @lhs.diff(v) v_prime = @rhs.diff(v) @@ -192,13 +207,16 @@ module RubyAlgebra 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 / rhs_val + + lhs_val.to_f / rhs_val end + def constant? @lhs.constant? && @rhs.constant? end @@ -209,6 +227,7 @@ module RubyAlgebra def initialize(base, exponent) raise ArgumentError unless base.is_a?(Expression) && exponent.is_a?(Expression) + @base = base @exponent = exponent end @@ -235,12 +254,13 @@ module RubyAlgebra 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, "Дифференцирование степени с неконстантным показателем не реализовано" + raise NotImplementedError, 'Дифференцирование степени с неконстантным показателем не реализовано' end n = @exponent.value @@ -250,12 +270,15 @@ module RubyAlgebra 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 + + base_val**exp_val end + def constant? @base.constant? && @exponent.constant? end @@ -286,15 +309,18 @@ module RubyAlgebra 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 @@ -330,15 +356,18 @@ module RubyAlgebra 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 diff --git a/lib/ruby_algebra/polynomial.rb b/lib/ruby_algebra/polynomial.rb new file mode 100644 index 0000000..007b1e3 --- /dev/null +++ b/lib/ruby_algebra/polynomial.rb @@ -0,0 +1,232 @@ +# frozen_string_literal: true + +module RubyAlgebra + # Многочлен - сумма одночленов + class Polynomial + # одночлены + attr_reader :terms + + def initialize(terms = []) + @terms = if terms.is_a? Term + [terms] + else + terms + end + simplify! + end + + def to_s + if @terms.empty? + result = '' + else + result = @terms[-1].to_s + i = @terms.length - 2 + while i >= 0 + result += if @terms[i].coeff > 0 + " + #{@terms[i]}" + else + " - #{-@terms[i]}" + end + i -= 1 + end + end + result + end + + def diff + raise NotImplementedError + end + + def evaluate(values) + result = 0 + @terms.each do |term| + # ... + end + result + end + + def +(other) + if other.is_a? Polynomial + Polynomial.new(@terms + other.terms).simplify + elsif other.is_a? Term + Polynomial.new(@terms + [other]).simplify + else + Polynomial.new(@terms + [Term.new(other)]).simplify + end + end + + def *(other) + if other.is_a? Polynomial + result = Polynomial.new + other.terms.each do |term| + result += self * term + end + result + else + Polynomial.new(@terms.map { |term| term * other }) + end + end + + def **(other) + result = Polynomial.new([Term.new(1)]) + other.times do + result *= self + end + result + end + + def -(other) + self + -other + end + + def +@ + self + end + + def -@ + self * -1 + end + + def ==(other) + @terms == other.terms + end + + def simplify + i = 0 + new_terms = @terms.filter { |t| !t.zero? } + while i < new_terms.length + j = i + 1 + while j < new_terms.length + if new_terms[i].similar_to?(new_terms[j]) + new_terms[i] += new_terms[j] + if new_terms[i].zero? + new_terms.delete_at(i) + i -= 1 + j -= 1 + end + new_terms.delete_at(j) + j -= 1 + end + j += 1 + end + i += 1 + end + Polynomial.new(new_terms.sort_by { |t| t.total_power }) + end + + def simplify! + i = 0 + new_terms = @terms.filter { |t| !t.zero? } + while i < new_terms.length + j = i + 1 + while j < new_terms.length + if new_terms[i].similar_to?(new_terms[j]) + new_terms[i] += new_terms[j] + if new_terms[i].zero? + new_terms.delete_at(i) + i -= 1 + j -= 1 + end + new_terms.delete_at(j) + j -= 1 + end + j += 1 + end + i += 1 + end + @terms = new_terms.sort_by { |t| t.total_power } + end + end + + # Одночлен в форме a_n * x_1^k_1 * ... * x_n^k_n + class Term + attr_reader :coeff, :variables + + def initialize(coeff = 0, variables = {}) + @coeff = coeff + @variables = {} + variables.each do |var, power| + next if power.zero? + + if @variables[var].nil? + @variables[var] = power + else + @variables[var] += power + end + end + end + + def diff(var) + raise NotImplementedError + end + + def evaluate(values) + raise NotImplementedError + end + + def +(other) + raise NotImplementedError, 'addition of non-similar terms is not implemented' unless similar_to?(other) + + Term.new(@coeff + other.coeff, @variables) + end + + def *(other) + return Term.new if @coeff.zero? || other.coeff.zero? + + new_variables = @variables.clone + other.variables.each do |symbol, power| + if new_variables[symbol].nil? + new_variables[symbol] = power + else + new_variables[symbol] += power + end + end + Term.new(@coeff * other.coeff, new_variables) + end + + def +@ + Term.new(@coeff, @variables) + end + + def -@ + Term.new(-@coeff, @variables) + end + + def ==(other) + @coeff == other.coeff && @variables == other.variables + end + + def total_power + @variables.values.sum + end + + def to_s + result = coeff.to_s + unless @variables.empty? + first = true + @variables.each do |var, power| + result += if first + "#{var}^#{power}" + else + " * #{var}^#{power}" + end + first = false + end + end + result + end + + def similar_to?(other) + return false if @variables.length != other.variables.length + + other.variables.each do |var, power| + return false if @variables[var] != power + end + true + end + + def zero? + @coeff.zero? + end + end +end From a420ba3ce2beb1f27aac60faee579f34069e5303 Mon Sep 17 00:00:00 2001 From: Slavasil Date: Mon, 13 Apr 2026 13:23:12 +0300 Subject: [PATCH 2/8] 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 From 7a6f8974a8d29134f4ee2750e7171fa4f9fe41ba Mon Sep 17 00:00:00 2001 From: Slavasil Date: Mon, 13 Apr 2026 13:58:42 +0300 Subject: [PATCH 3/8] fix display command parser --- lib/ruby_algebra/command.rb | 9 ++++++++- lib/ruby_algebra/parser.rb | 36 ++++++++++++++++-------------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/lib/ruby_algebra/command.rb b/lib/ruby_algebra/command.rb index 167162d..b6ad03c 100644 --- a/lib/ruby_algebra/command.rb +++ b/lib/ruby_algebra/command.rb @@ -17,7 +17,8 @@ module RubyAlgebra end def to_s - result = @lhs.is_a?(Array) ? @lhs.join(', ') : @lhs.to_s + result = "Assignment command: " + result += @lhs.is_a?(Array) ? @lhs.join(', ') : @lhs.to_s result += ' := ' result += @operand1.to_s case @operation @@ -42,5 +43,11 @@ module RubyAlgebra def initialize(polynomial) @polynomial = polynomial end + + def to_s + result = "Display command: " + result += @polynomial.to_s + result + end end end diff --git a/lib/ruby_algebra/parser.rb b/lib/ruby_algebra/parser.rb index bc6ccbc..656dfa5 100644 --- a/lib/ruby_algebra/parser.rb +++ b/lib/ruby_algebra/parser.rb @@ -189,21 +189,19 @@ module RubyAlgebra def self.parse_command(expr) tokenizer = Tokenizer.new(expr) + n = tokenizer.lookahead if n.type == :id && !n.variable? - parse_assignment_command(tokenizer) + left_hand_side = tokenizer.next_token.symbol else - parse_display_command(tokenizer) + return DisplayCommand.new(parse_polynomial(tokenizer)) end - end - def self.parse_assignment_command(tokenizer) - left_hand_side = tokenizer.next_token - n = tokenizer.next_token - if n.type == :assign + if tokenizer.lookahead.type == :assign + n = tokenizer.next_token operand1 = tokenizer.lookahead unless operand1.type == :id && !operand1.variable? - return AssignmentCommand.new(left_hand_side.symbol, :assign, parse_polynomial(tokenizer), nil) + return AssignmentCommand.new(left_hand_side, :assign, parse_polynomial(tokenizer), nil) end tokenizer.next_token operator = tokenizer.next_token @@ -216,21 +214,22 @@ module RubyAlgebra end if operand2.type == :num if operator.type == :mult - return AssignmentCommand.new(left_hand_side.symbol, :scale, operand1.symbol, operand2.value) + return AssignmentCommand.new(left_hand_side, :scale, operand1.symbol, operand2.value) else - return AssignmentCommand.new(left_hand_side.symbol, :scale, operand1.symbol, 1.0 / operand2.value) + return AssignmentCommand.new(left_hand_side, :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) + return AssignmentCommand.new(left_hand_side, :add, operand1.symbol, operand2.symbol) when :minus - return AssignmentCommand.new(left_hand_side.symbol, :sub, operand1.symbol, operand2.symbol) + return AssignmentCommand.new(left_hand_side, :sub, operand1.symbol, operand2.symbol) when :mult - return AssignmentCommand.new(left_hand_side.symbol, :mult, operand1.symbol, operand2.symbol) + return AssignmentCommand.new(left_hand_side, :mult, operand1.symbol, operand2.symbol) end end - elsif n.type == :comma + elsif tokenizer.lookahead.type == :comma + n = tokenizer.next_token second_lhs = tokenizer.next_token unless second_lhs.type == :id && !second_lhs.variable? raise StandardError, "unexpected token #{second_lhs.type}, expected polynomial variable" @@ -249,16 +248,14 @@ module RubyAlgebra 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) + return AssignmentCommand.new([left_hand_side, second_lhs.symbol], :div, operand1.symbol) + elsif tokenizer.lookahead.type == :end + return DisplayCommand.new(left_hand_side) else raise StandardError, "unexpected token #{n.type}, expected ':=' or ','" end end - def self.parse_display_command(tokenizer) - DisplayCommand.new(parse_polynomial(tokenizer)) - end - def self.parse_polynomial(tokenizer) puts "parse_polynomial " terms = [parse_term(tokenizer)] @@ -305,7 +302,6 @@ module RubyAlgebra 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 *' From d8a24a13490b5f80cc6f47d0f61bdd526a4de2ae Mon Sep 17 00:00:00 2001 From: Slavasil Date: Mon, 13 Apr 2026 13:59:04 +0300 Subject: [PATCH 4/8] fix 1x being printed instead of x --- lib/ruby_algebra/polynomial.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ruby_algebra/polynomial.rb b/lib/ruby_algebra/polynomial.rb index 1ddf2a9..5d5faa6 100644 --- a/lib/ruby_algebra/polynomial.rb +++ b/lib/ruby_algebra/polynomial.rb @@ -203,7 +203,7 @@ module RubyAlgebra end def to_s - result = coeff.to_s + result = @coeff != 1 ? @coeff.to_s : '' unless @variables.empty? first = true @variables.each do |var, power| From 286e79f21ad05590cb26ca80611f9c707cdc0680 Mon Sep 17 00:00:00 2001 From: Slavasil Date: Mon, 13 Apr 2026 14:57:03 +0300 Subject: [PATCH 5/8] add diff command variant --- lib/ruby_algebra/command.rb | 27 ++++++----- lib/ruby_algebra/parser.rb | 89 ++++++++++++++++++++++++++----------- 2 files changed, 78 insertions(+), 38 deletions(-) diff --git a/lib/ruby_algebra/command.rb b/lib/ruby_algebra/command.rb index b6ad03c..c9f517e 100644 --- a/lib/ruby_algebra/command.rb +++ b/lib/ruby_algebra/command.rb @@ -19,19 +19,22 @@ module RubyAlgebra def to_s result = "Assignment command: " 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 += ' / ' + if @operation == :diff + result += ' := Diff(' + @operand1.to_s + @operand2.map { |v| ", #{v}"}.join + ')' + else + 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 end - result += @operand2.to_s if @operation != :assign result end end diff --git a/lib/ruby_algebra/parser.rb b/lib/ruby_algebra/parser.rb index 656dfa5..80ef029 100644 --- a/lib/ruby_algebra/parser.rb +++ b/lib/ruby_algebra/parser.rb @@ -141,7 +141,7 @@ module RubyAlgebra token = IdentifierToken.new(@expr[@i...j]) @i = j return token - elsif c.match(%r{[+\-*/^():]}) + elsif c.match(%r{[+\-*/^():,]}) puts 'operator' token = nil case c @@ -159,6 +159,8 @@ module RubyAlgebra token = ParenthesisToken.new(false) when ')' token = ParenthesisToken.new(true) + when ',' + token = CommaToken.new end @i += 1 if c == ':' && @expr[@i] == '=' @@ -200,33 +202,68 @@ module RubyAlgebra if tokenizer.lookahead.type == :assign n = tokenizer.next_token operand1 = tokenizer.lookahead - unless operand1.type == :id && !operand1.variable? - return AssignmentCommand.new(left_hand_side, :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, :scale, operand1.symbol, operand2.value) + if operand1.type == :id && !operand1.variable? + if operand1.symbol == 'Diff' + tokenizer.next_token + n = tokenizer.next_token + unless n.type == :paren && n.closing == false + raise StandardError, "unexpected token #{n.type}, expected (" + end + n = tokenizer.lookahead + if n.type == :id && !n.variable? + tokenizer.next_token + target = n.symbol + else + target = parse_polynomial(tokenizer) + end + diff_variables = [] + while tokenizer.lookahead.type == :comma + tokenizer.next_token + n = tokenizer.next_token + unless n.type == :id && n.variable? + raise StandardError, "unexpected token #{n.type}, expected variable" + end + diff_variables << n.symbol + end + n = tokenizer.next_token + unless n.type == :paren && n.closing == true + raise StandardError, "unexpected token #{n.type}, expected )" + end + unless tokenizer.next_token.type == :end + raise StandardError, "unexpected token at the end" + end + return AssignmentCommand.new(left_hand_side, :diff, target, diff_variables) + elsif operand1.symbol == 'Subs' + raise NotImplementedError else - return AssignmentCommand.new(left_hand_side, :scale, operand1.symbol, 1.0 / operand2.value) - end - elsif operand2.type == :id - case operator.type - when :plus - return AssignmentCommand.new(left_hand_side, :add, operand1.symbol, operand2.symbol) - when :minus - return AssignmentCommand.new(left_hand_side, :sub, operand1.symbol, operand2.symbol) - when :mult - return AssignmentCommand.new(left_hand_side, :mult, operand1.symbol, operand2.symbol) + 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, :scale, operand1.symbol, operand2.value) + else + return AssignmentCommand.new(left_hand_side, :scale, operand1.symbol, 1.0 / operand2.value) + end + elsif operand2.type == :id + case operator.type + when :plus + return AssignmentCommand.new(left_hand_side, :add, operand1.symbol, operand2.symbol) + when :minus + return AssignmentCommand.new(left_hand_side, :sub, operand1.symbol, operand2.symbol) + when :mult + return AssignmentCommand.new(left_hand_side, :mult, operand1.symbol, operand2.symbol) + end + end end + else + return AssignmentCommand.new(left_hand_side, :assign, parse_polynomial(tokenizer), nil) end elsif tokenizer.lookahead.type == :comma n = tokenizer.next_token From f283d039c1abd108a2413fbbc7f81f04d08cdad8 Mon Sep 17 00:00:00 2001 From: Slavasil Date: Mon, 13 Apr 2026 15:05:52 +0300 Subject: [PATCH 6/8] add subs command variant --- lib/ruby_algebra/command.rb | 2 ++ lib/ruby_algebra/parser.rb | 50 +++++++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/lib/ruby_algebra/command.rb b/lib/ruby_algebra/command.rb index c9f517e..3a7c643 100644 --- a/lib/ruby_algebra/command.rb +++ b/lib/ruby_algebra/command.rb @@ -21,6 +21,8 @@ module RubyAlgebra result += @lhs.is_a?(Array) ? @lhs.join(', ') : @lhs.to_s if @operation == :diff result += ' := Diff(' + @operand1.to_s + @operand2.map { |v| ", #{v}"}.join + ')' + elsif @operation == :subs + result += ' := Subs(' + @operand1.to_s + @operand2.map { |variable, value| ", #{variable}=#{value}"}.join + ')' else result += ' := ' + @operand1.to_s case @operation diff --git a/lib/ruby_algebra/parser.rb b/lib/ruby_algebra/parser.rb index 80ef029..5ff60d5 100644 --- a/lib/ruby_algebra/parser.rb +++ b/lib/ruby_algebra/parser.rb @@ -68,6 +68,14 @@ module RubyAlgebra end end + # Знак равенства = + class EqualsToken < Token + def initialize + super() + @type = :equals + end + end + # Запятая class CommaToken < Token def initialize @@ -141,7 +149,7 @@ module RubyAlgebra token = IdentifierToken.new(@expr[@i...j]) @i = j return token - elsif c.match(%r{[+\-*/^():,]}) + elsif c.match(%r{[+\-*/^():,=]}) puts 'operator' token = nil case c @@ -161,6 +169,8 @@ module RubyAlgebra token = ParenthesisToken.new(true) when ',' token = CommaToken.new + when '=' + token = EqualsToken.new end @i += 1 if c == ':' && @expr[@i] == '=' @@ -234,7 +244,43 @@ module RubyAlgebra end return AssignmentCommand.new(left_hand_side, :diff, target, diff_variables) elsif operand1.symbol == 'Subs' - raise NotImplementedError + tokenizer.next_token + n = tokenizer.next_token + unless n.type == :paren && n.closing == false + raise StandardError, "unexpected token #{n.type}, expected (" + end + n = tokenizer.lookahead + if n.type == :id && !n.variable? + tokenizer.next_token + target = n.symbol + else + target = parse_polynomial(tokenizer) + end + substitutions = {} + while tokenizer.lookahead.type == :comma + tokenizer.next_token + n = tokenizer.next_token + unless n.type == :id && n.variable? + raise StandardError, "unexpected token #{n.type}, expected variable" + end + variable = n.symbol + unless tokenizer.next_token.type == :equals + raise StandardError, "unexpected token #{n.type}, expected =" + end + n = tokenizer.next_token + unless n.type == :num + raise StandardError, "unexpected token #{n.type}, expected number" + end + substitutions[variable] = n.value + end + n = tokenizer.next_token + unless n.type == :paren && n.closing == true + raise StandardError, "unexpected token #{n.type}, expected )" + end + unless tokenizer.next_token.type == :end + raise StandardError, "unexpected token at the end" + end + return AssignmentCommand.new(left_hand_side, :subs, target, substitutions) else tokenizer.next_token operator = tokenizer.next_token From 1fff529e8381530418940c214288b262b831a40f Mon Sep 17 00:00:00 2001 From: Slavasil Date: Mon, 13 Apr 2026 19:33:50 +0300 Subject: [PATCH 7/8] implement interpreter and demo + bug fixes - implement interpreter and demo - replace StandardError with appropriate subclasses - implement division - rewrite Polynomial.simplify() --- bin/demo | 34 ++++++---- lib/ruby_algebra.rb | 1 + lib/ruby_algebra/command.rb | 22 +++++-- lib/ruby_algebra/interpreter.rb | 70 ++++++++++++++++++++ lib/ruby_algebra/parser.rb | 84 ++++++++++-------------- lib/ruby_algebra/polynomial.rb | 109 ++++++++++++++++++++++---------- 6 files changed, 221 insertions(+), 99 deletions(-) create mode 100644 lib/ruby_algebra/interpreter.rb diff --git a/bin/demo b/bin/demo index 4f0f08a..c08072a 100755 --- a/bin/demo +++ b/bin/demo @@ -4,21 +4,27 @@ require 'bundler/setup' require 'ruby_algebra' -puts 'RubyMaple © RubyMaple Creators, 2026. No rights reserved.' +print "RubyMaple © RubyMaple Creators, 2026. No rights reserved.\nPress Ctrl+D to quit.\n" +interp = RubyAlgebra::Interpreter.new loop do - puts 'Введите формулу (пустая строка для выхода): ' - inp = $stdin.gets.chomp - exit if inp.empty? - begin - formula = RubyAlgebra::Parser.parse(inp) - rescue StandardError - puts 'Ошибка синтаксиса' - end - puts "Формула: #{formula}" - puts "Производная по x: #{formula.diff('x')}" - puts "Производная по y: #{formula.diff('y')}" + print '> ' + inp = $stdin.gets + break if inp.nil? - value = formula.evaluate - puts "Числовое значение: #{value}" unless value.nil? + inp = inp.chomp + next if inp.empty? + + begin + command = RubyAlgebra::Parser.parse_command(inp) + rescue StandardError => e + puts "Syntax error: #{e.message}" + next + end + begin + puts "< #{interp.execute(command)}" + rescue StandardError => e + puts "Calculation error: #{e.message}" + end end +puts diff --git a/lib/ruby_algebra.rb b/lib/ruby_algebra.rb index 2664ef9..d36913d 100644 --- a/lib/ruby_algebra.rb +++ b/lib/ruby_algebra.rb @@ -4,3 +4,4 @@ require_relative 'ruby_algebra/version' require_relative 'ruby_algebra/command' require_relative 'ruby_algebra/polynomial' require_relative 'ruby_algebra/parser' +require_relative 'ruby_algebra/interpreter' diff --git a/lib/ruby_algebra/command.rb b/lib/ruby_algebra/command.rb index 3a7c643..0a551d4 100644 --- a/lib/ruby_algebra/command.rb +++ b/lib/ruby_algebra/command.rb @@ -3,6 +3,11 @@ module RubyAlgebra # Команда интерпретатора class Command + attr_reader :type + + def type + raise NotImplementedError + end end # Команда присвоения @@ -39,20 +44,29 @@ module RubyAlgebra end result end + + def type + :assignment + end end # Команда вывода на экран class DisplayCommand < Command - attr_reader :polynomial + attr_reader :item - def initialize(polynomial) - @polynomial = polynomial + def initialize(item) + @type = :display + @item = item end def to_s result = "Display command: " - result += @polynomial.to_s + result += @item.to_s result end + + def type + :display + end end end diff --git a/lib/ruby_algebra/interpreter.rb b/lib/ruby_algebra/interpreter.rb new file mode 100644 index 0000000..f8e8098 --- /dev/null +++ b/lib/ruby_algebra/interpreter.rb @@ -0,0 +1,70 @@ +module RubyAlgebra + class Interpreter + def initialize + @locals = {} + end + + def execute(command) + case command.type + when :assignment + unless command.operand1.is_a?(Polynomial) + operand1 = @locals[command.operand1] + return "Undeclared polynomial: #{command.operand1}" if operand1.nil? + else + operand1 = command.operand1 + end + + unless [:assign, :scale, :diff, :subs].include?(command.operation) + operand2 = @locals[command.operand2] + return "Undeclared polynomial: #{command.operand2}" if operand2.nil? + else + operand2 = command.operand2 + end + + case command.operation + when :assign + @locals[command.lhs] = operand1 + return "#{command.lhs} = #{operand1}" + when :add + result = operand1 + operand2 + @locals[command.lhs] = result + return "#{command.lhs} = #{result}" + when :sub + result = operand1 - operand2 + @locals[command.lhs] = result + return "#{command.lhs} = #{result}" + when :mult, :scale + result = operand1 * operand2 + @locals[command.lhs] = result + return "#{command.lhs} = #{result}" + when :div + result = operand1 / operand2 + @locals[command.lhs[0]] = result[0] + @locals[command.lhs[1]] = result[1] + return "Quotient: #{command.lhs[0]} = #{result[0]}, remainder: #{command.lhs[1]} = #{result[1]}" + when :diff + result = operand1 + operand2.each do |variable| + result = result.diff(variable) + end + @locals[command.lhs] = result + return "#{command.lhs} = #{result}" + when :subs + result = operand1.evaluate(operand2) + @locals[command.lhs] = result + return "#{command.lhs} = #{result}" + end + when :display + unless command.item.is_a? Polynomial + unless @locals[command.item].nil? + return @locals[command.item].to_s + else + return "Undeclared polynomial: #{command.item}" + end + else + return command.item.to_s + end + end + end + end +end diff --git a/lib/ruby_algebra/parser.rb b/lib/ruby_algebra/parser.rb index 5ff60d5..0c8f328 100644 --- a/lib/ruby_algebra/parser.rb +++ b/lib/ruby_algebra/parser.rb @@ -118,6 +118,9 @@ module RubyAlgebra end end + class TokenizerError < StandardError; end + class ParserError < StandardError; end + # Класс для разбора выражений на токены class Tokenizer attr_reader :lookahead @@ -135,22 +138,16 @@ module RubyAlgebra 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 '+' @@ -179,21 +176,17 @@ module RubyAlgebra end 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 + raise TokenizerError, '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 - puts 'unknown' - @i += 1 + raise TokenizerError, "unrecognized character '#{c}'" end end end @@ -217,7 +210,7 @@ module RubyAlgebra tokenizer.next_token n = tokenizer.next_token unless n.type == :paren && n.closing == false - raise StandardError, "unexpected token #{n.type}, expected (" + raise ParserError, "unexpected token #{n.type}, expected (" end n = tokenizer.lookahead if n.type == :id && !n.variable? @@ -231,23 +224,23 @@ module RubyAlgebra tokenizer.next_token n = tokenizer.next_token unless n.type == :id && n.variable? - raise StandardError, "unexpected token #{n.type}, expected variable" + raise ParserError, "unexpected token #{n.type}, expected variable" end diff_variables << n.symbol end n = tokenizer.next_token unless n.type == :paren && n.closing == true - raise StandardError, "unexpected token #{n.type}, expected )" + raise ParserError, "unexpected token #{n.type}, expected )" end unless tokenizer.next_token.type == :end - raise StandardError, "unexpected token at the end" + raise ParserError, "unexpected token at the end" end return AssignmentCommand.new(left_hand_side, :diff, target, diff_variables) elsif operand1.symbol == 'Subs' tokenizer.next_token n = tokenizer.next_token unless n.type == :paren && n.closing == false - raise StandardError, "unexpected token #{n.type}, expected (" + raise ParserError, "unexpected token #{n.type}, expected (" end n = tokenizer.lookahead if n.type == :id && !n.variable? @@ -261,35 +254,35 @@ module RubyAlgebra tokenizer.next_token n = tokenizer.next_token unless n.type == :id && n.variable? - raise StandardError, "unexpected token #{n.type}, expected variable" + raise ParserError, "unexpected token #{n.type}, expected variable" end variable = n.symbol unless tokenizer.next_token.type == :equals - raise StandardError, "unexpected token #{n.type}, expected =" + raise ParserError, "unexpected token #{n.type}, expected =" end n = tokenizer.next_token unless n.type == :num - raise StandardError, "unexpected token #{n.type}, expected number" + raise ParserError, "unexpected token #{n.type}, expected number" end substitutions[variable] = n.value end n = tokenizer.next_token unless n.type == :paren && n.closing == true - raise StandardError, "unexpected token #{n.type}, expected )" + raise ParserError, "unexpected token #{n.type}, expected )" end unless tokenizer.next_token.type == :end - raise StandardError, "unexpected token at the end" + raise ParserError, "unexpected token at the end" end return AssignmentCommand.new(left_hand_side, :subs, target, substitutions) else tokenizer.next_token operator = tokenizer.next_token - raise StandardError, "unexpected token #{operator.type}, expected +, -, * or /" unless [:plus, :minus, :mult, :div].include?(operator.type) + raise ParserError, "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' + raise ParserError, 'unsupported operation or invalid syntax' end if operand2.type == :num if operator.type == :mult @@ -315,49 +308,52 @@ module RubyAlgebra n = tokenizer.next_token second_lhs = tokenizer.next_token unless second_lhs.type == :id && !second_lhs.variable? - raise StandardError, "unexpected token #{second_lhs.type}, expected polynomial variable" + raise ParserError, "unexpected token #{second_lhs.type}, expected polynomial variable" end unless tokenizer.next_token.type == :assign - raise StandardError, "unexpected token, expected :=" + raise ParserError, "unexpected token, expected :=" end operand1 = tokenizer.next_token unless operand1.type == :id && !operand1.variable? - raise StandardError, "unexpected token #{operand1.type}, expected polynomial variable" + raise ParserError, "unexpected token #{operand1.type}, expected polynomial variable" end unless tokenizer.next_token.type == :div - raise StandardError, "unexpected token, expected /" + raise ParserError, "unexpected token, expected /" end operand2 = tokenizer.next_token unless operand2.type == :id && !operand2.variable? - raise StandardError, "unexpected token #{operand2.type}, expected polynomial variable" + raise ParserError, "unexpected token #{operand2.type}, expected polynomial variable" end - return AssignmentCommand.new([left_hand_side, second_lhs.symbol], :div, operand1.symbol) + return AssignmentCommand.new([left_hand_side, second_lhs.symbol], :div, operand1.symbol, operand2.symbol) elsif tokenizer.lookahead.type == :end return DisplayCommand.new(left_hand_side) else - raise StandardError, "unexpected token #{n.type}, expected ':=' or ','" + raise ParserError, "unexpected token #{n.type}, expected ':=' or ','" end end def self.parse_polynomial(tokenizer) - puts "parse_polynomial " - terms = [parse_term(tokenizer)] + first_term_negative = false + if tokenizer.lookahead.type == :plus || tokenizer.lookahead.type == :minus + first_term_negative = tokenizer.lookahead.type == :minus + tokenizer.next_token + end + first_term = parse_term(tokenizer) + first_term *= -1 if first_term_negative + terms = [first_term] 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" + raise ParserError, "unexpected token #{n.type}, expected number or variable" end if n.type == :num - puts "eat coeff" tokenizer.next_token coeff = n.value variables = {} @@ -368,44 +364,34 @@ module RubyAlgebra 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' 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" + raise ParserError, "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" + raise ParserError, "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 diff --git a/lib/ruby_algebra/polynomial.rb b/lib/ruby_algebra/polynomial.rb index 5d5faa6..7c2fb9e 100644 --- a/lib/ruby_algebra/polynomial.rb +++ b/lib/ruby_algebra/polynomial.rb @@ -17,7 +17,7 @@ module RubyAlgebra def to_s if @terms.empty? - result = '' + result = '0' else result = @terms[-1].to_s i = @terms.length - 2 @@ -37,7 +37,7 @@ module RubyAlgebra raise NotImplementedError end - def evaluate(values) + def substitute(values) result = 0 @terms.each do |term| # ... @@ -67,6 +67,50 @@ module RubyAlgebra end end + def /(other) + raise StandardError, "cannot divide by zero" if other.zero? + return [Polynomial.new, Polynomial.new] if self.zero? + + # Сначала проверить, что оба многочлена от одной и той же переменной + t = if @terms.empty? || @terms[-1].variables.empty? then other.terms[-1] else @terms[-1] end + unless t.variables.empty? + only_variable = t.variables.keys[0] + @terms.each do |term| + next if term.variables.empty? + unless term.variables.length == 1 && term.variables.keys[0] == only_variable + raise StandardError, "cannot divide polynomials with different variables" + end + end + other.terms.each do |term| + next if term.variables.empty? + unless term.variables.length == 1 && term.variables.keys[0] == only_variable + raise StandardError, "cannot divide polynomials with different variables" + end + end + end + + quotient = Polynomial.new + remainder = self + divisor_power = unless other.terms.last.variables.empty? then other.terms.last.variables.first[1] else 0 end + loop do + break if remainder.zero? + + dividend_power = unless remainder.terms.last.variables.empty? then remainder.terms.last.variables.first[1] else 0 end + break if dividend_power < divisor_power + + result_power = dividend_power - divisor_power + result_coeff = remainder.terms.last.coeff.to_f / other.terms.last.coeff + if result_power > 0 + q = Term.new(result_coeff, {only_variable => result_power}) + else + q = Term.new(result_coeff, {}) + end + quotient += q + remainder -= other * q + end + [quotient, remainder] + end + def **(other) result = Polynomial.new([Term.new(1)]) other.times do @@ -91,50 +135,40 @@ module RubyAlgebra @terms == other.terms end - def simplify + def _simplify i = 0 new_terms = @terms.filter { |t| !t.zero? } while i < new_terms.length j = i + 1 + annihilated = false while j < new_terms.length if new_terms[i].similar_to?(new_terms[j]) new_terms[i] += new_terms[j] + new_terms.delete_at(j) if new_terms[i].zero? new_terms.delete_at(i) - i -= 1 - j -= 1 + annihilated = true + break end - new_terms.delete_at(j) - j -= 1 + else + j += 1 end - j += 1 end - i += 1 + i += 1 unless annihilated end - Polynomial.new(new_terms.sort_by { |t| t.total_power }) + new_terms.sort_by { |t| t.total_power } + end + + def simplify + Polynomial.new(_simplify) end def simplify! - i = 0 - new_terms = @terms.filter { |t| !t.zero? } - while i < new_terms.length - j = i + 1 - while j < new_terms.length - if new_terms[i].similar_to?(new_terms[j]) - new_terms[i] += new_terms[j] - if new_terms[i].zero? - new_terms.delete_at(i) - i -= 1 - j -= 1 - end - new_terms.delete_at(j) - j -= 1 - end - j += 1 - end - i += 1 - end - @terms = new_terms.sort_by { |t| t.total_power } + @terms = _simplify + end + + def zero? + @terms.all? { |t| t.zero? } end end @@ -160,7 +194,7 @@ module RubyAlgebra raise NotImplementedError end - def evaluate(values) + def substitute(values) raise NotImplementedError end @@ -186,6 +220,11 @@ module RubyAlgebra Term.new(@coeff * other.coeff, new_variables) end + def /(other) + raise StandardError, "cannot divide by zero" if other.zero? + + end + def +@ Term.new(@coeff, @variables) end @@ -203,7 +242,13 @@ module RubyAlgebra end def to_s - result = @coeff != 1 ? @coeff.to_s : '' + if @coeff == 1 && !@variables.empty? + result = '' + elsif @coeff == -1 && !@variables.empty? + result = '-' + else + result = @coeff.to_s + end unless @variables.empty? first = true @variables.each do |var, power| From 037c34b38e228d3eb05256a797d7ca465959dcdf Mon Sep 17 00:00:00 2001 From: Slavasil Date: Mon, 13 Apr 2026 19:58:40 +0300 Subject: [PATCH 8/8] add substitution and differentiation --- lib/ruby_algebra/interpreter.rb | 2 +- lib/ruby_algebra/parser.rb | 9 ++++++++- lib/ruby_algebra/polynomial.rb | 32 ++++++++++++++++++++++---------- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/lib/ruby_algebra/interpreter.rb b/lib/ruby_algebra/interpreter.rb index f8e8098..d9228cc 100644 --- a/lib/ruby_algebra/interpreter.rb +++ b/lib/ruby_algebra/interpreter.rb @@ -50,7 +50,7 @@ module RubyAlgebra @locals[command.lhs] = result return "#{command.lhs} = #{result}" when :subs - result = operand1.evaluate(operand2) + result = operand1.substitute(operand2) @locals[command.lhs] = result return "#{command.lhs} = #{result}" end diff --git a/lib/ruby_algebra/parser.rb b/lib/ruby_algebra/parser.rb index 0c8f328..12c6419 100644 --- a/lib/ruby_algebra/parser.rb +++ b/lib/ruby_algebra/parser.rb @@ -261,10 +261,17 @@ module RubyAlgebra raise ParserError, "unexpected token #{n.type}, expected =" end n = tokenizer.next_token + unless n.type == :num || n.type == :plus || n.type == :minus + raise ParserError, "unexpected token #{n.type}, expected -, + or number" + end + if n.type == :plus || n.type == :minus + negative = true if n.type == :minus + n = tokenizer.next_token + end unless n.type == :num raise ParserError, "unexpected token #{n.type}, expected number" end - substitutions[variable] = n.value + substitutions[variable] = if negative then -n.value else n.value end end n = tokenizer.next_token unless n.type == :paren && n.closing == true diff --git a/lib/ruby_algebra/polynomial.rb b/lib/ruby_algebra/polynomial.rb index 7c2fb9e..c8ab8bd 100644 --- a/lib/ruby_algebra/polynomial.rb +++ b/lib/ruby_algebra/polynomial.rb @@ -33,16 +33,12 @@ module RubyAlgebra result end - def diff - raise NotImplementedError + def diff(variable) + Polynomial.new(@terms.map { |term| term.diff(variable) }) end def substitute(values) - result = 0 - @terms.each do |term| - # ... - end - result + Polynomial.new(@terms.map { |term| term.substitute(values) }) end def +(other) @@ -190,12 +186,28 @@ module RubyAlgebra end end - def diff(var) - raise NotImplementedError + def diff(variable) + return Term.new if @variables[variable].nil? + new_coeff = @coeff + new_variables = @variables.filter { |var, power| var != variable } + unless @variables[variable] == 1 + new_coeff *= @variables[variable] + new_variables[variable] = @variables[variable] - 1 + end + Term.new(new_coeff, new_variables) end def substitute(values) - raise NotImplementedError + new_coeff = @coeff + new_variables = {} + @variables.each do |var, pow| + if values[var].nil? + new_variables[var] = pow + else + new_coeff *= values[var] ** pow + end + end + Term.new(new_coeff, new_variables) end def +(other)