Newer
Older
Ruby / keisan5.rb
@SAITO Azuma SAITO Azuma on 7 Jan 7 KB 2026-01-07 23:35:20
# 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