{
  "id": "P007",
  "slug": "calculator",
  "key": "P007-calculator",
  "title": "Calculator",
  "summary": "Type an expression with + - * / and parentheses; it is turned into postfix and worked out on a stack in exact fractions, so 0.1 + 0.2 is 0.3. Clear error messages, a history and ans for the last answer.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P007 - Calculator\n\nType an expression with `+ - * /`, parentheses and a minus in front, and it\nis worked out exactly: every number is a fraction, so `0.1 + 0.2` is `0.3`\nand `1 / 3 * 3` is `1`. `ans` stands for the last answer, `history` lists what\nwas worked out, `clear` forgets it, `quit` ends. A prompt rather than a menu,\nwhich is what a calculator is; the history lives while the program runs.\n\n- `main.eml` - the prompt, the commands and the history\n- `expr.eml` - three steps from text to answer: cut the text into numbers,\n  operators and parentheses; put them in postfix order with the\n  shunting-yard method, checking the shape of the expression on the way\n  (minus in front binds tightest, then `* /`, then `+ -`, all left to\n  right); work the postfix out on a stack\n- `frac.eml` - exact fractions kept in lowest terms, and how an answer is\n  shown: an integer as it is, a fraction whose decimal ends as that decimal\n  (5/2 is 2.5), any other fraction as `n/d, about` its value to ten places,\n  rounded half up\n\nIntegers have no size limit (`123456789 * 987654321` is exact), but EML has\nno `//`, and `a / b` goes through a float that cannot hold a large quotient,\nso whole-number division is written out as long division by doubling.\n\nWhat is reported instead of an answer: a number that is not one (`1..2`,\n`.5`), a character that is not part of an expression, a missing number or\noperator (`3 4`, `*3`, `()`), an unclosed or unopened parenthesis, an\nexpression that stops early (`2 +`), division by zero, and `ans` before any\nanswer.\n\nSessions: `sessions/basic.in` works out integers, decimals that floats get\nwrong, fractions that do not end, `ans`, a minus in front, precedence, a big\nproduct and its exact quotient, and shows and clears the history;\n`sessions/bad-input.in` types every kind of error above, then a good\nexpression.\n\nBuilt on the verified corpus cases `rpn-evaluator` (working out an\nexpression in postfix order on a stack) and `simple-stack` (a list used as a\nstack: push onto the end, pop from the end).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P007 calculator: type an expression and get its value - exactly, because\n# every number is a fraction. ans is the last answer; history lists what was\n# worked out, and clear forgets it. The history lives while the program runs.\nimport expr\nimport frac\n\ndef trim(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\n\"Calculator: + - * / and ( ), in exact fractions; ans is the last answer.\" ^0\n\"Commands: history, clear, quit.\" ^0\n[] => history\nNone => ans\nTrue => running\nwhile running:\n    trim(input(\"calc> \")) => line\n    if line == \"quit\":\n        False => running\n    elif line == \"history\":\n        if len(history) == 0:\n            \"  (nothing yet)\" ^0\n        0 => i\n        for h in history:\n            i + 1 => i\n            (\"  \" + str(i) + \") \" + h[0] + \" = \" + frac.shown(h[1])) ^0\n    elif line == \"clear\":\n        [] => history\n        None => ans\n        \"History and ans cleared.\" ^0\n    elif line == \"\":\n        \"Type an expression, or quit.\" ^0\n    else:\n        expr.run(line, ans) => r\n        if r[0]:\n            (\"= \" + frac.shown(r[1])) ^0\n            history + [[line, r[1]]] => history\n            r[1] => ans\n        else:\n            r[1] ^0\n\"Bye.\" ^0\n",
      "python": "import expr\nimport frac\n\ndef trim(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\nprint(\"Calculator: + - * / and ( ), in exact fractions; ans is the last answer.\")\nprint(\"Commands: history, clear, quit.\")\nhistory = []\nans = None\nrunning = True\nwhile running:\n    line = trim(input(\"calc> \"))\n    if line == \"quit\":\n        running = False\n    elif line == \"history\":\n        if len(history) == 0:\n            print(\"  (nothing yet)\")\n        i = 0\n        for h in history:\n            i = i + 1\n            print(\"  \" + str(i) + \") \" + h[0] + \" = \" + frac.shown(h[1]))\n    elif line == \"clear\":\n        history = []\n        ans = None\n        print(\"History and ans cleared.\")\n    elif line == \"\":\n        print(\"Type an expression, or quit.\")\n    else:\n        r = expr.run(line, ans)\n        if r[0]:\n            print(\"= \" + frac.shown(r[1]))\n            history = history + [[line, r[1]]]\n            ans = r[1]\n        else:\n            print(r[1])\nprint(\"Bye.\")\n"
    },
    {
      "name": "expr.eml",
      "eml": "# P007 calculator - from typed text to an answer, in three steps. tokens()\n# cuts the text into numbers, operators and parentheses; postfix() puts them\n# in postfix order with the shunting-yard method, checking the shape of the\n# expression as it goes; evaluate() works the postfix out on a stack. Each\n# step returns [True, result] or [False, what is wrong].\nimport frac\n\ndef tokens(s):\n    # [kind, value, text]: kind is \"num\" (value a fraction), \"op\" (+ - * /),\n    # \"(\", \")\" or \"ans\".\n    [] => out\n    0 => i\n    len(s) => n\n    while i < n:\n        s[i] => c\n        if c == \" \":\n            i + 1 => i\n        elif c in \"0123456789.\":\n            i => start\n            0 => points\n            while i < n and s[i] in \"0123456789.\":\n                if s[i] == \".\":\n                    points + 1 => points\n                i + 1 => i\n            s[start:i] => text\n            if points > 1 or text[0] == \".\" or text[len(text) - 1] == \".\":\n                return [False, \"Not a number: \" + text]\n            out + [[\"num\", frac.decimal(text), text]] => out\n        elif c in \"+-*/\":\n            out + [[\"op\", c, c]] => out\n            i + 1 => i\n        elif c == \"(\" or c == \")\":\n            out + [[c, c, c]] => out\n            i + 1 => i\n        elif s[i:i + 3] == \"ans\":\n            out + [[\"ans\", \"ans\", \"ans\"]] => out\n            i + 3 => i\n        else:\n            return [False, \"Unexpected character: \" + c]\n    return [True, out]\n\ndef rank(t):\n    # How tightly an operator binds: minus-in-front (\"neg\") above * and /,\n    # above + and -.\n    if t[0] == \"neg\":\n        return 3\n    if t[1] == \"*\" or t[1] == \"/\":\n        return 2\n    return 1\n\ndef postfix(toks):\n    [] => out\n    [] => ops\n    True => want_number\n    for t in toks:\n        if want_number:\n            if t[0] == \"num\" or t[0] == \"ans\":\n                out + [t] => out\n                False => want_number\n            elif t[0] == \"(\":\n                ops + [t] => ops\n            elif t[0] == \"op\" and t[1] == \"-\":\n                ops + [[\"neg\", \"-\", \"-\"]] => ops\n            else:\n                return [False, \"A number is missing before \" + t[2] + \".\"]\n        elif t[0] == \"op\":\n            # Left to right: first finish every operator on the stack that\n            # binds at least as tightly.\n            while len(ops) > 0 and ops[len(ops) - 1][0] != \"(\" and rank(ops[len(ops) - 1]) >= rank(t):\n                out + [ops[len(ops) - 1]] => out\n                ops[0:len(ops) - 1] => ops\n            ops + [t] => ops\n            True => want_number\n        elif t[0] == \")\":\n            while len(ops) > 0 and ops[len(ops) - 1][0] != \"(\":\n                out + [ops[len(ops) - 1]] => out\n                ops[0:len(ops) - 1] => ops\n            if len(ops) == 0:\n                return [False, \"There is no ( for this ).\"]\n            ops[0:len(ops) - 1] => ops\n        else:\n            return [False, \"An operator is missing before \" + t[2] + \".\"]\n    if want_number:\n        return [False, \"The expression is incomplete.\"]\n    while len(ops) > 0:\n        if ops[len(ops) - 1][0] == \"(\":\n            return [False, \"A ( is not closed.\"]\n        out + [ops[len(ops) - 1]] => out\n        ops[0:len(ops) - 1] => ops\n    return [True, out]\n\ndef evaluate(post, ans):\n    # Work the postfix out on a stack; ans is the last answer or None.\n    [] => stack\n    for t in post:\n        if t[0] == \"num\":\n            stack + [t[1]] => stack\n        elif t[0] == \"ans\":\n            if ans == None:\n                return [False, \"There is no answer yet for ans.\"]\n            stack + [ans] => stack\n        elif t[0] == \"neg\":\n            stack[0:len(stack) - 1] + [frac.negated(stack[len(stack) - 1])] => stack\n        else:\n            stack[len(stack) - 2] => x\n            stack[len(stack) - 1] => y\n            stack[0:len(stack) - 2] => stack\n            if t[1] == \"+\":\n                frac.plus(x, y) => z\n            elif t[1] == \"-\":\n                frac.minus(x, y) => z\n            elif t[1] == \"*\":\n                frac.times(x, y) => z\n            else:\n                if y[0] == 0:\n                    return [False, \"Division by zero.\"]\n                frac.over(x, y) => z\n            stack + [z] => stack\n    return [True, stack[0]]\n\ndef run(text, ans):\n    tokens(text) => r\n    if r[0]:\n        postfix(r[1]) => r\n    if r[0]:\n        evaluate(r[1], ans) => r\n    return r\n",
      "python": "import frac\n\ndef tokens(s):\n    out = []\n    i = 0\n    n = len(s)\n    while i < n:\n        c = s[i]\n        if c == \" \":\n            i = i + 1\n        elif c in \"0123456789.\":\n            start = i\n            points = 0\n            while i < n and s[i] in \"0123456789.\":\n                if s[i] == \".\":\n                    points = points + 1\n                i = i + 1\n            text = s[start:i]\n            if points > 1 or text[0] == \".\" or text[len(text) - 1] == \".\":\n                return [False, \"Not a number: \" + text]\n            out = out + [[\"num\", frac.decimal(text), text]]\n        elif c in \"+-*/\":\n            out = out + [[\"op\", c, c]]\n            i = i + 1\n        elif c == \"(\" or c == \")\":\n            out = out + [[c, c, c]]\n            i = i + 1\n        elif s[i:i + 3] == \"ans\":\n            out = out + [[\"ans\", \"ans\", \"ans\"]]\n            i = i + 3\n        else:\n            return [False, \"Unexpected character: \" + c]\n    return [True, out]\n\ndef rank(t):\n    if t[0] == \"neg\":\n        return 3\n    if t[1] == \"*\" or t[1] == \"/\":\n        return 2\n    return 1\n\ndef postfix(toks):\n    out = []\n    ops = []\n    want_number = True\n    for t in toks:\n        if want_number:\n            if t[0] == \"num\" or t[0] == \"ans\":\n                out = out + [t]\n                want_number = False\n            elif t[0] == \"(\":\n                ops = ops + [t]\n            elif t[0] == \"op\" and t[1] == \"-\":\n                ops = ops + [[\"neg\", \"-\", \"-\"]]\n            else:\n                return [False, \"A number is missing before \" + t[2] + \".\"]\n        elif t[0] == \"op\":\n            while len(ops) > 0 and ops[len(ops) - 1][0] != \"(\" and rank(ops[len(ops) - 1]) >= rank(t):\n                out = out + [ops[len(ops) - 1]]\n                ops = ops[0:len(ops) - 1]\n            ops = ops + [t]\n            want_number = True\n        elif t[0] == \")\":\n            while len(ops) > 0 and ops[len(ops) - 1][0] != \"(\":\n                out = out + [ops[len(ops) - 1]]\n                ops = ops[0:len(ops) - 1]\n            if len(ops) == 0:\n                return [False, \"There is no ( for this ).\"]\n            ops = ops[0:len(ops) - 1]\n        else:\n            return [False, \"An operator is missing before \" + t[2] + \".\"]\n    if want_number:\n        return [False, \"The expression is incomplete.\"]\n    while len(ops) > 0:\n        if ops[len(ops) - 1][0] == \"(\":\n            return [False, \"A ( is not closed.\"]\n        out = out + [ops[len(ops) - 1]]\n        ops = ops[0:len(ops) - 1]\n    return [True, out]\n\ndef evaluate(post, ans):\n    stack = []\n    for t in post:\n        if t[0] == \"num\":\n            stack = stack + [t[1]]\n        elif t[0] == \"ans\":\n            if ans == None:\n                return [False, \"There is no answer yet for ans.\"]\n            stack = stack + [ans]\n        elif t[0] == \"neg\":\n            stack = stack[0:len(stack) - 1] + [frac.negated(stack[len(stack) - 1])]\n        else:\n            x = stack[len(stack) - 2]\n            y = stack[len(stack) - 1]\n            stack = stack[0:len(stack) - 2]\n            if t[1] == \"+\":\n                z = frac.plus(x, y)\n            elif t[1] == \"-\":\n                z = frac.minus(x, y)\n            elif t[1] == \"*\":\n                z = frac.times(x, y)\n            else:\n                if y[0] == 0:\n                    return [False, \"Division by zero.\"]\n                z = frac.over(x, y)\n            stack = stack + [z]\n    return [True, stack[0]]\n\ndef run(text, ans):\n    r = tokens(text)\n    if r[0]:\n        r = postfix(r[1])\n    if r[0]:\n        r = evaluate(r[1], ans)\n    return r\n"
    },
    {
      "name": "frac.eml",
      "eml": "# P007 calculator - exact fractions. A number is a list [n, d]: n / d in\n# lowest terms with d > 0, so 0.1 is [1, 10] and 0.1 + 0.2 is exactly [3, 10].\n# Integers have no size limit, but EML has no //, and a / b goes through a\n# float that cannot hold a large quotient exactly, so whole-number division\n# is written out below.\n\ndef quotient(a, b):\n    # a // b for whole numbers a >= 0 and b > 0, exact at any size. Long\n    # division by doubling: take away the largest b * 2^k that still fits.\n    0 => q\n    while a >= b:\n        b => m\n        1 => k\n        while m + m <= a:\n            m + m => m\n            k + k => k\n        a - m => a\n        q + k => q\n    return q\n\ndef gcd(a, b):\n    # Greatest common divisor of whole numbers a, b >= 0 (Euclid).\n    while b != 0:\n        a % b => r\n        b => a\n        r => b\n    return a\n\ndef make(n, d):\n    # n / d in lowest terms with a positive denominator (d != 0).\n    if d < 0:\n        0 - n => n\n        0 - d => d\n    abs(n) => a\n    gcd(a, d) => g\n    if n < 0:\n        return [0 - quotient(a, g), quotient(d, g)]\n    return [quotient(a, g), quotient(d, g)]\n\ndef plus(x, y):\n    return make(x[0] * y[1] + y[0] * x[1], x[1] * y[1])\n\ndef minus(x, y):\n    return make(x[0] * y[1] - y[0] * x[1], x[1] * y[1])\n\ndef times(x, y):\n    return make(x[0] * y[0], x[1] * y[1])\n\ndef over(x, y):\n    # x / y; the caller has checked that y is not zero.\n    return make(x[0] * y[1], x[1] * y[0])\n\ndef negated(x):\n    return [0 - x[0], x[1]]\n\ndef decimal(text):\n    # A number typed as digits with at most one point (\"12\", \"0.25\") as a\n    # fraction, exactly: 12.345 is 12345 / 1000.\n    \"\" => digits\n    1 => scale\n    False => point\n    for c in text:\n        if c == \".\":\n            True => point\n        else:\n            digits + c => digits\n            if point:\n                scale * 10 => scale\n    return make(int(digits), scale)\n\ndef with_point(m, k):\n    # The whole number m >= 0 written with k digits after a point.\n    str(m) => s\n    while len(s) <= k:\n        \"0\" + s => s\n    if k == 0:\n        return s\n    return s[0:len(s) - k] + \".\" + s[len(s) - k:len(s)]\n\ndef shown(x):\n    # An integer as it is; a fraction whose decimal ends, as that decimal\n    # (5/2 is 2.5); any other fraction as n/d with its value to ten places,\n    # rounded half up (\"1/3, about 0.3333333333\").\n    \"\" => sign\n    if x[0] < 0:\n        \"-\" => sign\n        negated(x) => x\n    if x[1] == 1:\n        return sign + str(x[0])\n    # The decimal ends if the denominator divides a power of ten.\n    1 => p\n    0 => k\n    while p % x[1] != 0 and k < 60:\n        p * 10 => p\n        k + 1 => k\n    if p % x[1] == 0:\n        return sign + with_point(x[0] * quotient(p, x[1]), k)\n    10000000000 => ten10\n    quotient(2 * x[0] * ten10 + x[1], 2 * x[1]) => t\n    return sign + str(x[0]) + \"/\" + str(x[1]) + \", about \" + sign + with_point(t, 10)\n",
      "python": "def quotient(a, b):\n    q = 0\n    while a >= b:\n        m = b\n        k = 1\n        while m + m <= a:\n            m = m + m\n            k = k + k\n        a = a - m\n        q = q + k\n    return q\n\ndef gcd(a, b):\n    while b != 0:\n        r = a % b\n        a = b\n        b = r\n    return a\n\ndef make(n, d):\n    if d < 0:\n        n = 0 - n\n        d = 0 - d\n    a = abs(n)\n    g = gcd(a, d)\n    if n < 0:\n        return [0 - quotient(a, g), quotient(d, g)]\n    return [quotient(a, g), quotient(d, g)]\n\ndef plus(x, y):\n    return make(x[0] * y[1] + y[0] * x[1], x[1] * y[1])\n\ndef minus(x, y):\n    return make(x[0] * y[1] - y[0] * x[1], x[1] * y[1])\n\ndef times(x, y):\n    return make(x[0] * y[0], x[1] * y[1])\n\ndef over(x, y):\n    return make(x[0] * y[1], x[1] * y[0])\n\ndef negated(x):\n    return [0 - x[0], x[1]]\n\ndef decimal(text):\n    digits = \"\"\n    scale = 1\n    point = False\n    for c in text:\n        if c == \".\":\n            point = True\n        else:\n            digits = digits + c\n            if point:\n                scale = scale * 10\n    return make(int(digits), scale)\n\ndef with_point(m, k):\n    s = str(m)\n    while len(s) <= k:\n        s = \"0\" + s\n    if k == 0:\n        return s\n    return s[0:len(s) - k] + \".\" + s[len(s) - k:len(s)]\n\ndef shown(x):\n    sign = \"\"\n    if x[0] < 0:\n        sign = \"-\"\n        x = negated(x)\n    if x[1] == 1:\n        return sign + str(x[0])\n    p = 1\n    k = 0\n    while p % x[1] != 0 and k < 60:\n        p = p * 10\n        k = k + 1\n    if p % x[1] == 0:\n        return sign + with_point(x[0] * quotient(p, x[1]), k)\n    ten10 = 10000000000\n    t = quotient(2 * x[0] * ten10 + x[1], 2 * x[1])\n    return sign + str(x[0]) + \"/\" + str(x[1]) + \", about \" + sign + with_point(t, 10)\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "ans + 1\n\n2 +\n(1 + 2\n1 + 2)\n3 4\n4 / (2 - 2)\n2 * x\n1..2\n.5 + 1\n*3\n7 / 0\n()\n2 * (3 + 4)\nquit\n",
      "screen": "Calculator: + - * / and ( ), in exact fractions; ans is the last answer.\nCommands: history, clear, quit.\ncalc> ans + 1\nThere is no answer yet for ans.\ncalc> \nType an expression, or quit.\ncalc> 2 +\nThe expression is incomplete.\ncalc> (1 + 2\nA ( is not closed.\ncalc> 1 + 2)\nThere is no ( for this ).\ncalc> 3 4\nAn operator is missing before 4.\ncalc> 4 / (2 - 2)\nDivision by zero.\ncalc> 2 * x\nUnexpected character: x\ncalc> 1..2\nNot a number: 1..2\ncalc> .5 + 1\nNot a number: .5\ncalc> *3\nA number is missing before *.\ncalc> 7 / 0\nDivision by zero.\ncalc> ()\nA number is missing before ).\ncalc> 2 * (3 + 4)\n= 14\ncalc> quit\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "2 * (3 + 4)\n0.1 + 0.2\n10 / 4\n1 / 3\nans * 3\n2 / 3\n-(2 + 3) * 4\n2 * -3\n1 - 2 / 3\n123456789 * 987654321\nans / 987654321\n7 - 2 - 1\nhistory\nclear\nhistory\nquit\n",
      "screen": "Calculator: + - * / and ( ), in exact fractions; ans is the last answer.\nCommands: history, clear, quit.\ncalc> 2 * (3 + 4)\n= 14\ncalc> 0.1 + 0.2\n= 0.3\ncalc> 10 / 4\n= 2.5\ncalc> 1 / 3\n= 1/3, about 0.3333333333\ncalc> ans * 3\n= 1\ncalc> 2 / 3\n= 2/3, about 0.6666666667\ncalc> -(2 + 3) * 4\n= -20\ncalc> 2 * -3\n= -6\ncalc> 1 - 2 / 3\n= 1/3, about 0.3333333333\ncalc> 123456789 * 987654321\n= 121932631112635269\ncalc> ans / 987654321\n= 123456789\ncalc> 7 - 2 - 1\n= 4\ncalc> history\n  1) 2 * (3 + 4) = 14\n  2) 0.1 + 0.2 = 0.3\n  3) 10 / 4 = 2.5\n  4) 1 / 3 = 1/3, about 0.3333333333\n  5) ans * 3 = 1\n  6) 2 / 3 = 2/3, about 0.6666666667\n  7) -(2 + 3) * 4 = -20\n  8) 2 * -3 = -6\n  9) 1 - 2 / 3 = 1/3, about 0.3333333333\n  10) 123456789 * 987654321 = 121932631112635269\n  11) ans / 987654321 = 123456789\n  12) 7 - 2 - 1 = 4\ncalc> clear\nHistory and ans cleared.\ncalc> history\n  (nothing yet)\ncalc> quit\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "rpn-evaluator",
      "caseId": "093-rpn-evaluator",
      "title": "Reverse Polish Notation evaluator"
    },
    {
      "slug": "simple-stack",
      "caseId": "042-simple-stack",
      "title": "Case corpus: a self-authored LIFO stack"
    }
  ],
  "updated": "2026-09-29"
}
