# encoding: UTF-8
# frozen_string_literal: true
require "curses"
include Curses
OPS = { "+" => 1, "-" => 1, "*" => 2, "/" => 2 }.freeze
SETSUMEI = [
" 〜説明〜 ",
" ",
"この電卓は、キーを打って計算内容を入力します",
"すべて半角で入れてください",
"",
"まず数字は数字キーで13527などとそのまま打ちます。",
"足し算、引き算も + や - キーでそのまま打ちます。",
"しかし、掛け算は *(アスタリスク) で打ちます。",
"割り算は/(スラッシュ)で打ちます。",
"ここは気をつけてください。",
"",
"この電卓は括弧も使うことができます。",
"そのまま ( や ) で打ちます。",
"負の数も使えますが、例えば-5を入れたいときは",
"(0-5)と書く必要はありません(今の版は -5 もOK)。",
"結果を見たいときはEnterを押してください。",
"何の計算かわかりにくくなるので*や/と入力しても",
"×や÷などと表示されます。",
"",
"見終わったらEnterキーを押してください。"
].freeze
def screen_cols
if Curses.respond_to?(:cols)
Curses.cols
elsif defined?(Curses::COLS)
Curses::COLS
else
80
end
end
def screen_lines
if Curses.respond_to?(:lines)
Curses.lines
elsif defined?(Curses::LINES)
Curses::LINES
else
24
end
end
def tail_str(str, width)
return "" if width <= 0
return str if str.size <= width
str[str.size - width, width]
end
def center(y, txt)
Curses.setpos(y, 0)
Curses.clrtoeol
w = screen_cols
x = (w - txt.size) / 2
x = 0 if x < 0
Curses.setpos(y, x)
Curses.addstr(txt)
end
def show_help
Curses.clear
w = screen_cols
SETSUMEI.each_with_index do |line, i|
break if i >= screen_lines
Curses.setpos(i, 0)
Curses.clrtoeol
Curses.addstr(line.to_s[0, w - 1]) if w > 1
end
Curses.refresh
# Enterで戻る(他キーは無視)
loop do
k = Curses.getch
break if k == 10 || k == "\n"
end
Curses.clear
end
def tokenize(s)
t = []
i = 0
prev = :start
while i < s.size
c = s[i]
if c =~ /\s/
i += 1
next
end
if c =~ /[0-9.]/
j = i
dot = 0
while j < s.size && s[j] =~ /[0-9.]/
dot += 1 if s[j] == "."
raise "too many dots" if dot > 1
j += 1
end
t << [:n, s[i...j].to_f]
i = j
prev = :num
next
end
if c == "("
t << [:l, c]
i += 1
prev = :l
next
end
if c == ")"
t << [:r, c]
i += 1
prev = :r
next
end
if "+-*/".include?(c)
# 単項マイナス: -5 や -(1+2)
if c == "-" && (prev == :start || prev == :op || prev == :l)
t << [:n, 0.0]
t << [:op, "-"]
i += 1
prev = :op
next
end
t << [:op, c]
i += 1
prev = :op
next
end
raise "bad char: #{c.inspect}"
end
t
end
def to_rpn(tokens)
out = []
ops = []
tokens.each do |k, v|
if k == :n
out << [k, v]
elsif k == :op
while !ops.empty? && ops[-1][0] == :op && OPS[ops[-1][1]] >= OPS[v]
out << ops.pop
end
ops << [k, v]
elsif k == :l
ops << [k, v]
else # :r
found = false
until ops.empty?
x = ops.pop
if x[0] == :l
found = true
break
else
out << x
end
end
raise "paren mismatch" unless found
end
end
until ops.empty?
x = ops.pop
raise "paren mismatch" 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 "bad expr" if a.nil? || b.nil?
if v == "+"
st << (a + b)
elsif v == "-"
st << (a - b)
elsif v == "*"
st << (a * b)
elsif v == "/"
raise "div by zero" if b == 0
st << (a / b)
else
raise "unknown op: #{v.inspect}"
end
end
raise "bad expr" unless st.size == 1
st[0]
end
def calc(expr)
eval_rpn(to_rpn(tokenize(expr)))
end
def show_char(c)
if c == "*"
"×"
elsif c == "/"
"÷"
else
c
end
end
def read_key
k = Curses.getch
if k == 10 || k == "\n"
return [:enter, nil]
end
if k == 8 || k == 127 || k == Curses::KEY_BACKSPACE
return [:bs, nil]
end
if k.is_a?(String)
return [:ch, k]
end
[:ig, nil]
end
Curses.init_screen
Curses.noecho
begin
Curses.curs_set(0)
rescue
end
begin
expr = String.new
disp = String.new
msg = String.new
Curses.clear
center(0, "電卓: Enter/=で計算, BSで消す, c=クリア, m=説明, q=終了")
loop do
w = screen_cols
h = screen_lines
center(h / 2, tail_str(disp, w - 2))
center(h / 2 + 1, tail_str(msg.to_s, w - 2))
Curses.refresh
act, ch = read_key
next if act == :ig
msg = String.new
if act == :bs
expr.chop!
disp.chop!
next
end
if act == :enter
begin
r = calc(expr)
msg = "= #{r}"
expr = r.to_s
disp = expr.dup
rescue => e
msg = "ERR: #{e.message}"
end
next
end
# :ch
break if ch == "q"
if ch == "m"
show_help
center(0, "電卓: Enter/=で計算, BSで消す, c=クリア, m=説明, q=終了")
next
end
if ch == "c"
expr = String.new
disp = String.new
msg = String.new
next
end
if ch == "="
begin
r = calc(expr)
msg = "= #{r}"
expr = r.to_s
disp = expr.dup
rescue => e
msg = "ERR: #{e.message}"
end
next
end
# / をエスケープしてあるのが重要
if ch =~ /[0-9+\-*\/().]/
expr << ch
disp << show_char(ch)
end
end
ensure
Curses.close_screen
end