演算子の優先度できたよ!

昨日のメモの通りでいけた。
ついでに、クラス化したり、単行演算子も実装したりした。

class Calc
	class Operator
		def initialize( priority, proc )
			@priority = priority
			@proc = proc
		end
		attr_reader :priority, :proc
	end
	
	class TokenType
		def initialize( regex )
			@regex = regex
			@prev_white_list = []
		end
		
		attr_reader :regex
		attr_accessor :prev_white_list
	end
	
	class Token
		def initialize( type, val = nil )
			@type = type
			@val = val
		end
		attr_reader :type, :val
	end
	
	class Error < StandardError ; end
	
	BINARY_OPERATORS = {
		'**' => Operator.new( 4, proc{|a,b| a**b } ),
		'*' => Operator.new( 2, proc{|a,b| a*b } ),
		'/' => Operator.new( 2, proc{|a,b| a/b } ),
		'%' => Operator.new( 2, proc{|a,b| a%b } ),
		'+' => Operator.new( 1, proc{|a,b| a+b } ),
		'-' => Operator.new( 1, proc{|a,b| a-b } ),
	}
	
	UNARY_OPERATORS = {
		'+' => Operator.new( 5, proc{|a| +a } ),
		'-' => Operator.new( 3, proc{|a| -a } ),
	}
	
	TOKEN_TYPES = [
		TOKEN_TYPE_SPACE = TokenType.new( /\s+/ ),
		TOKEN_TYPE_BINARY_OPERATOR = TokenType.new( Regexp.new( 
			BINARY_OPERATORS.keys.sort_by{|v| -v.size }.map{|v| Regexp.escape(v) }.join( '|' ) ) ),
		TOKEN_TYPE_UNARY_OPERATOR = TokenType.new( Regexp.new( 
			UNARY_OPERATORS.keys.sort_by{|v| -v.size }.map{|v| Regexp.escape(v) }.join( '|' ) ) ),
		TOKEN_TYPE_NUM = TokenType.new( /-?[0-9]+(?:\.[0-9]+)?/ ),
		TOKEN_TYPE_LEFT_BRACKETS = TokenType.new( /\(/ ),
		TOKEN_TYPE_RIGHT_BRACKETS = TokenType.new( /\)/ ),
	]

	[TOKEN_TYPE_NUM, TOKEN_TYPE_UNARY_OPERATOR, TOKEN_TYPE_LEFT_BRACKETS].each do |i|
		i.prev_white_list = 
			[nil, TOKEN_TYPE_LEFT_BRACKETS, TOKEN_TYPE_BINARY_OPERATOR, TOKEN_TYPE_UNARY_OPERATOR]
	end
	
	[TOKEN_TYPE_BINARY_OPERATOR, TOKEN_TYPE_RIGHT_BRACKETS].each do |i|
		i.prev_white_list = 
			[TOKEN_TYPE_NUM, TOKEN_TYPE_RIGHT_BRACKETS]
	end

	def calculate( expr )
		lex( expr )
		return calculate_rpn( ToRpnConverter.new.convert( @tokens ) )
	end
	
	def self.calculate( expr )
		return self.new.calculate( expr )
	end
	
	private
	def lex( expr )
		@tokens = []
		i = 0
		prev_type = nil
		while expr[i]
			find = nil
			TOKEN_TYPES.each do |token_type|
				find = ( /\A#{token_type.regex}/ =~ expr[i..-1] )
				unless token_type == TOKEN_TYPE_SPACE
					find = nil if find && !token_type.prev_white_list.include?( prev_type )
				end
				if find
					case token_type
					when TOKEN_TYPE_BINARY_OPERATOR, TOKEN_TYPE_UNARY_OPERATOR
						@tokens << Token.new( token_type, $& )
					when TOKEN_TYPE_NUM
						@tokens << Token.new( token_type, $&.to_f )
					when TOKEN_TYPE_LEFT_BRACKETS, TOKEN_TYPE_RIGHT_BRACKETS
						@tokens << Token.new( token_type )
					end
					i += find + $&.size
					prev_type = token_type unless token_type == TOKEN_TYPE_SPACE
					break
				end
			end
			raise Error, '構文エラーです' unless find
		end
	end
	
	# 逆ポーランド記法(Reversed Polish Notation)への変換器
	class ToRpnConverter
		def convert( tokens )
			@tokens = tokens
			@rpn_tokens = []
			@rpn_tokens_ptr = 0
			@prev_type = nil
			@rs_stack = []
			@brackets_level = 0
			
			@tokens.each_with_index do |@token,@tokens_i|
				case @token.type
				when TOKEN_TYPE_NUM
					insert_rpn_tokens
					@rpn_tokens_ptr += 1
					if [TOKEN_TYPE_BINARY_OPERATOR, TOKEN_TYPE_UNARY_OPERATOR].include?( @prev_type )
						# 右オペランドの場合
						rop_exit
					end
				when TOKEN_TYPE_BINARY_OPERATOR, TOKEN_TYPE_UNARY_OPERATOR
					insert_rpn_tokens
					@rs_stack.push( @brackets_level )
				when TOKEN_TYPE_LEFT_BRACKETS
					@brackets_level += 1
				when TOKEN_TYPE_RIGHT_BRACKETS
					raise Error, '不正な閉じカッコです' unless @brackets_level > 0
					@brackets_level -= 1
					rop_exit
				else
					raise Error, '不明なトークンです'
				end
				@prev_type = @token.type
				#debug_print
			end
			raise Error, '式が途中で終了しています' unless @rpn_tokens.size == @rpn_tokens_ptr
			raise Error, 'カッコが閉じられていません' unless @brackets_level == 0
			
			return @rpn_tokens
		end
		
		private
		def insert_rpn_tokens
			@rpn_tokens.insert( @rpn_tokens_ptr, @token )
		end
		
		def debug_print
			@rpn_tokens.each_with_index do |t,i|
				print ' ' if i != 0
				print '| ' if i == @rpn_tokens_ptr
				print t.val
				if t.type == TOKEN_TYPE_UNARY_OPERATOR
					print '@'
				end
			end
			print ' |' if @rpn_tokens.size == @rpn_tokens_ptr
			print "\n"
		end
		
		# 右オペランド(Right OPerand)の終了を確認し、処理する
		def rop_exit
			while rop_end?
				@rs_stack.pop
				@rpn_tokens_ptr += 1
			end
		end
		
		# 右オペランドは終了している?
		def rop_end?
			return false unless @rs_stack.last == @brackets_level
			next_token = @tokens[@tokens_i+1]
			token = @rpn_tokens[@rpn_tokens_ptr]
			return true unless next_token && token
			return true unless [next_token, token].all? {|v| 
				[TOKEN_TYPE_BINARY_OPERATOR, TOKEN_TYPE_UNARY_OPERATOR].include?( v.type ) }
			# next_token の priority の方が高ければ false
			return proc{|v| v[0] <= v[1] }.call( [next_token, token].map{|v| 
				( ( v.type == TOKEN_TYPE_BINARY_OPERATOR ) ?
					BINARY_OPERATORS : UNARY_OPERATORS )[v.val].priority } )
		end
		
	end
	
	def calculate_rpn( rpn_tokens )
		stack = []
		rpn_tokens.each do |token|
			case token.type
			when TOKEN_TYPE_NUM
				stack.push token.val
			when TOKEN_TYPE_BINARY_OPERATOR
				raise Error, 'オペランドが足りません' unless stack.size >= 2
				b = stack.pop
				a = stack.pop
				raise Error, '不明な演算子です' unless BINARY_OPERATORS.key?( token.val )
				stack.push BINARY_OPERATORS[token.val].proc.call( a, b )
			when TOKEN_TYPE_UNARY_OPERATOR
				raise Error, 'オペランドが足りません' unless stack.size >= 1
				a = stack.pop
				raise Error, '不明な演算子です' unless UNARY_OPERATORS.key?( token.val )
				stack.push UNARY_OPERATORS[token.val].proc.call( a )
			else 
				raise Error, '不明なトークンです'
			end
		end

		raise Error, 'オペランドが足りないか、余っています' unless stack.size == 1
		return stack[0]
	end
end

while s = gets
	begin
		puts Calc.calculate( s.chomp )
	rescue Calc::Error => e
		puts "Error: #{e}"
	end
end