6
\$\begingroup\$

I wrote a very minimal spreadsheet program in Python with tkinter.

Here I present the help text:

You can use this program to calculate neatly in table format.

SPACES ARE MANDATORY:

5 + B1 =             # Is correct
5+B1=                # Is wrong

In order to perform the calculation please end the formula with an equal sign = and press the ENTER or RETURN key.

What you write it will be executed as Python code, so you
* Can use all built-in functions and those in the math module** ** cell names must be surrounded by spaces, like min(3, B5 )
* Must be careful not to hand to this program to ill-intentioned people.

If a cell is modified, cells depending on it are NOT automatically updated. You must go back to them, delete what's after the = sign and press enter again.

Load and save work with .csv format.

Any and all criticism is welcome, I most prefer simplifications and ways to reduce repetition.

Here is a screenshot, (the above window is what pops-up when you press the HELP button):

Example usage

try:
    import tkinter as tk
    import filedialog as filedialog
except ImportError:
    import Tkinter as tk
    import tkFileDialog as filedialog

from string import ascii_uppercase
import math

HELP_TEXT ="""
Welcome to the minimal spreadsheet.

You can use this program to calculate neatly in table format.

SPACES ARE MANDATORY:

   5 + B1 =             # Is correct
   5+B1=                # Is wrong

In order to perform the calculation please end the formula with
an equal sign `=` and press the ENTER or RETURN key.

What you write it will be executed as Python code, so you
 * Can use all built-in functions and those in the math module**
   ** cell names must be surrounded by spaces, like `min(3, B5 )`
 * Must be careful not to hand to this program to ill-intentioned people.

If a cell is modified, cells depending on it are NOT automatically updated.
You must go back to them, delete what's after the `=` sign and press enter again.

Load and save work with `.csv` format.
"""

NUMBER_OF_ROWS = 15
NUMBER_OF_COLUMNS = 7

def is_reference(t):
    try:
        return t[0] in ascii_uppercase and t[1] in "1234567890" and (len(t) == 2 or t[-1] in "1234567890")
    except IndexError:
        return False

def get_reference(t, entries):
    x = ascii_uppercase.index(t[0])
    y = int(t[1:])
    return (entries[y-1][x]).get().split("=")[-1]

def evaluate_expression(entry, expression, entries):
    # Splitting by space is easier but frustating / unintutitve for the use
    # Splitting by regex is a bit more complex but allow space-less input, (more user-friendly)
    tokens = expression.replace('=', '').split(' ')
    tokens = [get_reference(t, entries) if is_reference(t) else t for t in tokens]
    entry.insert(len(expression), " " + str(eval(' '.join(tokens))))

root = tk.Tk()

root.wm_title("Minimal spreadsheet")

def display_popup(text):
    toplevel = tk.Toplevel()
    label1 = tk.Label(toplevel, text=text, height=0, width=100)
    label1.pack()

def save():
    filename = filedialog.asksaveasfilename()
    with open(filename,"w+") as f:
        for row in entries:
            f.write(','.join(cell.get() for cell in row) + "\n")

def open_():
    filename = filedialog.askopenfilename()
    with open(filename) as f:
        for y, line in enumerate(f):
            for x, val in enumerate(line.split(',')):
                entries[y][x].delete(0, tk.END)
                entries[y][x].insert(0, val if val != '''
''' else '')
                print val


tk.Button(root, text="Open", command=open_).grid(row=0, column=0)
tk.Button(root, text="Save", command=save).grid(row=0, column=1)


tk.Button(root, text="HELP", command=lambda: display_popup(HELP_TEXT)).grid(row=1, column=0)

for i in range(1, NUMBER_OF_ROWS+1):
    tk.Label(root, text=str(i)).grid(row=i+1, column=0)

for i in range(1, NUMBER_OF_COLUMNS+1):
    tk.Label(root, text=ascii_uppercase[i-1]).grid(row=0+1, column=i)

entries = []
for y in range(NUMBER_OF_ROWS):
    temp_entries = []
    for x in range(NUMBER_OF_COLUMNS):
        e = tk.Entry(root)
        e.grid(row=y+1+1, column=x+1)
        e.bind('<Return>', lambda _, e=e: evaluate_expression(e, e.get(), entries))# Not yet defined?
        temp_entries.append(e)
    entries.append(temp_entries)

root.mainloop()
\$\endgroup\$
4
\$\begingroup\$

is_reference

The possibility of an IndexError can be mostly eliminated by reordering the conditions:

return (len(t) == 2 or t[-1] in "1234567890") and t[0] in ascii_uppercase and t[1] in "1234567890"

The only case this doesn't handle is the empty t. For that, you can add t and at the start, that way the code will be perfectly safe, never raising an IndexError.

Another issue in this function is checking if a symbol is a letter or a digit. Using in for this is not great, as it may iterate over all the letters in the alphabet and digits. It would be better to rewrite using ranges:

return t and (len(t) == 2 or '0' <= t[-1] <= '9') and 'A' <= t[0] <= 'Z' and '0' <= t[1] <= '9'

Use the print() function

It's good to use the print() function instead of the print statement. It should be available in any modern 2.x version, it will keep the code closer to being 3.x-ready.

|improve this answer|||||
\$\endgroup\$
4
\$\begingroup\$

The code looks okayish so my point are mostly about code style and preferences, you might easily ignore any if you prefer a different coding style.

Apart form things that @janos mentioned:

1.get_reference

1.Not necessary parenthesis

2.You can use rsplit

So this:

return (entries[y-1][x]).get().split("=")[-1]

Becomes:

return entries[y-1][x].get().rsplit("=")[1]

3.evalutate_expression

tokens = [get_reference(t, entries) if is_reference(t) else t for t in tokens]

You can make this generator to save some memory:

tokens = (get_reference(t, entries) if is_reference(t) else t for t in tokens)

Note: I personally think that you should avoid using eval anywhere when it's possible, I know that your idea was to build a simple spreadsheet program, but still I think it makes sense to invest some time to create a proper functions parser instead of using eval

2.save

def save():
    filename = filedialog.asksaveasfilename()
    with open(filename,"w+") as f:
        for row in entries:
            f.write(','.join(cell.get() for cell in row) + "\n")

Can be simplified using writelines

def save():
    filename = filedialog.asksaveasfilename()
    with open(filename,"w+") as f:
        f.writelines(','.join(cell.get() for cell in row) for row in entries)

4.Move all the things related to TK initialization to if __name__ == '__main__' block.

|improve this answer|||||
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.