Newer
Older
Ruby / 20260105-2226_eb757619b8c14d93d1d88438af802d60.rb
@SAITO Azuma SAITO Azuma on 5 Jan 3 KB 2026-01-05 23:41:20
# 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