# encoding: UTF-8
# frozen_string_literal: true
require "curses"
include Curses
OPS = { "+" => 1, "-" => 1, "*" => 2, "/" => 2 }.freeze
SETSUMEI = [
" 〜説明〜 ",
" ",
"この電卓は、キーを打って計算内容を入力します",
"すべて半角で入れてください",
"",
"数字は数字キーでそのまま打ちます。",
"足し算、引き算も + や - キーでそのまま打ちます。",
"掛け算は *(アスタリスク) 、割り算は /(スラッシュ) です。",
"",
"括弧も使えます。( や ) をそのまま打ちます。",
"負の数も使えます。-5 や -(1+2) も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
h = screen_lines
SETSUMEI.each_with_index do |line, i|
break if i >= h
Curses.setpos(i, 0)
Curses.clrtoeol
Curses.addstr(line.to_s[0, w - 1]) if w > 1
end
Curses.refresh
loop do
k = Curses.getch
break if k == 10 || k == "\n"
end
Curses.clear
end
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
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)
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 run_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)
run_rpn(to_rpn(tokenize(expr)))
end
def show_char(c)
if c == "*"
"×"
elsif c == "/"
"÷"
else
c
end
end
def read_key
k = Curses.getch
return [:enter, nil] if k == 10 || k == "\n"
return [:bs, nil] if k == 8 || k == 127 || k == Curses::KEY_BACKSPACE
return [:left, nil] if k == Curses::KEY_LEFT
return [:right, nil] if k == Curses::KEY_RIGHT
return [:up, nil] if k == Curses::KEY_UP
return [:down, nil] if k == Curses::KEY_DOWN
return [:ig, nil] if k.is_a?(Integer)
return [:ch, k] if k.is_a?(String)
[:ig, nil]
end
Curses.init_screen
Curses.noecho
Curses.stdscr.keypad(true) rescue nil
begin
Curses.curs_set(1)
rescue
end
begin
expr = String.new
disp = String.new
msg = String.new
cursor = 0
history = []
h_idx = -1
Curses.clear
center(0, "電卓: Enter/=で計算, BSで消す, c=クリア, m=説明, q=終了 ←→で移動, ↑↓で履歴")
loop do
w = screen_cols
h = screen_lines
width = w - 2
width = 1 if width < 1
view_start = disp.size - width
view_start = 0 if view_start < 0
visible = disp[view_start, width] || ""
y = h / 2
center(y, visible)
center(y + 1, tail_str(msg.to_s, width))
x0 = [(w - visible.size) / 2, 0].max
cx = cursor - view_start
if cx >= 0 && cx <= visible.size
Curses.setpos(y, x0 + cx)
end
Curses.refresh
act, ch = read_key
next if act == :ig
msg = String.new
if act == :left
cursor -= 1 if cursor > 0
next
end
if act == :right
cursor += 1 if cursor < expr.size
next
end
if act == :up
if !history.empty?
h_idx = history.size - 1 if h_idx == -1
h_idx -= 1 if h_idx > 0
expr = history[h_idx].dup
disp = expr.dup
disp.gsub!("*", "×")
disp.gsub!("/", "÷")
cursor = expr.size
end
next
end
if act == :down
if !history.empty? && h_idx != -1
h_idx += 1
if h_idx >= history.size
h_idx = -1
expr = String.new
disp = String.new
else
expr = history[h_idx].dup
disp = expr.dup
disp.gsub!("*", "×")
disp.gsub!("/", "÷")
end
cursor = expr.size
end
next
end
if act == :bs
if cursor > 0
expr.slice!(cursor - 1)
disp.slice!(cursor - 1)
cursor -= 1
end
next
end
if act == :enter
begin
r = calc(expr)
msg = "= #{r}"
history << expr.dup unless expr.empty?
h_idx = -1
expr = r.to_s
disp = expr.dup
cursor = expr.size
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
cursor = 0
h_idx = -1
next
end
if ch == "="
begin
r = calc(expr)
msg = "= #{r}"
history << expr.dup unless expr.empty?
h_idx = -1
expr = r.to_s
disp = expr.dup
cursor = expr.size
rescue => e
msg = "ERR: #{e.message}"
end
next
end
if ch =~ /[0-9+\-*\/().]/
expr.insert(cursor, ch)
disp.insert(cursor, show_char(ch))
cursor += 1
end
end
ensure
Curses.close_screen
end