File: android-tkinter/CODE/calculator.py (original) (raw)

#!/usr/local/bin/python """ ################################################################################ PyCalc 4.0: a Python/tkinter calculator program and GUI component.

[SA] 4.0, Sep-2017: standalone release of PyCalc, PyClock, PyPhoto, PyToe. Copyright: 1996-2019 M.Lutz, from book "Programming Python, 4th Edition". License: provided freely, but with no warranties of any kind. Homepage: http://learning-python.com/pygadgets.html.

#-------------------------------------------------------------------------------

ANDROID VERSION, March-April 2019 (trivially patched June 2021)

Per http://learning-python.com/using-tkinter-programs-on-android.html#PyGadgets.

Replace original file with this custom version on your Android device (only).

Search for "# ANDROID" for all changes, or date for these recent changes:

[Jun1421] Drop a harmless but confusing hardcoded "1" for RunningOnMac

[Apr1919] Open "hist" a bit wider for usability: small phones have room.

[Apr1219] Reduce the initial size of the "hist" history display for better

fit (though accommodating both phone orientations is impossible).

Do similar for "cmd" popups: shrink, use a smaller font to allow

more content, redo build code to clip Run button last if resized.

Also gets new "OK" button and size of helpmessage.py dialogs.

[Apr0419] Enable enter/return in keyboards too (but shifted operator keys fail

with no known workaround; tap/click GUI buttons for operator keys).

Also keep half of history on "clear" trims (not Android specific).

[Apr0219] Enable backspace in on-screen keyboards too (but enter doesn't work).

Also fix traceback on rare empty+"eval" made more likely by new "back".

[Mar3119] "back" backspace button, fix stuck-on buttons, history color/font.

All undated Android changes ahead were made in this release.

#-------------------------------------------------------------------------------

Evaluates expressions as they are entered per operator precedence, catching GUI button clicks and keyboard keys for expression entry.

Versions:

4.0 (2017, standalone rlease [SA]) -automatic comma separators in main display -enhanced help display -Mac OS port -new configs model: file or args, more options -extra operators via new 'more' button frame -x! factorial, -x, int(x), and 'e' extra ops -x^y power operator with same precedence as * and / -statistics module functions import

3.0+ (2010, PP4E, version number retained): -port to run under Python 3.X (only) -drop 'L' keypress (the long type is now dead in earnest)

3.0 changes (2005, PP3E): -use 'readonly' entry state, not 'disabled', else field is greyed out (fix for 2.3 Tkinter change); -avoid extended display precision for floats by using str(), instead of x/repr() (fix for Python change); -apply font to input field to make it larger; -use justify=right for input field so it displays on right, not left; -add 'E+' and 'E-' buttons (and 'E' keypress) for float exponents; 'E' keypress must generally be followed digits, not + or - optr key; -remove 'L' button (but still allow 'L' keypress): superfluous now, because Python auto converts up if too big ('L' forced this in past); -use smaller font size overall; -auto scroll to the end in the history window

2.0 (1999, PP2E) evaluated expression parts as entered, added integrated command-line popups, a recent calculations history display popup, fonts and colors configuration, help and about popups, preimported math/random constants, and more;

1.0 (1996, PP1E) was a very simplistic calculator GUI that just built up Python expressions and passed them to eval() as a whole;

Misc. notes: done 4.0: add a commas-insertion mode (see StringVarCommas); done 4.0: allow '**' as an operator key (added as "^" key) todo: allow '+' and 'J' inputs for complex Numbers todo: use new decimal type for fixed precision floats; as is, can use 'cmd' popup windows to input and evaluate things like complex, but can't be input via main window; caveat: PyCalc's precision, accuracy, and some of its behaviour, is currently bound by result of the built-in str() call; ################################################################################ """

import sys from tkinter import * # widgets, consts from PP4E.Gui.Tools.guimixin import GuiMixin # quit method from PP4E.Gui.Tools.widgets import label, entry, button, frame # widget builders

debugme = True def trace(*args): if debugme: print(args)

[SA] port to Mac OS

ANDROID, Jun21: drop a spurious hardcoded "1#" (true) here, else Android

runs macos menubar code at end, though it was a harmless no-op; all other

macos-specific cases use "RunningOnMac or RunningOnAndoid" (hence the "1")

RunningOnMac = sys.platform.startswith('darwin') # all Mac OS (X) [not Android]

[SA]: set window icons on Windows and Linux

from windowicons import trySetWindowIcon

ANDROID - stuck-on buttons fix, etc.

RunningOnAndroid = True

################################################################################

The main class - handles user interface.

An extended Frame, on new Toplevel, or embedded in another container widget.

################################################################################

class CalcGui(GuiMixin, Frame):

Operators = "+-*/^="                             # button lists, [SA] +^ 
Operands  = ["abcd", "0123", "4567", "89()"]     # customizable here

# [SA] additions, from book's subclasses
Extras = [(' x! ',  'factorial(%s)'),
          ('-x ',   '-(%s)'),
          ('x^2',   '(%s)**2'),
          ('1/x',   '1.0/(%s)'),
          ('sqrt',  'sqrt(%s)'),
          ('int',   'int(%s)')]

def __init__(self, parent=None, configs=object()):
    Frame.__init__(self, parent)                 # None=default Tk root
    self.pack(expand=YES, fill=BOTH)             # all parts expandable
    self.eval = Evaluator()                      # embed a stack handler
    self.text = StringVarCommas()                # extended linked variable
    self.text.set("0")
    self.erase = 1                               # clear "0" text next
    self.makeWidgets(configs)                    # build the GUI itself
    if not parent or not isinstance(parent, Frame):
        self.master.title('PyCalc 4.0')          # title iff owns window
        self.master.iconname("PyCalc")           # ditto for key bindings
        self.master.bind('<KeyPress>', self.onKeyboard)
        self.entry.config(state='readonly')      # 3.0: not 'disabled'=grey
    else:
        self.entry.config(state='normal')
        self.entry.focus()
    self.root = parent

def makeWidgets(self, configs): 
    """
    build the GUI (N frames plus text-entry), register events:
    """
    font = configs.Font                          # font, color configurable
    bg, fg = configs.BgColor, configs.FgColor

    self.entry = entry(self, TOP, self.text)
    self.entry.config(font=font)                 # 3.0: make display larger
    self.entry.config(justify=RIGHT)             # 3.0: on right, not left
   #self.entry.pack(expand=NO, fill=X)           # [SA] grow vertically too

    for row in self.Operands:
        frm = frame(self, TOP)
        for char in row:
            if RunningOnMac or RunningOnAndroid:   # ANDROID - else bg botched temp
                # [SA] emulate colored buttons
                l = label(frm, LEFT, char,
                       fg=fg, bg=bg, font=font)
                l.bind('<Button-1>', lambda e, op=char: self.onOperand(op))
            else:
                button(frm, LEFT, char, lambda op=char: self.onOperand(op),
                       fg=fg, bg=bg, font=font)

    frm = frame(self, TOP)
    for char in self.Operators:
        if RunningOnMac or RunningOnAndroid:       # ANDROID - else bg botched temp
            # [SA] emulate colored buttons
            l = label(frm, LEFT, char,
                   fg=bg, bg=fg, font=font)
            l.bind('<Button-1>', lambda e, op=char: self.onOperator(op))

        else:
            button(frm, LEFT, char, lambda op=char: self.onOperator(op),
                   fg=bg, bg=fg, font=font)

    frm = frame(self, TOP)
    button(frm, LEFT, 'dot ', lambda: self.onOperand('.'))
    button(frm, LEFT, ' E+ ', lambda: self.text.set(self.text.get()+'E+'))
    button(frm, LEFT, ' E- ', lambda: self.text.set(self.text.get()+'E-'))
    button(frm, LEFT, 'cmd ', lambda: self.onMakeCmdline(configs))
    button(frm, LEFT, 'help', self.help)
    button(frm, LEFT, 'quit', self.quit)       # from guimixin

    frm = frame(self, BOTTOM)
    button(frm, LEFT, 'eval ', self.onEval)
    button(frm, LEFT, 'hist ', lambda: self.onHist(configs))
    button(frm, LEFT, 'more ', self.onMore)

    # ANDROID - add backspace button for touch, if no keyboard
    button(frm, LEFT, 'back ', lambda: self.onKeyboard(type('dummy', (), dict(char='\b'))))
                        # or=> lambda: self.event_generate('<BackSpace>'))
    button(frm, LEFT, 'clear', self.onClear)

    # [SA] make+hide additions
    morefrm = frame(self, TOP)
    for (lab, expr) in self.Extras:
        button(morefrm, LEFT, lab, (lambda expr=expr: self.onExtra(expr)))
    button(morefrm, LEFT, 'pi', lambda: self.onLiteral('pi'))
    button(morefrm, LEFT, 'e',  lambda: self.onLiteral('e'))
    morefrm.pack_forget()
    self.morefrm, self.moretgl = morefrm, 0

    # [SA] bind Delete for erase on Macs (\b isn't auto)
    #
    # ANDROID [Apr0219] - ditto, though you need an on-demand keyboard, 
    # and this isn't very useful because the enter/return key doesn't 
    # work (its <KeyPress> sends "" for all on-screen keyboards tested);
    # all keys work fine and as expected in "cmd" command-line popups;
    #
    # ANDROID [Apr0419] - fix return key by extra handler, but operator 
    # keys still don't work: in Pydroid 3 they don't set .char, .keysym
    # is unusable (see ahead), and this is not worth further work when  
    # "cmd" works fully and the GUI's buttons can be tapped or clicked;
    # binding to <plus> and <asterisk> here doesn't help (but why?-TBD);
    # note that tkinter fires <Return> xor <KeyPress> (preferring first);
    #
    if RunningOnMac or RunningOnAndroid:
        class Dummy1: char = '\b'
        self.master.bind('<BackSpace>', lambda e: self.onKeyboard(Dummy1))
    if RunningOnAndroid:
        class Dummy2: char = '\r'    # or use type() or instances
        self.master.bind('<Return>', lambda e:self.onKeyboard(Dummy2))       

def onMore(self):
    """
    [SA] show/hide extra-row additions
    """
    self.moretgl += 1
    if self.moretgl % 2:
        self.morefrm.pack(expand=YES, fill=BOTH)
    else:
        self.morefrm.pack_forget()

def onClear(self):
    """
    clear calculator state 
    """
    self.eval.clear()
    self.text.set('0')
    self.erase = 1

def onEval(self):
    """
    run eval operation: eval all still-open exprs
    """
    self.eval.shiftOpnd(self.text.get())     # last or only opnd
    self.eval.closeall()                     # apply all optrs left
    self.text.set(self.eval.popOpnd())       # need to pop: optr next?
    self.erase = 1

def onOperand(self, char):
    """
    handle an operand button or keypress
    """
    if char == '(':
        self.eval.open()
        self.text.set('(')                      # clear text next
        self.erase = 1
    elif char == ')':
        self.eval.shiftOpnd(self.text.get())    # last or only nested opnd
        self.eval.close()                       # pop here too: optr next?
        self.text.set(self.eval.popOpnd())
        self.erase = 1
    else:
        if self.erase:
            self.text.set(char)                     # clears last value
        else:
            self.text.set(self.text.get() + char)   # else append to opnd
        self.erase = 0
    self.update()

def onOperator(self, char):
    """
    handle an operator button or keypress
    """
    self.eval.shiftOpnd(self.text.get())    # push opnd on left
    self.eval.shiftOptr(char)               # eval exprs to left?
    self.text.set(self.eval.topOpnd())      # push optr, show opnd|result
    self.erase = 1   
    self.update()                       # erased on next opnd|'('

def onExtra(self, expr):
    """
    [SA] addition: run extra-row expr with value substitution
    """
    try:
        self.text.set(self.eval.runstring(expr % self.text.get()))
    except:
        self.text.set('ERROR')

def onLiteral(self, literal):
    """
    [SA] addition: run extra-row literal expr
    """
    self.text.set(self.eval.runstring(literal))  # e.g., 'pi', 'e'

def onMakeCmdline(self, configs):
    """
    new non-modal top-level window for arbitrary Python code
    """
    new = Toplevel()                            # new top-level window
    new.title('PyCalc Command Line')
    trySetWindowIcon(new, 'icons', 'pygadgets') # [SA] for win+lin

    frm = frame(new, TOP)                       # only the Entry expands
    label(frm, LEFT, '>>>').pack(expand=NO)

    # ANDROID [Apr1219]: make+pack button first so clipped last if resized
    onButton = (lambda: self.onCmdline(var, ent))
    onReturn = (lambda event: self.onCmdline(var, ent))
    button(frm, RIGHT, 'Run', onButton).pack(expand=NO)

    # ANDROID [Apr1219]: use smaller font to shrink and allow more content on phones
    cmdfont = 'courier 8 normal'                # was configs.Font (= main buttons)
    var = StringVar()                           # [SA] no commas here
    ent = entry(frm, LEFT, var, width=30)       # ANDROID [Apr1219] smaller, was 40
    ent.config(font=cmdfont)                    # [SA] now configurable
    ent.bind('<Return>', onReturn)
    var.set(self.text.get())
    ent.focus()   # [SA] on this entry and window

def onCmdline(self, var, ent): 
    """
    evaluate cmdline pop-up input
    """
    try:
        value = self.eval.runstring(var.get())
        var.set('OKAY')
        if value != None:                 # run in eval namespace dict
            self.text.set(value)          # expression or statement
            self.erase = 1
            var.set('OKAY => '+ value)
    except:                               # result in calc field
        var.set('ERROR')                  # status in pop-up field
    ent.icursor(END)                      # insert point after text
    ent.select_range(0, END)              # select msg so next key deletes

def onKeyboard(self, event):
    """
    on keyboard press event, pretend button was pressed,
    or handle extras - backspace (not delete), ?=help;
    """
    #        
    # ANDROID [Apr0219-Apr0419] - in Pydroid 3's tkinter:
    # -on backspace, event .char='' and .keysym='BackSpace'
    # -on return,    event .char='' and .keysym='Return'
    # this differs from expected behavior; addressed with 
    # '<Return>' and '<BackSpace>' handler binds elsewhere;
    #
    # ANDROID [Apr0419] _shifted_ operator keys (e.g. '+')
    # .char is '' too, but .keysym is not usable - tkinter
    # sends two separate events: Shift_L/R + the *unshifted*
    # key (though using "cmd" popups seems to oddly fix this);
    # punt: this seems a tkinter bug - use "cmd" or buttons;
    #
    tracekb = False  #True
    if RunningOnAndroid and tracekb: 
        print('char=%r, keysym=%r' % 
          tuple(getattr(event, attr, 'none') for attr in ('char', 'keysym')))
   
    pressed = event.char 
    if pressed != '':
        if pressed in self.Operators:
            self.onOperator(pressed)
        else:
            for row in self.Operands:
                if pressed in row:
                    self.onOperand(pressed)
                    break
            else:                                          # 4E: drop 'Ll'
                if pressed == '.':
                    self.onOperand(pressed)                # can start opnd
                if pressed in 'Ee':  # 2e10, no +/-
                    self.text.set(self.text.get()+pressed) # can't: no erase
                elif pressed == '\r':
                    self.onEval()                          # enter key=eval
                elif pressed == ' ':
                    self.onClear()                         # spacebar=clear
                elif pressed == '\b':
                    self.text.set(self.text.get()[:-1])    # backspace or "back"
                elif pressed == '?':                       # [SA] +Mac delete
                    self.help()

def onHist(self, configs):
    """
    show recent calcs log popup
    """
    from tkinter.scrolledtext import ScrolledText     # or PP4E.Gui.Tour

    new = Toplevel()                                  # make new window
    new.title('PyCalc History')
    trySetWindowIcon(new, 'icons', 'pygadgets')       # [SA] for win+lin

    # new window goes away on ok press or enter key
    ok = Button(new, text=' OK ', command=new.destroy)
    ok.pack(pady=1, side=BOTTOM)                      # pack first=clip last
    new.bind("<Return>", (lambda event: new.destroy()))

    text = ScrolledText(new, bg='beige')              # add Text + scrollbar
    bg, font = configs.HistBgColor, configs.HistFont  # [SA] now configurable
    text.config(bg=bg, font=font)
    text.insert('0.0', self.eval.getHist())           # get Evaluator text
    text.see(END)                                     # 3.0: scroll to end
    text.pack(expand=YES, fill=BOTH)

    # ANDROID [Apr1219] - start smaller for fit on phones, user can resize;
    # hist font preset in __main__ is larger than help font in helpmessage.py,
    # and smaller than "cmd" popup font above, but all are tailored for fit;
    # ANDROID [Apr1919] - open "hist" 45 wide, not 40, for ease (there's room);
    #
    text.config(width=45, height=20)    # chars, lines (more or less: see Tk)

    # go modal until window destroyed
    ok.focus_set()                      # make new window modal:
    new.grab_set()                      # get keyboard focus, grab app
    new.wait_window()                   # don't return till new.destroy

def help(self):
    """
    [SA] fully redesigned, and helpmessage replaces self.infobox();
    called for 'help' button click, '?' keyboard press, Mac menus;
    """
    from helpmessage import showhelp
    showhelp(self.root, 'PyCalc', self.HelpText, forcetext=False,
             setwinicon=lambda win:
                    trySetWindowIcon(win, 'icons', 'pygadgets'))
    #if self.root: self.root.focus_force()   # now done in helpmessage

HelpText = ('PyCalc 4.0\n'
            '\n'
            'A Python/tkinter calculator GUI.\n'
            'For Mac OS, Windows, Linux, and Android.\n'
            'From the book Programming Python.\n'
            'Author and © M. Lutz 1996-2019.\n'
            '\n'
            'Use button clicks or keyboard presses to '
            'input numbers and operators, or type '
            'Python expression code in a "cmd" popup.\n'
            '\n'
            'Keyboard usage: spacebar="clear", enter="eval", '
            '.="dot", ?="help", backspace or delete=erase 1 character.  '
            'Comma separators are inserted automatically for '
            'display as numbers are entered and shown.\n'
            '\n'
            'Tips:\n'
            '▶ "=" assigns variables '
            '(e.g., ab=99, ab+1)\n'
            '▶ "eval" evaluates pending expressions\n'
            '▶ "more" shows/hides extra keys\n'
            # ANDROID
            '▶ "back" is backspace for main display\n'
            '▶ "hist" displays recent calculations\n'
            '▶ "^" is x^y power (Python\'s "**")\n'
            '▶ "int" and "* 1." convert to int and float\n'
            '\n'
            'The "cmd" dialog supports entry of additional '
            'ops, including all functions in Python\'s math, '
            'random, statistics, and builtins modules.  E.g., '
            'sin(x), log(x, b), random(), mean([]), max(x, y), set().\n'
            '\n'
            # ANDROID
            'Android users: the "back" key is backspace '
            'for touch.  Keyboards are limited: use the '
            '"cmd" command-line popup for general entry.\n'
            '\n'
            'Version history (see source for changes):\n'
            '● 4.0: Jan 2019, Android release\n'
            '● 4.0: Sep 2017, standalone release\n'
            '● 3.1: May 2010, Programming Python 4E\n'
            '● 3.0 2005 3E, 2.0 1999 2E, 1.0 1996 1E\n'
            '\n'
            'For downloads and more apps, visit:\n'
            'http://learning-python.com/programs.html'
           )

################################################################################

The expression evaluator class.

Embedded in and used by a CalcGui instance, to perform calculations.

################################################################################

class Evaluator: def init(self): self.names = {} # a names-space for my vars self.opnd, self.optr = [], [] # two empty stacks self.hist = [] # my prev calcs history log

    # preimport math modules into calc's namespace for "cmd"
    # namespaces are disjoint: set(dir(math)) & set(dir(other))
    
    self.runstring("from math import *")         # sin(x), log(x, b), pi, e
    self.runstring("from random import *")       # plus builtins: max(), abs()
    try:
        # [SA] new in py 3.4, ignore if absent
        self.runstring("from statistics import *")   # mean(), median(), etc
    except:
        print('Note: PyCalc cannot load statistics module in your Python;')
        print('upgrade to Python 3.4 or later to use its tools in PyCalc.')

def clear(self):
    self.opnd, self.optr = [], []           # leave names intact
    if len(self.hist) > 128:                # don't let hist get too big
        # 
        # ANDROID [Apr0419] - keep latest half of history (all platforms)
        # self.hist = ['clear']
        #
        self.hist = self.hist[-64:] + ['--clear and trim--']
    else:
        self.hist.append('--clear--')

def popOpnd(self):
    value = self.opnd[-1]                   # pop/return top|last opnd
    self.opnd[-1:] = []                     # to display and shift next
    return value                            # or x.pop(), or del x[-1]

def topOpnd(self):
    return self.opnd[-1]                    # top operand (end of list)

def open(self):
    self.optr.append('(')                   # treat '(' like an operator

def close(self):                            # on ')' pop downto highest '('
    self.shiftOptr(')')                     # ok if empty: stays empty
    self.optr[-2:] = []                     # pop, or added again by optr

def closeall(self):
    while self.optr:                        # force rest on 'eval'
        self.reduce()                       # last may be a var name
    try:
        self.opnd[0] = self.runstring(self.opnd[0])
        #
        # ANDROID [Apr0219] - fix a rare special case: an immediate backspace 
        # or new "back" button followed by "eval" can cause an empty string to 
        # succeed as a statement and return+push None, which breaks commify();
        # other "back" empty-string cases all fail normally as "*ERROR*" opnds;
        # this can occur in pre-Android PyCalc too but is likelier with "back";
        #
        assert self.opnd[0] != None
    except:
        self.opnd[0] = '*ERROR*'            # pop else added again next:

afterMe = {'^': ['+', '-', '(', '='],       # [SA] add power operator
           '*': ['+', '-', '(', '='],       # class member
           '/': ['+', '-', '(', '='],       # optrs to not pop for key
           '+': ['(', '='],                 # if prior optr is this: push
           '-': ['(', '='],                 # else: pop/eval prior optr
           ')': ['(', '='],                 # all left-associative as is
           '=': ['('] }

def shiftOpnd(self, newopnd):               # push opnd at optr, ')', eval
    self.opnd.append(newopnd)

def shiftOptr(self, newoptr):               # apply ops with <= priority
    while (self.optr and
           self.optr[-1] not in self.afterMe[newoptr]):
        self.reduce()
    self.optr.append(newoptr)               # push this op above result
                                            # optrs assume next opnd erases
def reduce(self):
    trace(self.optr, self.opnd)
    try:                                    # collapse the top expr
        operator       = self.optr[-1]      # pop top optr (at end)
        [left, right]  = self.opnd[-2:]     # pop top 2 opnds (at end)
        self.optr[-1:] = []                 # delete slice in-place
        self.opnd[-2:] = []
        result = self.runstring(left + operator + right)
        if result == None:
            result = left                   # assignment? key var name
        self.opnd.append(result)            # push result string back
    except:
        self.opnd.append('*ERROR*')         # stack/number/name error

def runstring(self, rawcode):
    code = rawcode.replace('^', '**')                     # [SA] xlate power
    try:                                                  # 3.0: not `x`/repr
        result = str(eval(code, self.names, self.names))  # try expr: string
        self.hist.append(rawcode + ' => ' + result)       # add to hist log
    except:
        exec(code, self.names, self.names)                # try stmt: None
        self.hist.append(rawcode)
        result = None
    return result

def getHist(self):
    return '\n'.join(self.hist)

################################################################################

[SA] StringVar wrapper class.

Used to manage thousands-separator commas display with minimal changes.

################################################################################

class StringVarCommas(StringVar): """ in the main number-display area (only: not cmd): auto insert commas as numbers are entered, and remove them when fetched for the evaluator; this also removes commas on backspace erases by a get+set combination; literal comma presses are simply ignored (TBD: use them as a toggle?);

some lex errors are caught here (e.g., '1E2.'), but others pass here 
and fail later in the evaluator (e.g., '1..2', '..1', '1EE2', '.'), 
due in part to split(x, 1): removing the 1 would make more fail here;
"""

# extend StringVar interface

def get(self):
    text = StringVar.get(self)         # get display text 
    return self.decommify(text)        # strip commas

def set(self, text):
    text = self.commify(text)          # add commas
    StringVar.set(self, text)          # set display text


# add text processing methods

def decommify(self, text):
    return text.replace(',', '')

def commify(self, text):
    text = self.decommify(text)
   #text = '{:,}'.format(num)          # requires an eval()

    # strip sign if added by -X key
    if text.startswith('-'):
        sign, text = '-' , text[1:]
    else:
        sign = ''

    # add commas to whole-number part
    try:
        if text.isdigit():                                  # all digits: 'xxx'
            return sign + '{:,}'.format(int(text))          # add ',' and sign 

        elif '.' in text:                                   # also for '.' or '..'
            whole, rest = text.split('.', 1)                # 'x.y' 'x.' '.y' 'x.yEz'
            if whole:                                       # nov17: '.any' => '.any'
                whole = '{:,}'.format(int(whole))           # (whole or '0') adds '0'
            return sign + whole + '.' + rest                # covers '0.1e2', '.1e-2'

        elif 'E' in text.upper() and 'ERROR' not in text:
            whole, exp = text.upper().split('E', 1)         #  no '.' but whole base
            whole = '{:,}'.format(int(whole))               # 'xe+z', 'xe-z', 'xez'
            return sign + whole + 'E' + exp

        else:
            return text                                     # other: allow 'abcd'

    except:
        return 'ERROR'   # anything not recognized: avoid uncaught exception in GUI

################################################################################

Main logic - when run standalone.

Get optional configs via command-line args, make and start a CalcGui object.

ANDROID - the config file is launcher-only, and Pydroid 3 doesn't do cmd-line

arguments, so configuration options are currently limited to defaults below.

################################################################################

if name == 'main': from getConfigs import getConfigs # [SA] new common gadgets utility

defaults = dict(InitialSize=None,      # None=let tkinter decide
                BgColor='wheat',       # main-display options
                FgColor='black', 
                Font=('courier', 14, 'bold'),  # or 'family...' str arg
                
                # ANDROID - bg was beige, but clashes with main window default
                # ANDROID - font was None, but default 5-pt font is very small
                #
                HistBgColor='ivory',
                HistFont='courier 6 normal')

configs = getConfigs('PyCalc', defaults)       # load from file or args

root = Tk()                                    # non-default top-level window
if configs.InitialSize:
    root.geometry(configs.InitialSize)         # 'Wxh' size string
trySetWindowIcon(root, 'icons', 'pygadgets')   # [SA] for win+lin
calc = CalcGui(root, configs)                  # build gui on root

if RunningOnMac:
    # Mac requires menus, deiconifies, focus

    # [SA] on Mac, customize app-wide automatic top-of-display menu
    from guimaker_pp4e import fixAppleMenuBar
    fixAppleMenuBar(window=root,
                    appname='PyCalc',
                    helpaction=calc.help,      # bound method (has self)
                    aboutaction=None,
                    quitaction=calc.quit)      # app-wide quit: ask

    # [SA] reopen auto on dock/app click and fix tk focus loss on deiconify
    def onReopen():
        root.lift()
        root.update()
        temp = Toplevel()
        temp.lower()
        temp.destroy()
    root.createcommand('::tk::mac::ReopenApplication', onReopen)

root.mainloop()