File: quac.pyw
   1 #!/usr/bin/python3
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright © 2024 pacman64
   6 #
   7 # Permission is hereby granted, free of charge, to any person obtaining a copy
   8 # of this software and associated documentation files (the “Software”), to deal
   9 # in the Software without restriction, including without limitation the rights
  10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11 # copies of the Software, and to permit persons to whom the Software is
  12 # furnished to do so, subject to the following conditions:
  13 #
  14 # The above copyright notice and this permission notice shall be included in
  15 # all copies or substantial portions of the Software.
  16 #
  17 # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23 # SOFTWARE.
  24 
  25 
  26 # quac.pyw
  27 #
  28 # Quick Adder/Calculator (QuAC) does what it says with a GUI.
  29 
  30 
  31 import math
  32 from math import \
  33     acos, acosh, asin, asinh, atan, atan2, atanh, ceil, comb, \
  34     copysign, cos, cosh, degrees, dist, e, erf, erfc, exp, expm1, \
  35     fabs, factorial, floor, fmod, frexp, fsum, gamma, gcd, hypot, inf, \
  36     isclose, isfinite, isinf, isnan, isqrt, lcm, ldexp, lgamma, log, \
  37     log10, log1p, log2, modf, nan, nextafter, perm, pi, pow, prod, \
  38     radians, remainder, sin, sinh, sqrt, tan, tanh, tau, trunc, ulp
  39 try:
  40     from math import cbrt, exp2
  41 except Exception:
  42     pass
  43 
  44 from random import \
  45     betavariate, choice, choices, expovariate, gammavariate, gauss, \
  46     getrandbits, getstate, lognormvariate, normalvariate, paretovariate, \
  47     randbytes, randint, random, randrange, sample, seed, setstate, \
  48     shuffle, triangular, uniform, vonmisesvariate, weibullvariate
  49 
  50 from statistics import \
  51     bisect_left, bisect_right, fmean, \
  52     geometric_mean, harmonic_mean, mean, median, \
  53     median_grouped, median_high, median_low, mode, multimode, pstdev, \
  54     pvariance, quantiles, stdev, variance
  55 try:
  56     from statistics import \
  57         correlation, covariance, linear_regression, mul, reduce
  58 except Exception:
  59     pass
  60 
  61 from tkinter import Tk, Button, Entry, Frame, Label, END, EventType
  62 from tkinter.messagebox import showerror
  63 from tkinter.ttk import Treeview
  64 
  65 
  66 # some convenience aliases to various funcs from the python stdlib
  67 geomean = geometric_mean
  68 harmean = harmonic_mean
  69 sd = stdev
  70 popsd = pstdev
  71 var = variance
  72 popvar = pvariance
  73 randbeta = betavariate
  74 randexp = expovariate
  75 randgamma = gammavariate
  76 randlognorm = lognormvariate
  77 randnorm = normalvariate
  78 randweibull = weibullvariate
  79 
  80 # some occasionally-useful values
  81 kb = 1024
  82 mb = 1024 * kb
  83 gb = 1024 * mb
  84 tb = 1024 * gb
  85 pb = 1024 * tb
  86 mole = 602214076000000000000000
  87 mol = mole
  88 
  89 
  90 # results is a list of formula-result pairs
  91 results = []
  92 
  93 
  94 # quick guide for the treeview-specific api
  95 # https://riptutorial.com/tkinter/example/31880/treeview--basic-example
  96 
  97 
  98 def update() -> None:
  99     'Evaluates the current value/formula, then updates the bottom table.'
 100 
 101     s = inp.get().strip()
 102     if s == '':
 103         # ignore empty(ish) inputs
 104         return
 105 
 106     try:
 107         v = float(eval(s))
 108         inp.config(bg='white', fg='black')
 109         results.append((s, v))
 110     except Exception:
 111         inp.config(bg='darkred', fg='white')
 112     # clear the output
 113     for c in out.get_children():
 114         out.delete(c)
 115 
 116     total = 0
 117     for e in results:
 118         total += float(e[1])
 119     win.title(f'Σ ≈ {total:,.4f} (QuAC)')
 120 
 121     # update output rows
 122     j = 0  # the actual row-insertion index
 123     for i, e in enumerate(results):
 124         # visually-separate groups of 5 rows by adding an empty one
 125         if i > 0 and i % 5 == 0:
 126             out.insert('', j, text='')
 127             j += 1
 128         val = [e[0], f'{e[1]:,.6f}']
 129         out.insert('', j, text=str(j+1), values=val)
 130         j += 1
 131 
 132 
 133 def check_shortcuts(event: EventType) -> None:
 134     char = event.char
 135     if char == '':
 136         return
 137     # copy formula and result to clipboard when enter is pressed
 138     if ord(char) == 13:
 139         update()
 140     # quit when esc key is pressed
 141     if ord(char) == 27:
 142         win.quit()
 143 
 144 
 145 def clear() -> None:
 146     global results
 147     results = []
 148     # clear the output
 149     for c in out.get_children():
 150         out.delete(c)
 151     # auto-focus on formula input
 152     inp.select_range(0, END)
 153     inp.focus_set()
 154     # reset title too
 155     win.title('Quick Adder/Calculator (QuAC)')
 156 
 157 
 158 try:
 159     win = Tk()
 160     win.title('Quick Adder/Calculator (QuAC)')
 161     win.bind('<Escape>', lambda _: win.quit())
 162 
 163     # top of window: formula-input and button to clear results
 164     top = Frame(win)
 165     top.pack()
 166     Label(top, text='Number or Formula').grid(row=0, column=0, padx=12)
 167     inp = Entry(top, text='', width=28, borderwidth=1)
 168     inp.grid(row=0, column=1, padx=0)
 169     inp.bind('<KeyPress>', check_shortcuts)
 170     inp.select_range(0, END)
 171     inp.focus_set()
 172     cl = Button(top, text='Clear', font=8, command=clear)
 173     cl.grid(row=0, column=2, padx=0, sticky='e')
 174     cl.bind('<KeyPress>', lambda _: clear())
 175 
 176     # bottom of window only has the formula-results table
 177     bottom = Frame(win)
 178     bottom.pack(padx=0)
 179     out = Treeview(bottom, column=['formula', 'result'], height=36)
 180     out.heading('#0', text='#')
 181     out.column('#0',  width=40)
 182     out.heading('formula', text='formula')
 183     out.column('formula', width=200, anchor='e')
 184     out.heading('result', text='result')
 185     out.column('result', width=240, anchor='e')
 186     out.pack()
 187 
 188     # run gui
 189     win.mainloop()
 190 except Exception as e:
 191     showerror('Error', str(e))
 192     win.quit()