# frozen_string_literal: true
require "curses"
include Curses
OPS = { "+" => 1, "-" => 1, "*" => 2, "/" => 2 }.freeze
def tokenize(s)
t = []
i = 0
prev = :start # :start, :num, :op, :l, :r
while i < s.size
c = s[i]
if c =~ /\s/
i += 1
next
end
# number
if c =~ /[0-9.]/
j = i
dot = 0
while j < s.size && s[j] =~ /[0-9.]/
dot += 1 if s[j] == "."
raise "数値の '.' が多すぎます" if dot > 1
j += 1
end
t << [:n, s[i...j].to_f]
i = j
prev = :num
next
end
# parens
if c == "("
t << [:l, c]
i += 1
prev = :l
next
end
if c == ")"
t << [:r, c]
i += 1
prev = :r
next
end
# operators (+ - * /)
if "+-*/".include?(c)
# unary minus: at start / after operator / after '('
if c == "-" && [:start, :op, :l].include?(prev)
# convert "-X" to "0 - X"
t << [:n, 0.0]
t << [:op, "-"]
i += 1
prev = :op
next
end
t << [:op, c]
i += 1
prev = :op
next
end
raise "不正な文字: #{c.inspect}"
end
t
end
def to_rpn(tokens)
out = []
ops = []
tokens.each do |k, v|
case k
when :n
out << [k, v]
when :op
while !ops.empty? && ops[-1][0] == :op && OPS[ops[-1][1]] >= OPS[v]
out << ops.pop
end
ops << [k, v]
when :l
ops << [k, v]
when :r
found = false
until ops.empty?
x = ops.pop
if x[0] == :l
found = true
break
end
out << x
end
raise "括弧が対応していません" unless found
end
end
until ops.empty?
x = ops.pop
raise "括弧が対応していません" if x[0] == :l
out << x
end
out
end
def eval_rpn(rpn)
st = []
rpn.each do |k, v|
if k == :n
st << v
next
end
b = st.pop
a = st.pop
raise "式が壊れています" if a.nil? || b.nil?
res =
case v
when "+" then a + b
when "-" then a - b
when "*" then a * b
when "/"
raise "0で割れません" if b == 0
a / b
else
raise "未知の演算子: #{v.inspect}"
end
st << res
end
raise "式が壊れています" unless st.size == 1
st[0]
end
def calc(expr)
eval_rpn(to_rpn(tokenize(expr)))
end
def show_char(c)
return "×" if c == "*"
return "÷" if c == "/"
c
end
def read_key
k = getch
return [:enter, nil] if k == 10 || k == "\n"
return [:bs, nil] if k == 8 || k == 127 || k == KEY_BACKSPACE
return [:ch, k] if k.is_a?(String)
[:ig, nil]
end
def center(y, txt)
setpos(y, 0)
clrtoeol
x = [(cols - txt.size) / 2, 0].max
setpos(y, x)
addstr(txt)
end
init_screen
noecho
(curs_set(0) rescue nil)
begin
expr = ""
disp = ""
msg = ""
clear
center(0, "電卓: Enter/= で計算, BSで消す, cでクリア, qで終了")
loop do
s = disp[-(cols - 2)..] || disp
center(lines / 2, s)
center(lines / 2 + 1, msg.to_s[0, cols - 2])
refresh
act, ch = read_key
next if act == :ig
msg = ""
case act
when :bs
expr.chop!
disp.chop!
when :enter
begin
r = calc(expr)
msg = "= #{r}"
# 結果を次の入力に引き継ぐ(連続計算用)
expr = r.to_s
disp = expr.dup
rescue => e
msg = "ERR: #{e.message}"
end
when :ch
break if ch == "q"
if ch == "c"
expr.clear
disp.clear
msg = ""
next
end
if ch == "="
act = :enter
redo
end
if ch =~ /[0-9+\-*/().]/
expr << ch
disp << show_char(ch)
end
end
end
ensure
close_screen
end