1#!/usr/bin/env python3 2 3#**************************************************************************** 4# calccore.py, provides the non-GUI base classes 5# 6# rpCalc, an RPN calculator 7# Copyright (C) 2014, Douglas W. Bell 8# 9# This is free software; you can redistribute it and/or modify it under the 10# terms of the GNU General Public License, either Version 2 or any later 11# version. This program is distributed in the hope that it will be useful, 12# but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. 13#***************************************************************************** 14 15import math 16import option 17import optiondefaults 18import calcstack 19 20class Mode: 21 """Enum for calculator modes. 22 """ 23 entryMode = 100 # in num entry - adds to num string 24 saveMode = 101 # after result - previous result becomes Y 25 replMode = 102 # after enter key - replaces X 26 expMode = 103 # in exponent entry - adds to exp string 27 memStoMode = 104 # in memory register entry - needs 0-9 for num to store 28 memRclMode = 105 # in memory register entry - needs 0-9 for num to recall 29 decPlcMode = 106 # in decimal places entry - needs 0-9 for value 30 errorMode = 107 # error notification - any cmd to resume 31 32 33class CalcCore: 34 """Reverse Polish calculator functionality. 35 """ 36 minMaxHist = 10 37 maxMaxHist = 10000 38 minNumBits = 4 39 maxNumBits = 128 40 def __init__(self): 41 self.stack = calcstack.CalcStack() 42 self.option = option.Option('rpcalc', 20) 43 self.option.loadAll(optiondefaults.defaultList) 44 self.restoreStack() 45 self.xStr = '' 46 self.updateXStr() 47 self.flag = Mode.saveMode 48 self.base = 10 49 self.numBits = 0 50 self.useTwosComplement = False 51 self.history = [] 52 self.histChg = 0 53 self.setAltBaseOptions() 54 55 def setAltBaseOptions(self): 56 """Update bit limit and two's complement use. 57 """ 58 self.numBits = self.option.intData('AltBaseBits', CalcCore.minNumBits, 59 CalcCore.maxNumBits) 60 if not self.numBits: 61 self.numBits = CalcCore.maxNumBits 62 self.useTwosComplement = self.option.boolData('UseTwosComplement') 63 64 def restoreStack(self): 65 """Read stack from option file. 66 """ 67 if self.option.boolData('SaveStacks'): 68 self.stack.replaceAll([self.option.numData('Stack' + repr(x)) for 69 x in range(4)]) 70 self.mem = [self.option.numData('Mem' + repr(x)) for x in 71 range(10)] 72 else: 73 self.mem = [0.0] * 10 74 75 def saveStack(self): 76 """Store stack to option file. 77 """ 78 if self.option.boolData('SaveStacks'): 79 [self.option.changeData('Stack' + repr(x), repr(self.stack[x]), 1) 80 for x in range(4)] 81 [self.option.changeData('Mem' + repr(x), repr(self.mem[x]), 1) 82 for x in range(10)] 83 self.option.writeChanges() 84 85 def updateXStr(self): 86 """get display string from X register. 87 """ 88 if abs(self.stack[0]) > 1e299: 89 self.xStr = 'error 9' 90 self.flag = Mode.errorMode 91 self.stack[0] = 0.0 92 if abs(self.stack[1]) > 1e299: 93 self.stack.replaceXY(0.0) 94 else: 95 self.xStr = self.formatNum(self.stack[0]) 96 97 def formatNum(self, num): 98 """Return number formatted per options. 99 """ 100 absNum = abs(num) 101 plcs = self.option.intData('NumDecimalPlaces', 0, 9) 102 forceSci = self.option.boolData('ForceSciNotation') 103 useEng = self.option.boolData('UseEngNotation') 104 exp = 0 105 if absNum != 0.0 and (absNum < 1e-4 or absNum >= 1e7 or forceSci 106 or useEng): 107 exp = int(math.floor(math.log10(absNum))) 108 if useEng: 109 exp = 3 * (exp // 3) 110 num /= 10**exp 111 num = round(num, plcs) # check if rounding bumps exponent 112 if useEng and abs(num) >= 1000.0: 113 num /= 1000.0 114 exp += 3 115 elif not useEng and abs(num) >= 10.0: 116 num /= 10.0 117 exp += 1 118 numStr = '{: 0.{pl}f}'.format(num, pl=plcs) 119 if self.option.boolData('ThousandsSeparator'): 120 numStr = self.addThousandsSep(numStr) 121 if exp != 0 or forceSci: 122 expDigits = 4 123 if self.option.boolData('TrimExponents'): 124 expDigits = 1 125 numStr = '{0}e{1:+0{pl}d}'.format(numStr, exp, pl=expDigits) 126 return numStr 127 128 def addThousandsSep(self, numStr): 129 """Return number string with thousands separators added. 130 """ 131 leadChar = '' 132 if numStr[0] < '0' or numStr[0] > '9': 133 leadChar = numStr[0] 134 numStr = numStr[1:] 135 numStr = numStr.replace(' ', '') 136 decPos = numStr.find('.') 137 if decPos < 0: 138 decPos = len(numStr) 139 for i in range(decPos - 3, 0, -3): 140 numStr = numStr[:i] + ' ' + numStr[i:] 141 return leadChar + numStr 142 143 def sciFormatX(self, decPlcs): 144 """Return X register str in sci notation. 145 """ 146 return '{: 0.{pl}e}'.format(self.stack[0], pl=decPlcs) 147 148 def newXValue(self, value): 149 """Push X onto stack, replace with value. 150 """ 151 self.stack.enterX() 152 self.stack[0] = float(value) 153 self.updateXStr() 154 self.flag = Mode.saveMode 155 156 def numEntry(self, entStr): 157 """Interpret a digit entered depending on mode. 158 """ 159 if self.flag == Mode.saveMode: 160 self.stack.enterX() 161 if self.flag in (Mode.entryMode, Mode.expMode): 162 if self.base == 10: 163 newStr = self.xStr + entStr 164 else: 165 newStr = self.numberStr(self.stack[0], self.base) + entStr 166 else: 167 newStr = ' ' + entStr # space for minus sign 168 if newStr == ' .': 169 newStr = ' 0.' 170 try: 171 num = self.convertNum(newStr) 172 except ValueError: 173 return False 174 if self.base != 10: 175 newStr = self.formatNum(num) # decimal num in main display 176 self.stack[0] = num 177 if self.option.boolData('ThousandsSeparator'): 178 newStr = self.addThousandsSep(newStr) 179 self.xStr = newStr 180 if self.flag != Mode.expMode: 181 self.flag = Mode.entryMode 182 return True 183 184 def numberStr(self, number, base): 185 """Return string of number in given base (2-16). 186 """ 187 digits = '0123456789abcdef' 188 number = int(round(number)) 189 result = '' 190 sign = '' 191 if number == 0: 192 return '0' 193 if self.useTwosComplement: 194 if number >= 2**(self.numBits - 1) or \ 195 number < -2**(self.numBits - 1): 196 return 'overflow' 197 if number < 0: 198 number = 2**self.numBits + number 199 else: 200 if number < 0: 201 number = abs(number) 202 sign = '-' 203 if number >= 2**self.numBits: 204 return 'overflow' 205 while number: 206 number, remainder = divmod(number, base) 207 result = '{0}{1}'.format(digits[remainder], result) 208 return '{0}{1}'.format(sign, result) 209 210 def convertNum(self, numStr): 211 """Convert number string to float using current base. 212 """ 213 numStr = numStr.replace(' ', '') 214 if self.base == 10: 215 return float(numStr) 216 num = float(int(numStr, self.base)) 217 if num >= 2**self.numBits: 218 self.xStr = 'error 9' 219 self.flag = Mode.errorMode 220 self.stack[0] = num 221 raise ValueError 222 if self.useTwosComplement and num >= 2**(self.numBits - 1): 223 num = num - 2**self.numBits 224 return num 225 226 def expCmd(self): 227 """Command to add an exponent. 228 """ 229 if self.flag == Mode.expMode or self.base != 10: 230 return False 231 if self.flag == Mode.entryMode: 232 self.xStr = self.xStr + 'e+0' 233 else: 234 if self.flag == Mode.saveMode: 235 self.stack.enterX() 236 self.stack[0]= 1.0 237 self.xStr = '1e+0' 238 self.flag = Mode.expMode 239 return True 240 241 def bspCmd(self): 242 """Backspace command. 243 """ 244 if self.base != 10 and self.flag == Mode.entryMode: 245 self.xStr = self.numberStr(self.stack[0], self.base) 246 if self.xStr[0] != '-': 247 self.xStr = ' ' + self.xStr 248 if self.flag == Mode.entryMode and len(self.xStr) > 2: 249 self.xStr = self.xStr[:-1] 250 elif self.flag == Mode.expMode: 251 numExp = self.xStr.split('e', 1) 252 if len(numExp[1]) > 2: 253 self.xStr = self.xStr[:-1] 254 else: 255 self.xStr = numExp[0] 256 self.flag = Mode.entryMode 257 else: 258 self.stack[0] = 0.0 259 self.updateXStr() 260 self.flag = Mode.replMode 261 return True 262 self.stack[0] = self.convertNum(self.xStr) 263 if self.base != 10: 264 self.xStr = self.formatNum(self.stack[0]) 265 if self.option.boolData('ThousandsSeparator'): 266 self.xStr = self.addThousandsSep(self.xStr) 267 return True 268 269 def chsCmd(self): 270 """Change sign command. 271 """ 272 if self.flag == Mode.expMode: 273 numExp = self.xStr.split('e', 1) 274 if numExp[1][0] == '+': 275 self.xStr = numExp[0] + 'e-' + numExp[1][1:] 276 else: 277 self.xStr = numExp[0] + 'e+' + numExp[1][1:] 278 else: 279 if self.xStr[0] == ' ': 280 self.xStr = '-' + self.xStr[1:] 281 else: 282 self.xStr = ' ' + self.xStr[1:] 283 self.stack[0] = float(self.xStr.replace(' ', '')) 284 return True 285 286 def memStoRcl(self, numStr): 287 """Handle memMode number entry for mem & dec plcs. 288 """ 289 if len(numStr) == 1 and '0' <= numStr <= '9': 290 num = int(numStr) 291 if self.flag == Mode.memStoMode: 292 self.mem[num] = self.stack[0] 293 elif self.flag == Mode.memRclMode: 294 self.stack.enterX() 295 self.stack[0] = self.mem[num] 296 else: # decimal place mode 297 self.option.changeData('NumDecimalPlaces', numStr, 1) 298 self.option.writeChanges() 299 elif numStr == '<-': # backspace 300 pass 301 else: 302 return False 303 self.updateXStr() 304 self.flag = Mode.saveMode 305 return True 306 307 def angleConv(self): 308 """Return angular conversion factor from options. 309 """ 310 type = self.option.strData('AngleUnit') 311 if type == 'rad': 312 return 1.0 313 if type == 'grad': 314 return math.pi / 200 315 return math.pi / 180 # degree 316 317 def cmd(self, cmdStr): 318 """Main command interpreter - returns true/false if change made. 319 """ 320 if self.flag in (Mode.memStoMode, Mode.memRclMode, Mode.decPlcMode): 321 return self.memStoRcl(cmdStr) 322 if self.flag == Mode.errorMode: # reset display, ignore next command 323 self.updateXStr() 324 self.flag = Mode.saveMode 325 return True 326 eqn = '' 327 try: 328 if len(cmdStr) == 1: 329 if '0' <= cmdStr <= '9' or cmdStr == '.': 330 return self.numEntry(cmdStr) 331 if self.base == 16 and 'A' <= cmdStr <= 'F': 332 return self.numEntry(cmdStr) 333 if cmdStr in '+-*/': 334 eqn = '{0} {1} {2}'.format(self.formatNum(self.stack[1]), 335 cmdStr, 336 self.formatNum(self.stack[0])) 337 if cmdStr == '+': 338 self.stack.replaceXY(self.stack[1] + self.stack[0]) 339 elif cmdStr == '-': 340 self.stack.replaceXY(self.stack[1] - self.stack[0]) 341 elif cmdStr == '*': 342 self.stack.replaceXY(self.stack[1] * self.stack[0]) 343 elif cmdStr == '/': 344 self.stack.replaceXY(self.stack[1] / self.stack[0]) 345 else: 346 return False 347 elif cmdStr == 'ENT': # enter 348 self.stack.enterX() 349 self.flag = Mode.replMode 350 self.updateXStr() 351 return True 352 elif cmdStr == 'EXP': 353 return self.expCmd() 354 elif cmdStr == 'X<>Y': # exchange 355 self.stack[0], self.stack[1] = self.stack[1], self.stack[0] 356 elif cmdStr == 'CHS': # change sign 357 return self.chsCmd() 358 elif cmdStr == 'CLR': # clear 359 self.stack.replaceAll([0.0, 0.0, 0.0, 0.0]) 360 elif cmdStr == '<-': # backspace 361 return self.bspCmd() 362 elif cmdStr == 'STO': # store to memory 363 self.flag = Mode.memStoMode 364 self.xStr = '0-9:' 365 return True 366 elif cmdStr == 'RCL': # recall from memory 367 self.flag = Mode.memRclMode 368 self.xStr = '0-9:' 369 return True 370 elif cmdStr == 'PLCS': # change dec plc setting 371 self.flag = Mode.decPlcMode 372 self.xStr = '0-9:' 373 return True 374 elif cmdStr == 'SCI': # toggle fix/sci setting 375 orig = self.option.boolData('ForceSciNotation') 376 new = orig and 'no' or 'yes' 377 self.option.changeData('ForceSciNotation', new, 1) 378 self.option.writeChanges 379 elif cmdStr == 'DEG': # change deg/rad setting 380 orig = self.option.strData('AngleUnit') 381 new = orig == 'deg' and 'rad' or 'deg' 382 self.option.changeData('AngleUnit', new, 1) 383 self.option.writeChanges() 384 elif cmdStr == 'R<': # roll stack back 385 self.stack.rollBack() 386 elif cmdStr == 'R>': # roll stack forward 387 self.stack.rollUp() 388 elif cmdStr == 'PI': # pi constant 389 self.stack.enterX() 390 self.stack[0] = math.pi 391 elif cmdStr == 'X^2': # square 392 eqn = '{0}^2'.format(self.formatNum(self.stack[0])) 393 self.stack[0] = self.stack[0] * self.stack[0] 394 elif cmdStr == 'Y^X': # x power of y 395 eqn = '({0})^{1}'.format(self.formatNum(self.stack[1]), 396 self.formatNum(self.stack[0])) 397 self.stack.replaceXY(self.stack[1] ** self.stack[0]) 398 elif cmdStr == 'XRT': # x root of y 399 eqn = '({0})^(1/{1})'.format(self.formatNum(self.stack[1]), 400 self.formatNum(self.stack[0])) 401 self.stack.replaceXY(self.stack[1] ** (1/self.stack[0])) 402 elif cmdStr == 'RCIP': # 1/x 403 eqn = '1 / ({0})'.format(self.formatNum(self.stack[0])) 404 self.stack[0] = 1 / self.stack[0] 405 elif cmdStr == 'E^X': # inverse natural log 406 eqn = 'e^({0})'.format(self.formatNum(self.stack[0])) 407 self.stack[0] = math.exp(self.stack[0]) 408 elif cmdStr == 'TN^X': # inverse base 10 log 409 eqn = '10^({0})'.format(self.formatNum(self.stack[0])) 410 self.stack[0] = 10.0 ** self.stack[0] 411 else: 412 eqn = '{0}({1})'.format(cmdStr, self.formatNum(self.stack[0])) 413 if cmdStr == 'SQRT': # square root 414 self.stack[0] = math.sqrt(self.stack[0]) 415 elif cmdStr == 'SIN': # sine 416 self.stack[0] = math.sin(self.stack[0] * 417 self.angleConv()) 418 elif cmdStr == 'COS': # cosine 419 self.stack[0] = math.cos(self.stack[0] * 420 self.angleConv()) 421 elif cmdStr == 'TAN': # tangent 422 self.stack[0] = math.tan(self.stack[0] * 423 self.angleConv()) 424 elif cmdStr == 'LN': # natural log 425 self.stack[0] = math.log(self.stack[0]) 426 elif cmdStr == 'ASIN': # arcsine 427 self.stack[0] = math.asin(self.stack[0]) \ 428 / self.angleConv() 429 elif cmdStr == 'ACOS': # arccosine 430 self.stack[0] = math.acos(self.stack[0]) \ 431 / self.angleConv() 432 elif cmdStr == 'ATAN': # arctangent 433 self.stack[0] = math.atan(self.stack[0]) \ 434 / self.angleConv() 435 elif cmdStr == 'LOG': # base 10 log 436 self.stack[0] = math.log10(self.stack[0]) 437 else: 438 return False 439 self.flag = Mode.saveMode 440 self.updateXStr() 441 if eqn: 442 self.history.append((eqn, self.stack[0])) 443 self.histChg += 1 444 maxLen = self.option.intData('MaxHistLength', 445 CalcCore.minMaxHist, 446 CalcCore.maxMaxHist) 447 while len(self.history) > maxLen: 448 del self.history[0] 449 return True 450 except (ValueError, ZeroDivisionError): 451 self.xStr = 'error 0' 452 self.flag = Mode.errorMode 453 return True 454 except OverflowError: 455 self.xStr = 'error 9' 456 self.flag = Mode.errorMode 457 return True 458 459 def printDebug(self): 460 """Print display string and all registers for debug. 461 """ 462 print('x =', self.xStr) 463 print('\n'.join([repr(num) for num in self.stack])) 464 465 466if __name__ == '__main__': 467 calc = CalcCore() 468 calc.printDebug() 469 while 1: 470 ans = input('Entry->') 471 if ans in ('ENT', 'X<>Y', 'CHS', 'CLR', '<-', 'X^2', 'SQRT', 'Y^X', 472 'XRT', 'RCIP', 'SIN', 'COS', 'TAN', 'LN', 'E^X', 'ASIN', 473 'ACOS', 'ATAN', 'LOG', 'TN^X', 'STO', 'RCL', 'R<', 'R>', 474 'PI'): 475 calc.cmd(ans) 476 calc.printDebug() 477 else: 478 for ch in ans: 479 if ch == 'e': 480 ch = 'ENT' 481 calc.cmd(ch) 482 calc.printDebug() 483