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