#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import math
import sys
import readline
# Супер-крутой консольный калькулятор - только стандартная библиотека
# Поддерживает: скобки, + - * / // % ^, унарный -, функции math, переменные, история, ans
OPS = {
'+': (1, 'left'),
'-': (1, 'left'),
'*': (2, 'left'),
'/': (2, 'left'),
'//': (2, 'left'),
'%': (2, 'left'),
'^': (3, 'right'),
}
UNARY = 'u-' # токен для унарного минуса
FUNCTIONS = {
'sin': math.sin,
'cos': math.cos,
'tan': math.tan,
'asin': math.asin,
'acos': math.acos,
'atan': math.atan,
'sqrt': math.sqrt,
'log': lambda x, base=math.e: math.log(x, base) if base != math.e else math.log(x),
'ln': math.log,
'log10': math.log10,
'exp': math.exp,
'pow': math.pow,
'abs': abs,
'floor': math.floor,
'ceil': math.ceil,
'round': round,
'factorial': math.factorial,
}
CONSTS = {
'pi': math.pi,
'e': math.e,
}
# токенайзер
def tokenize(s):
tokens = []
i = 0
L = len(s)
while i < L:
c = s[i]
if c.isspace():
i += 1
continue
if c.isdigit() or (c == '.' and i+1 < L and s[i+1].isdigit()):
j = i
has_dot = False
while j < L and (s[j].isdigit() or s[j] == '.'):
if s[j] == '.':
if has_dot:
break
has_dot = True
j += 1
tokens.append(s[i:j])
i = j
continue
if c.isalpha() or c == '_':
j = i
while j < L and (s[j].isalnum() or s[j] == '_'):
j += 1
tokens.append(s[i:j])
i = j
continue
# многосимвольные операторы
if s.startswith('//', i):
tokens.append('//'); i += 2; continue
# одиночные операторы и скобки
if c in '+-*/%^(),=%':
tokens.append(c)
i += 1
continue
if c == '=':
tokens.append('='); i += 1; continue
raise ValueError(f"Неизвестный символ: {c}")
return tokens
# шантинг-ярд -> ОПЗ (RPN)
def to_rpn(tokens):
out = []
stack = []
prev = None
i = 0
while i < len(tokens):
tok = tokens[i]
if is_number(tok) or is_variable(tok):
out.append(tok)
elif tok in FUNCTIONS:
stack.append(tok)
elif tok == ',':
while stack and stack[-1] != '(':
out.append(stack.pop())
if not stack:
raise ValueError("Неправильно размещённая запятая или несоответствие скобок")
elif tok == '(':
stack.append(tok)
elif tok == ')':
while stack and stack[-1] != '(':
out.append(stack.pop())
if not stack:
raise ValueError("Несоответствие скобок")
stack.pop()
if stack and stack[-1] in FUNCTIONS:
out.append(stack.pop())
elif tok in OPS or tok == '-':
# определить унарный минус
if tok == '-' and (prev is None or (prev in OPS or prev in ('(', ','))):
op = UNARY
else:
op = tok
if op == UNARY:
prec = 4
assoc = 'right'
else:
prec, assoc = OPS.get(op, (None, None))
if prec is None:
raise ValueError(f"Неизвестный оператор: {op}")
while stack:
top = stack[-1]
if top == '(':
break
if top == UNARY:
top_prec = 4; top_assoc = 'right'
else:
if top in OPS:
top_prec, top_assoc = OPS[top]
else:
top_prec = 5; top_assoc = 'left'
if (assoc == 'left' and prec <= top_prec) or (assoc == 'right' and prec < top_prec):
out.append(stack.pop())
else:
break
stack.append(op)
else:
raise ValueError(f"Неизвестный токен в выражении: {tok}")
prev = tok
i += 1
while stack:
t = stack.pop()
if t in ('(', ')'):
raise ValueError("Несоответствие скобок")
out.append(t)
return out
def is_number(tok):
try:
float(tok)
return True
except:
return False
def is_variable(tok):
if tok in FUNCTIONS or tok in ('(', ')', ',',) or tok in OPS or tok == UNARY:
return False
if tok == 'ans':
return True
return tok.isidentifier()
# вычисление ОПЗ
def eval_rpn(rpn, variables):
st = []
for tok in rpn:
if is_number(tok):
st.append(float(tok))
elif tok in variables:
st.append(variables[tok])
elif tok in CONSTS:
st.append(CONSTS[tok])
elif tok == UNARY:
if not st:
raise ValueError("Отсутствует операнд для унарного '-'")
a = st.pop()
st.append(-a)
elif tok in OPS:
if len(st) < 2:
raise ValueError(f"Отсутствует операнд для оператора {tok}")
b = st.pop(); a = st.pop()
st.append(apply_op(a, b, tok))
elif tok in FUNCTIONS:
if tok == 'log' and len(st) >= 2:
b = st.pop(); a = st.pop()
st.append(FUNCTIONS[tok](a, b))
elif tok == 'pow' and len(st) >= 2:
b = st.pop(); a = st.pop()
st.append(FUNCTIONS[tok](a, b))
else:
if not st:
raise ValueError(f"Отсутствует операнд для функции {tok}")
a = st.pop()
if tok == 'factorial':
if not float(a).is_integer() or a < 0:
raise ValueError("factorial() определена только для неотрицательных целых")
st.append(FUNCTIONS[tok](int(a)))
else:
st.append(FUNCTIONS[tok](a))
else:
raise ValueError(f"Неизвестный токен при вычислении: {tok}")
if len(st) != 1:
raise ValueError("Неверное выражение")
return st[0]
def apply_op(a, b, op):
if op == '+': return a + b
if op == '-': return a - b
if op == '*': return a * b
if op == '/':
if b == 0:
raise ZeroDivisionError("деление на ноль")
return a / b
if op == '//':
if b == 0:
raise ZeroDivisionError("деление на ноль")
return a // b
if op == '%':
if b == 0:
raise ZeroDivisionError("деление по модулю на ноль")
return a % b
if op == '^':
return a ** b
raise ValueError(f"Неподдерживаемый оператор: {op}")
# верхнеуровневое вычисление, поддерживает присваивание name = expr
def evaluate(expr, variables):
tokens = tokenize(expr)
if not tokens:
raise ValueError("Пустое выражение")
# присваивание
if len(tokens) >= 3 and tokens[1] == '=' and is_variable(tokens[0]):
name = tokens[0]
rhs = tokens[2:]
rpn = to_rpn(rhs)
val = eval_rpn(rpn, variables)
variables[name] = val
variables['ans'] = val
return val, f"{name} = {val}"
else:
rpn = to_rpn(tokens)
val = eval_rpn(rpn, variables)
variables['ans'] = val
return val, str(val)
def pretty(x):
if isinstance(x, float):
if x.is_integer():
return str(int(x))
else:
return f"{x:.12g}"
return str(x)
def banner():
print("SuperKalk v1 - консольный калькулятор")
print("Команды: exit, quit, help, vars, history, clear, load <файл>")
print("Поддерживаемые функции: " + ", ".join(sorted(FUNCTIONS.keys())))
print("Константы: " + ", ".join(f"{k}={v}" for k, v in CONSTS.items()))
print("Пример: x = 2 * (3 + sqrt(4))")
print()
def repl():
variables = {'ans': 0.0}
history = []
banner()
while True:
try:
line = input('>>> ').strip()
except (EOFError, KeyboardInterrupt):
print()
break
if not line:
continue
history.append(line)
low = line.lower()
if low in ('exit', 'quit'):
break
if low == 'help':
print("Вводите выражения. Примеры:")
print(" 2 + 3*4")
print(" sin(pi/2) + ln(e)")
print(" x = 5")
print(" ans * 2")
continue
if low == 'vars':
for k, v in variables.items():
print(f"{k} = {pretty(v)}")
continue
if low == 'history':
for i, h in enumerate(history, 1):
print(f"{i}: {h}")
continue
if low == 'clear':
variables = {'ans': 0.0}
history = []
print("Переменные и история очищены.")
continue
if low.startswith('load '):
fn = line.split(' ', 1)[1]
try:
with open(fn, 'r', encoding='utf-8') as f:
for l in f:
l = l.strip()
if not l or l.startswith('#'):
continue
print('> ' + l)
try:
val, out = evaluate(l, variables)
print(pretty(val))
except Exception as e:
print("Ошибка:", e)
except Exception as e:
print("Не удалось загрузить файл:", e)
continue
try:
val, out = evaluate(line, variables)
print(pretty(val))
except Exception as e:
print("Ошибка:", e)
if __name__ == '__main__':
repl()