|
| 1 | +import math |
| 2 | +import re |
| 3 | +import types |
| 4 | +import pickle |
| 5 | + |
| 6 | +from collections import namedtuple |
| 7 | + |
| 8 | + |
| 9 | +class PyCalc: |
| 10 | + |
| 11 | + def __init__(self): |
| 12 | + |
| 13 | + full_info = namedtuple("full_info", ("func", "priority", "number_args", "regex", "tag")) |
| 14 | + regex_and_tag = namedtuple("regex_and_tag", ("regex", "tag")) |
| 15 | + |
| 16 | + cfg_name = "operators.pkl" |
| 17 | + |
| 18 | + self.tag_advanced = 'advanced' |
| 19 | + self.tag_constant = 'constant' |
| 20 | + self.tag_common = 'common' |
| 21 | + self.tag_number = 'number' |
| 22 | + |
| 23 | + try: |
| 24 | + |
| 25 | + with open(cfg_name, 'rb') as pickle_file: |
| 26 | + obj = pickle.load(pickle_file) |
| 27 | + self.constants, self.operators, self.token_exprs = obj |
| 28 | + |
| 29 | + except Exception: |
| 30 | + |
| 31 | + math_priority = 8 |
| 32 | + |
| 33 | + common_operators = { |
| 34 | + |
| 35 | + ",": full_info(None, 9, None, r',', self.tag_common), |
| 36 | + |
| 37 | + |
| 38 | + "abs": full_info(float.__abs__, 8, 1, r'abs', self.tag_common), |
| 39 | + "round": full_info(float.__round__, 8, 1, r'round', self.tag_common), |
| 40 | + |
| 41 | + "$": full_info(float.__pos__, 7, 1, r'\$', self.tag_common), |
| 42 | + "#": full_info(float.__neg__, 7, 1, r'\#', self.tag_common), |
| 43 | + |
| 44 | + "^": full_info(float.__pow__, 6, 2, r'\^', self.tag_common), |
| 45 | + |
| 46 | + "*": full_info(float.__mul__, 5, 2, r'\*', self.tag_common), |
| 47 | + "/": full_info(float.__truediv__, 5, 2, r'/', self.tag_common), |
| 48 | + |
| 49 | + "%": full_info(float.__mod__, 4, 2, r'%', self.tag_common), |
| 50 | + "//": full_info(float.__floordiv__, 4, 2, r'//', self.tag_common), |
| 51 | + |
| 52 | + "-": full_info(float.__sub__, 2, 2, r'-', self.tag_common), |
| 53 | + "+": full_info(float.__add__, 2, 2, r'\+', self.tag_common), |
| 54 | + |
| 55 | + |
| 56 | + |
| 57 | + "(": full_info(None, 1, 0, r'\(', self.tag_common), |
| 58 | + |
| 59 | + "<=": full_info(float.__le__, 0, 2, r'<=', self.tag_common), |
| 60 | + ">=": full_info(float.__ge__, 0, 2, r'>=', self.tag_common), |
| 61 | + "==": full_info(float.__eq__, 0, 2, r'==', self.tag_common), |
| 62 | + "!=": full_info(float.__ne__, 0, 2, r'!=', self.tag_common), |
| 63 | + "<": full_info(float.__lt__, 0, 2, r'<', self.tag_common), |
| 64 | + ">": full_info(float.__gt__, 0, 2, r'>', self.tag_common), |
| 65 | + |
| 66 | + ")": full_info(None, None, None, r'\)', self.tag_common), |
| 67 | + |
| 68 | + "space": full_info(None, None, None, r'[ \n\t]+', None), |
| 69 | + "int_n": full_info(None, None, None, r'[0-9]+', self.tag_number), |
| 70 | + "int_f": full_info(None, None, None, r'[0-9]+\.[0-9]', self.tag_number), |
| 71 | + "int_f2": full_info(None, None, None, r'\.[0-9]', self.tag_number), |
| 72 | + |
| 73 | + |
| 74 | + } |
| 75 | + |
| 76 | + math_operators, math_constants = PyCalc.get_math_operators(math_priority=math_priority, |
| 77 | + tag_operators=self.tag_advanced, |
| 78 | + tag_constants=self.tag_constant, |
| 79 | + tuple_template = full_info |
| 80 | + ) |
| 81 | + |
| 82 | + self.operators = common_operators |
| 83 | + self.operators.update(math_operators) |
| 84 | + self.constants = math_constants |
| 85 | + |
| 86 | + token_expressions = [] |
| 87 | + for item in self.operators.values(): |
| 88 | + token_expressions.append(regex_and_tag(item.regex, item.tag)) |
| 89 | + for item in self.constants.values(): |
| 90 | + token_expressions.append(regex_and_tag(item.regex, item.tag)) |
| 91 | + |
| 92 | + token_expressions.sort(reverse=True) |
| 93 | + self.token_exprs = token_expressions |
| 94 | + |
| 95 | + try: |
| 96 | + obj = [self.constants, |
| 97 | + self.operators, |
| 98 | + self.token_exprs] |
| 99 | + with open(cfg_name, 'wb') as pickle_file: |
| 100 | + pickle.dump(obj, pickle_file) |
| 101 | + except Exception: |
| 102 | + pass |
| 103 | + |
| 104 | + def stack_from_string(self, input_string): |
| 105 | + |
| 106 | + pattern = r"[0-9][ \n\t]+[0-9]" |
| 107 | + if re.search(pattern, input_string): |
| 108 | + raise RuntimeError("ERROR: Unknown syntax!") |
| 109 | + |
| 110 | + patterns_and_replacements = [ |
| 111 | + (r"--", r"+"), |
| 112 | + (r"\++\+", r"+"), |
| 113 | + (r"\+-", r"-"), |
| 114 | + (r"-\+", r"-"), |
| 115 | + (r"\)\(", r")*(") |
| 116 | + ] |
| 117 | + |
| 118 | + break_bool = True |
| 119 | + while break_bool: |
| 120 | + break_bool = False |
| 121 | + for item in patterns_and_replacements: |
| 122 | + input_string = re.sub(item[0], item[1], input_string) |
| 123 | + for item in patterns_and_replacements: |
| 124 | + if re.search(item[0], input_string): |
| 125 | + break_bool = True |
| 126 | + break |
| 127 | + |
| 128 | + str_and_tag = namedtuple("str_and_tag", ("s", "tag")) |
| 129 | + string_as_stack = PyCalc.lexer(input_string, self.token_exprs, str_and_tag) |
| 130 | + |
| 131 | + temporary_stack = ["$"] |
| 132 | + prev_item = str_and_tag("$", self.tag_common) |
| 133 | + bracket_balance = 0 |
| 134 | + |
| 135 | + for index, item in enumerate(string_as_stack): |
| 136 | + |
| 137 | + if item.s == "(": |
| 138 | + bracket_balance += 1 |
| 139 | + elif item.s == ")": |
| 140 | + bracket_balance -= 1 |
| 141 | + if bracket_balance < 0: |
| 142 | + raise RuntimeError("ERROR: brackets aren't balanced!") |
| 143 | + |
| 144 | + if ((item.tag == self.tag_constant or item.tag == self.tag_advanced or item.tag == self.tag_number) and |
| 145 | + (prev_item.s == ")" or prev_item.tag == self.tag_constant or prev_item.tag == self.tag_number)) or \ |
| 146 | + ((prev_item.tag == self.tag_constant or prev_item.tag == self.tag_number) and item.s == "("): |
| 147 | + temporary_stack.append("*") |
| 148 | + |
| 149 | + if prev_item.tag == self.tag_common and prev_item.s != ")": |
| 150 | + if item.s == "+": |
| 151 | + continue |
| 152 | + elif item.s == "-": |
| 153 | + temporary_stack.append("#") |
| 154 | + continue |
| 155 | + |
| 156 | + temporary_stack.append(item.s) |
| 157 | + prev_item = item |
| 158 | + else: |
| 159 | + string_as_stack = temporary_stack[1:] |
| 160 | + if bracket_balance != 0: |
| 161 | + raise RuntimeError("ERROR: brackets aren't balanced!") |
| 162 | + |
| 163 | + return string_as_stack |
| 164 | + |
| 165 | + def rpn_from_stacked_string(self, stack): |
| 166 | + |
| 167 | + temporary_stack = [] |
| 168 | + rpn_stack = [] |
| 169 | + |
| 170 | + for item in stack: |
| 171 | + |
| 172 | + if item not in self.operators: |
| 173 | + rpn_stack.append(item) |
| 174 | + elif temporary_stack: |
| 175 | + |
| 176 | + if item == ")": |
| 177 | + temp = temporary_stack.pop() |
| 178 | + while temp != "(": |
| 179 | + rpn_stack.append(temp) |
| 180 | + temp = temporary_stack.pop() |
| 181 | + elif item == "(": |
| 182 | + temporary_stack.append(item) |
| 183 | + elif item == ",": |
| 184 | + while temporary_stack[-1] != "(": |
| 185 | + rpn_stack.append(temporary_stack.pop()) |
| 186 | + elif self.operators[temporary_stack[-1]].priority <= self.operators[item].priority: |
| 187 | + temporary_stack.append(item) |
| 188 | + else: |
| 189 | + temp_priority = self.operators[item].priority |
| 190 | + while temporary_stack and self.operators[temporary_stack[-1]].priority >= temp_priority: |
| 191 | + rpn_stack.append(temporary_stack.pop()) |
| 192 | + else: |
| 193 | + temporary_stack.append(item) |
| 194 | + else: |
| 195 | + temporary_stack.append(item) |
| 196 | + else: |
| 197 | + while temporary_stack: |
| 198 | + rpn_stack.append(temporary_stack.pop()) |
| 199 | + |
| 200 | + return rpn_stack |
| 201 | + |
| 202 | + def execute_rpn(self, rpn_stack): |
| 203 | + |
| 204 | + temporary_stack = [] |
| 205 | + |
| 206 | + for item in rpn_stack: |
| 207 | + |
| 208 | + # print(item+":"+str(temporary_stack)) |
| 209 | + |
| 210 | + if item in self.constants: |
| 211 | + temporary_stack.append(self.constants[item].func) |
| 212 | + continue |
| 213 | + elif item in self.operators: |
| 214 | + |
| 215 | + count = self.operators[item].number_args |
| 216 | + args = [] |
| 217 | + for i in range(count): |
| 218 | + args.append(float(temporary_stack.pop())) |
| 219 | + args.reverse() |
| 220 | + result = self.operators[item].func(*args) |
| 221 | + |
| 222 | + temporary_stack.append(result) |
| 223 | + else: |
| 224 | + temporary_stack.append(item) |
| 225 | + |
| 226 | + result = temporary_stack.pop() |
| 227 | + if temporary_stack: |
| 228 | + raise RuntimeError("ERROR: Unknown operation!") |
| 229 | + |
| 230 | + return result |
| 231 | + |
| 232 | + def calculate(self, input_string): |
| 233 | + |
| 234 | + # print(input_string) |
| 235 | + try: |
| 236 | + stacked_string = self.stack_from_string(input_string) |
| 237 | + except RuntimeError as rerror: |
| 238 | + print(rerror.args[0]) |
| 239 | + exit(1) |
| 240 | + except ValueError as verror: |
| 241 | + print(verror.args[0]) |
| 242 | + exit(1) |
| 243 | + # print(stacked_string) |
| 244 | + stacked_rpn = self.rpn_from_stacked_string(stacked_string) |
| 245 | + # print(stacked_rpn) |
| 246 | + try: |
| 247 | + result = self.execute_rpn(stacked_rpn) |
| 248 | + except IndexError: |
| 249 | + print("ERROR: Wrong operations order!") |
| 250 | + exit(1) |
| 251 | + except ZeroDivisionError: |
| 252 | + print("ERROR: Division by zero!") |
| 253 | + exit(1) |
| 254 | + except RuntimeError as rerror: |
| 255 | + print(rerror.args[0]) |
| 256 | + exit(1) |
| 257 | + return result |
| 258 | + |
| 259 | + @staticmethod |
| 260 | + def get_math_operators(math_priority, tag_operators, tag_constants, tuple_template): |
| 261 | + """ |
| 262 | + Returns dictionary: |
| 263 | + {"operation_name": (math.operation_name, self.number_of_operation's arguments, operation's value)} |
| 264 | + """ |
| 265 | + |
| 266 | + pattern = r"\(.*\)" |
| 267 | + coma_pattern = r"\," |
| 268 | + |
| 269 | + math_operators = {} |
| 270 | + math_constants = {} |
| 271 | + |
| 272 | + for item in dir(math): |
| 273 | + if isinstance(math.__dict__.get(item), types.BuiltinFunctionType): |
| 274 | + |
| 275 | + if item.find("__"): |
| 276 | + res = re.search(pattern, math.__dict__.get(item).__doc__) |
| 277 | + res = re.findall(coma_pattern, res.group()) |
| 278 | + math_operators.update({item: tuple_template(math.__dict__.get(item), math_priority, len(res) + 1, |
| 279 | + item, tag_operators)}) |
| 280 | + |
| 281 | + else: |
| 282 | + if item.find("__"): |
| 283 | + math_constants.update({item: tuple_template(math.__dict__.get(item), None, None, item, |
| 284 | + tag_constants)}) |
| 285 | + |
| 286 | + return math_operators, math_constants |
| 287 | + |
| 288 | + @staticmethod |
| 289 | + def lexer(characters, token_exprs, tuple_template): |
| 290 | + pos = 0 |
| 291 | + tokens = [] |
| 292 | + while pos < len(characters): |
| 293 | + match = None |
| 294 | + for token_expr in token_exprs: |
| 295 | + pattern, tag = token_expr |
| 296 | + regex = re.compile(pattern) |
| 297 | + match = regex.match(characters, pos) |
| 298 | + if match: |
| 299 | + text = match.group(0) |
| 300 | + if tag: |
| 301 | + token = tuple_template(text, tag) |
| 302 | + tokens.append(token) |
| 303 | + break |
| 304 | + if not match: |
| 305 | + raise RuntimeError('Illegal character: %s\n' % characters[pos]) |
| 306 | + else: |
| 307 | + pos = match.end(0) |
| 308 | + return tokens |
0 commit comments