rpn.py

Created by xanderleadaren

Created on March 11, 2025

19 KB

Reverse Polish Notation[WP]

[Toolbox] for RPN-specific hotkeys, [var] for shortcuts to other features mapped to [alpha]+{key}

⚠️ Angles are set to degrees by default. The yellow top left indicator doesn’t reflect this.

[🏠] to exit the program and get back to NumWorks menu

RPN Tutorial by Hans Klaver: Klaverscholten.nl/rpn

Development and Roadmap: on GitHub

(Updated frequently)


__version__ = "2025-03-11 T 13:28 UTC+1"

from math import exp, log, log10, sin, asin, cos, acos, tan, atan, pi, sqrt
from time import sleep, monotonic
from random import random

from ion import keydown
from kandinsky import draw_string, fill_rect

from micropython import kbd_intr
kbd_intr(-1)  # Disable KeyboardInterrupt


# RPN AND PYTHON SPECIFIC FUNCTIONS

def python_int(foo):
    """Python-specific: keep integers instead of floats, if possible."""
    foo = float(foo)
    try: integer = int(foo)
    except OverflowError as message: draw_error(message)
    else:
        if foo == integer:
            foo = integer
        return foo

def python_trailing(value):
    # Remove Python-specific trailing 000000001 if possible
    if value.count(".") == 1 and (value[-1] == "1" or value[-1] == "2"):
        zeros = 0; last = -2
        while value[last] == "0": zeros += 1; last -= 1
        if zeros >= 7: value = value[:last+1]
    # Remove Python-specific trailing 9s if possible
    if value.count(".") == 1 and value[-1] == "9":
        nines = 0; last = -2
        while value[last] == "9": nines += 1; last -= 1
        if nines >= 7:
            value = value[:last+1]
            last_digit = int(value[-1]) + 1
            value = value[:-1] + str(last_digit)
    return value

def drop():
    stack.pop(0)
    if fixed: stack.append(stack[2])

def push(foo, history=True):
    try: top = python_int(foo)
    except Exception as message: draw_error(message)
    else:
        if history: global lastx; lastx = foo
        stack.insert(0, top)
    if fixed: stack.pop()

def evaluate1(operation):
    """Evaluate unary operations."""
    global entry, stack, lastx
    if not entry and stack:
        try: result = operation(stack[0])
        except Exception as message: draw_error(message)
        else: lastx = stack[0]; stack[0] = python_int(result)
    elif entry:
        try: result = operation(float(entry))
        except Exception as message: draw_error(message)
        else: lastx = entry; stack.insert(0, python_int(result)); entry = ""
        draw_command(0)
    draw_register(0, 0.2)

def evaluate2(operation, refresh=True):
    """Evaluate binary operations."""
    global entry, stack, lastx
    if not entry and len(stack) >= 2:
        try: result = operation(stack[1], stack[0])
        except Exception as message: draw_error(message)
        else: lastx = stack[0]; stack[1] = python_int(result); drop()
    elif entry and stack:
        try: result = operation(stack[0], float(entry))
        except Exception as message: draw_error(message)
        else: lastx = entry; stack[0] = python_int(result); entry = ""
    if refresh: display()


# MATH FUNCTIONS

def factorial(n):
    if float(n) != int(n):
        raise Exception("math domain error")
    else:
        prod = 1; max = n
        while max > 0: prod *= max; max -= 1
        return prod

def hms(dec):
    """Convert decimal time in hours to sexagesimal format."""
    hours = int(dec)
    minutes = int((dec - hours) * 60)
    seconds = ((dec - hours) * 60 - minutes) * 60
    return hours + minutes/100 + seconds/10000

def prime_facto(n):
    """Find the lowest prime divisor of a natural number n."""
    if float(n) != int(n):
        raise Exception("math domain error")
    else:
        div = 2
        while div**2 <= n:
            if n % div == 0: return div
            div += 1
        return 1


# GUI FUNCTIONS

def draw_register(level, timeout=0, selected=False):
    """Display the requested stack register on correct background and line height."""
    if fixed:
        height = 46; name = ("X:", "Y:", "Z:", "T:")
    else:
        height = 23; name = ("1:", "2:", "3:", "4:", "5:", "6:", "7:", "8:")
    bg_color = (245,250,255) if level % 2 == 0 else (255,254,255)
    bg_text = (214,213,231) if selected else bg_color
    value = python_trailing(str(stack[level]))
    y_text = 185 - (level+1)*height + (height - 18) // 2
    fill_rect(0, 184 - (level+1)*height, 320, height, bg_color)
    draw_string(name[level], 10, y_text, (0,0,0), bg_color)
    draw_string(value, 310 - 10*len(value), y_text, (0,0,0), bg_text)
    sleep(timeout)

def draw_stack(depth=8, timeout=0):
    """Refresh the first {depth} stack levels from stack top."""
    if fixed: depth = 4
    if depth > len(stack): depth = len(stack)
    for level in range(depth):
        draw_register(level)
    sleep(timeout)

def draw_command(timeout=0.2):
    """Refresh the command line, bottom of the screen."""
    fill_rect(0, 185, 320, 37, (255,254,255))
    draw_string(entry, 5, 195, (0,0,0), (255,254,255))
    blink_cursor(True)
    sleep(timeout)

def display(command_line=True):
    """Refresh the whole screen: background, all stack levels, separator, and command line."""
    fill_rect(0, 0, 320, 184, (245,250,255))
    # On fixed stack, drop oldest level if not displayable
    levels = 4 if fixed else 8
    if fixed and len(stack) > levels: stack.pop()
    n = len(stack) if len(stack) < levels else levels
    draw_stack(n)
    fill_rect(0, 184, 320, 1, (223,217,222))
    if command_line: draw_command()
    else: sleep(0.2)

def blink_cursor(forced=False):
    color = (0,0,0) if int(monotonic()) % 2 == 0 or forced else (255,254,255)
    fill_rect(5 + 10*len(entry), 194, 1, 18, color)


def draw_error(text):
    """Display an error or exception in a black dialog box."""
    fill_rect(144 - 5*len(str(text)), 89, 32 + 10*len(str(text)), 44, (0,0,0))
    draw_string(str(text), 160 - 5*len(str(text)), 102, (255,254,255), (0,0,0))
    sleep(0.5); pressed = False
    while not pressed:
        for i in range(53):
            if keydown(i): pressed = True
    display(False)


def draw_item(line, items, descriptions, selected=False):
    """Display a menu item line, eventually on a selected background."""
    h = 174 // len(items)
    bg_color = (214,213,231) if selected else (255,254,255)
    fill_rect(28, 49 + h*line, 264, h - 1, bg_color)
    draw_string(items[line], 35, 41 + h*line + h // 2, (0,0,0), bg_color)
    draw_string(descriptions[line], 285 - 10*len(descriptions[line]), 41 + h*line + h // 2, (164,165,164), bg_color)

def draw_menu(items, descriptions):
    """Display all items and descriptions menu inside a dialog box."""
    fill_rect(27, 48, 266, 174, (238,238,238))
    fill_rect(28, 49, 264, 173, (255,254,255))
    h = 174 // len(items)
    for i in range(len(items)):
        draw_item(i, items, descriptions)
        fill_rect(28, 48 + h*i, 264, 1, (238,238,238))
    fill_rect(28, 48 + h*len(items), 264, 1, (238,238,238))


def varbox():
    """Display a dialog with functions mapped to ALPHA + some key."""
    keys = ("D", "R", "C", "F", "H", "P", "?")
    desc = ("Set angles to degrees", "Set angles to radians", "Convert °F to °C", "Convert °C to °F", "Convert hrs to h:min", "Prime factorisation", "Random number in [0,1)")
    fill_rect(27, 27, 266, 21, (65,64,65))
    fill_rect(28, 28, 264, 19, (108,99,115))
    draw_string("Alpha shortcuts", 85, 28, (255,254,255), (108,99,115))
    draw_menu(keys, desc)
    sleep(0.5); pressed = False
    while not pressed:
        for i in (4, 5, 15):  # OK, BACK, VAR
            if keydown(i): pressed = True
    display()


def toolbox():
    """Display a dialog with common RPN functions and their mappings."""
    keys = (" xnt", "  (", "  )", " Ans", "[shift]Ans", "[shift] ÷", "[shift] -")
    desc = ("Fixed/dynamic stack", "ROLL (n)/all levels", "SWAP last two levels", "Copy last X value", "Copy 2nd level", "Inverse", "Change signs")
    fill_rect(27, 27, 266, 21, (65,64,65))
    fill_rect(28, 28, 264, 19, (108,99,115))
    draw_string("Hotkeys", 125, 28, (255,254,255), (108,99,115))
    draw_menu(keys, desc)
    sleep(0.5); pressed = False
    while not pressed:
        for i in (4, 5, 16):  # OK, BACK, TOOLBOX
            if keydown(i): pressed = True
    display()


def percentage():
    """Display a dialog with common percentage functions."""
    items = ("%", "Δ%", "%T", "±%", "MU%P")
    descriptions = ("Percentage of X", "Percent difference", "Percent of total", "Evolution or markup", "Markup on price")
    fill_rect(27, 27, 266, 21, (65,64,65))
    fill_rect(28, 28, 264, 19, (108,99,115))
    draw_string("Percentage", 110, 28, (255,254,255), (108,99,115))
    draw_menu(items, descriptions)
    line = 0; quit = False
    draw_item(0, items, descriptions, True)
    while not quit:
        sleep(0.13)
        if keydown(1) and line > 0:  # UP
            draw_item(line, items, descriptions)
            draw_item(line - 1, items, descriptions, True)
            line -= 1
        if keydown(2) and line < len(items) - 1:  # DOWN
            draw_item(line, items, descriptions)
            draw_item(line + 1, items, descriptions, True)
            line += 1
        if keydown(4) or keydown(52):  # OK/EXE
            if line == 0:  # % Percentage of X
                if entry and stack:
                    base = stack[0]
                    evaluate2(lambda x, y: x*y / 100, False)
                    push(base, False)
                    stack[0], stack[1] = stack[1], stack[0]
                elif not entry and len(stack) >= 2:
                    base = stack[1]
                    evaluate2(lambda x, y: x*y / 100, False)
                    push(base, False)
                    stack[0], stack[1] = stack[1], stack[0]
            elif line == 1:  # Δ% Percent difference
                if entry and stack:
                    base = stack[0]
                    evaluate2(lambda x, y: (y-x) / x * 100, False)
                    push(base, False)
                    stack[0], stack[1] = stack[1], stack[0]
                elif not entry and len(stack) >= 2:
                    base = stack[1]
                    evaluate2(lambda x, y: (y-x) / x * 100, False)
                    push(base, False)
                    stack[0], stack[1] = stack[1], stack[0]
            elif line == 2:  # %T Percent of total
                if entry and stack:
                    base = stack[0]
                    evaluate2(lambda x, y: y/x * 100, False)
                    push(base, False)
                    stack[0], stack[1] = stack[1], stack[0]
                elif not entry and len(stack) >= 2:
                    base = stack[1]
                    evaluate2(lambda x, y: y/x * 100, False)
                    push(base, False)
                    stack[0], stack[1] = stack[1], stack[0]
            elif line == 3:  # ±% Evolution, or markup on cost
                evaluate2(lambda x, y: x + x*y / 100)
            elif line == 4:  # MU%P Markup on price, or margin
                evaluate2(lambda x, y: (y-x) / y * 100)
            quit = True; display()
        if keydown(5): quit = True; display()  # BACK


# MAIN PROGRAM

# Original state: dynamic empty stack, no lastX, empty entry command line, angles in degrees
fixed = False; stack = []
lastx = ""; entry = ""
degrees = True

display()
while True:

    # Characters the user may enter on the command line
    if keydown(48): entry += "0"; draw_command()
    elif keydown(42): entry += "1"; draw_command()
    elif keydown(43): entry += "2"; draw_command()
    elif keydown(44): entry += "3"; draw_command()
    elif keydown(36): entry += "4"; draw_command()
    elif keydown(37): entry += "5"; draw_command()
    elif keydown(38): entry += "6"; draw_command()
    elif keydown(30): entry += "7"; draw_command()
    elif keydown(31): entry += "8"; draw_command()
    elif keydown(32): entry += "9"; draw_command()
    elif keydown(49):
        if not entry: entry = "0."
        else: entry += "."
        draw_command()
    elif keydown(50):
        if not entry: entry = "1e"
        else: entry += "e";
        draw_command()
    elif keydown(27):
        if entry: push(entry); entry = ""; draw_command()
        push(pi)
        draw_stack(8, 0.2)

    # RPN-specific
    elif keydown(14):  # XNT
        fixed = not fixed  # Switch between fixed or dynamic stack
        if fixed:  # Max 4 levels, equal to 0 if not used
            for level in range(4, len(stack)): stack.pop()
            for level in range(4 - len(stack)): stack.append(0)
        else:  # All levels should be empty if not used
            while stack[len(stack) - 1] == 0:
                stack.pop()
                if stack == [0]: stack = []; break
        display()
    elif keydown(51):  # Ans
        try:
            if entry: stack.insert(0, python_int(entry)); entry = ""  # LastX
        except Exception as message: draw_error(message)
        else: push(lastx); display()
    elif keydown(4) or keydown(52):  # OK/EXE
        if entry: push(entry); entry = ""; display()  # ENTER
        elif stack: push(stack[0]); draw_stack(8, 0.2)  # DUP
    elif keydown(17):  # BACKSPACE
        if not entry and stack: drop(); display(False)  # DROP stack top level
        else: entry = entry[:-1]; draw_command()  # CLEAR last character on command line
    elif keydown(33):  # (: (n) ROLL down
        if entry:
            try: pos = float(entry)
            except Exception as message: draw_error(message)
            else:
                if pos == int(pos) and int(pos) <= len(stack):
                    entry = ""; draw_command()
                    stack.insert(int(pos-1), stack.pop(0))
                    draw_stack(int(pos))
                else:
                    draw_error("invalid stack level number")
        elif len(stack) >= 2: stack.append(stack.pop(0)); draw_stack(8, 0.2)
    elif keydown(34):  # ): SWAP
        if entry: push(entry); entry = ""
        if len(stack) >= 2: stack[0], stack[1] = stack[1], stack[0]
        draw_stack(2, 0.2)
    elif keydown(1):  # UP: selection of levels if stack is dynamic
        if not fixed and stack:
            level = 0; draw_register(level, 0.2, True)
            while level >= 0:
                if keydown(1) and level < len(stack) - 1:  # UP
                    draw_register(level)
                    draw_register(level + 1, 0.2, True)
                    level += 1
                if keydown(2):  # DOWN
                    draw_register(level)
                    if level > 0: draw_register(level - 1, 0.2, True)
                    level -= 1
                if keydown(17):  # BACKSPACE: DROP
                    stack = stack[level:]; level = -1
                if keydown(4) or keydown(52):  # OK/EXE: PICK
                    stack[0] = stack[level]; level = -1
                if keydown(33):  # (: ROLL down
                    stack.insert(int(level), stack.pop(0)); level = -1
                if keydown(5): level = -1  # BACK: exit selection mode
            draw_register(0)

    # Unary operators
    elif keydown(18):
        if not entry and not stack: push(exp(1)); draw_register(0, 0.2)
        else: evaluate1(lambda x: exp(x))
    elif keydown(19): evaluate1(lambda x: log(x))
    elif keydown(20): evaluate1(lambda x: log10(x))
    elif keydown(24):
        if degrees: evaluate1(lambda x: sin(x * pi / 180))
        else: evaluate1(lambda x: sin(x))
    elif keydown(25):
        if degrees: evaluate1(lambda x: cos(x * pi / 180))
        else: evaluate1(lambda x: cos(x))
    elif keydown(26):
        if degrees: evaluate1(lambda x: tan(x * pi / 180))
        else: evaluate1(lambda x: tan(x))
    elif keydown(28): evaluate1(lambda x: sqrt(x))
    elif keydown(29): evaluate1(lambda x: x*x)

    # Binary operators
    elif keydown(23): evaluate2(lambda x, y: x ** y)
    elif keydown(39): evaluate2(lambda x, y: x * y)
    elif keydown(40): evaluate2(lambda x, y: x / y)
    elif keydown(45): evaluate2(lambda x, y: x + y)
    elif keydown(46):
        if entry and entry[-1] == "e" and entry.count("-") == 0: entry += "-"
        else: evaluate2(lambda x, y: x - y)

    # SHIFT operators
    elif keydown(12):  # SHIFT
        pressed = False; draw_string("shift", 270, 0, (255,254,255), (255,181,0)); sleep(0.2)
        while not pressed:
            if keydown(17):  # BACKSPACE: CLEAR
                stack = [0, 0, 0, 0] if fixed else []
                entry = ""; pressed = True; display()
            if keydown(24):
                if degrees: evaluate1(lambda x: asin(x) * 180 / pi)
                else: evaluate1(lambda x: asin(x))
                pressed = True
            if keydown(25):
                if degrees: evaluate1(lambda x: acos(x) * 180 / pi)
                else: evaluate1(lambda x: acos(x))
                pressed = True
            if keydown(26):
                if degrees: evaluate1(lambda x: atan(x) * 180 / pi)
                else: evaluate1(lambda x: atan(x))
                pressed = True
            if keydown(33):  # (: ROLL up
                if entry:
                    try: pos = float(entry)
                    except Exception as message: draw_error(message)
                    else:
                        entry = ""
                        if pos == int(pos) and int(pos) <= len(stack):
                            stack.insert(0, stack.pop(int(pos)-1))
                        else: draw_error("invalid stack level number")
                    display()
                elif len(stack) >= 2: stack.insert(0, stack.pop())
                pressed = True
            if keydown(40): evaluate1(lambda x: 1/x); pressed = True  # DIVISION
            if keydown(46): evaluate1(lambda x: -x); pressed = True  # MINUS
            if keydown(51):  # Ans: OVER
                if fixed or len(stack) >= 2: push(stack[1])
                pressed = True
            if keydown(12): pressed = True  # SHIFT
        display(False)

    # ALPHA operators
    elif keydown(13):  # ALPHA
        pressed = False; draw_string("alpha", 270, 0, (255,254,255), (255,181,0)); sleep(0.2)
        while not pressed:
            if keydown(6):  # HOME
                pressed = True; display(False); draw_error(__version__)
            if keydown(17):  # %: Percentage functions
                pressed = True; display(False); percentage()
            if keydown(20):  # C: Fahrenheit to Celsius
                evaluate1(lambda x: (x-32) * 5/9); pressed = True
            if keydown(21):  # D: Set angles to degrees
                degrees = True; pressed = True; display(False)
            if keydown(23):  # F: Celsius to Fahrenheit
                evaluate1(lambda x: x * 9/5 + 32); pressed = True
            if keydown(25):  # H
                evaluate1(lambda x: hms(x)); pressed = True
            if keydown(33):  # P: Prime factorisation
                if not entry and stack:
                    try: push(prime_facto(float(stack[0])), False)
                    except Exception as message: draw_error(message)
                    display(False)
                elif entry:
                    if float(entry) != int(float(entry)): draw_error("math domain error")
                    else: push(float(entry)); push(prime_facto(float(entry)), False); entry = ""
                    display()
                pressed = True
            if keydown(36):  # R: Set angles to radians
                degrees = False; pressed = True; display(False)
            if keydown(48):  # ?
                if not entry: push(random())
                pressed = True; display(False)
            if keydown(49): evaluate1(lambda x: factorial(x)); pressed = True
            if keydown(13): pressed = True; display(False)  # ALPHA

    elif keydown(16): toolbox()
    elif keydown(15): varbox()
    elif keydown(6): quit()  # HOME

    blink_cursor()

During your visit to our site, NumWorks needs to install "cookies" or use other technologies to collect data about you in order to:

With the exception of Cookies essential to the operation of the site, NumWorks leaves you the choice: you can accept Cookies for audience measurement by clicking on the "Accept and continue" button, or refuse these Cookies by clicking on the "Continue without accepting" button or by continuing your browsing. You can update your choice at any time by clicking on the link "Manage my cookies" at the bottom of the page. For more information, please consult our cookies policy.