{
  "id": "P114",
  "slug": "quadratic-solver",
  "key": "P114-quadratic-solver",
  "title": "Quadratic solver",
  "summary": "ax^2 + bx + c = 0 with exact rational, irrational and complex roots, square roots and divisions worked digit by digit on whole numbers; and the small root of x^2 + bx + 1 in floats, by the textbook formula and a stable one, against its exact digits.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P114 - Quadratic solver\n\nax² + bx + c = 0, solved exactly: rational, irrational and complex roots.\nThe program also solves x² + bx + 1 in floats, by the textbook formula and\nby a stable one, against the root's exact digits.\n\n- `main.eml` - the menu, the equation and its roots, and the float\n  comparison\n- `quad.eml` - reading coefficients, square roots and divisions digit by\n  digit, the solver, a float square root, and the small root two ways and\n  exactly\n\nHow each part works:\n\n- Coefficients have at most three decimals. They are scaled by 1000 to\n  whole numbers, which leaves the roots as they are, so the discriminant\n  b² - 4ac is exact.\n- The kind of root, as in the corpus case `quadratic-solver`, here with\n  complex roots too:\n  - a discriminant of 0 gives one double root;\n  - the square of a fraction gives two rational roots;\n  - any other positive one gives two irrational roots;\n  - a negative one gives complex roots.\n\n  A leading coefficient below zero is turned positive by changing every\n  sign, which leaves the roots as they are.\n- Square roots use the digit-pair method and divisions use long division,\n  both on whole numbers. Only adding, subtracting, multiplying and\n  comparing are used, so the 12 decimal places are exact: 18 are worked\n  out, and then rounded half away from zero.\n- Floats, as in the corpus case `catastrophic-cancellation`: for x² + bx +\n  1 with b large, the textbook formula takes -b + √(b² - 4) when the two\n  are nearly equal, and the digits they share cancel. The stable formula\n  works out the big root, where nothing cancels, and divides: the two\n  roots multiply to 1.\n  The float square root is Newton's method from above, so it is the same\n  in every Python. The program measures each printed float's relative\n  error against 20 exact digits.\n\nWhat is checked: menu choices 1 to 3; coefficients with at most 9 digits\nbefore the point and 3 after; b from 3 to 10¹² for the float comparison.\nAn empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- x² - 3x + 2 and 2x² - 7x + 3, with rational roots;\n- x² - 2, with irrational roots, and x² + 2x + 5, with complex roots;\n- x² - 2x + 1, a double root;\n- 2x - 4, 0 = 0 and 5 = 0, where a, or everything, is 0;\n- -x² + 2, and 0.5x² - 0.125x + 0.007;\n- the float comparison for b = 3, 10⁸ (the textbook root off by 49%) and\n  10¹² (off by 100%: it comes out as 0).\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- coefficients: a cancel, abc, four decimals, two later cancels, a comma\n  and ten digits;\n- the float comparison with a cancel, 2, 10¹² + 1 and x.\n\nBuilt on the verified corpus cases `quadratic-solver` (the quadratic\nformula and its discriminant) and `catastrophic-cancellation` (the same\nalgebra, and one arrangement keeps its digits).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P114 quadratic solver: exact roots of ax^2 + bx + c = 0, real or complex,\n# and the small root of x^2 + bx + 1 in floats, by the textbook formula and\n# the stable one, against its exact digits.\nimport quad\n\ndef plain(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\ndef number(s, most):\n    if len(s) == 0 or len(s) > most:\n        return -1\n    0 => n\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n * 10 + int(c) => n\n    return n\n\ndef ask_coef(name):\n    # [coefficient x 1000, text], [] to cancel, [\"!\"] after a complaint\n    plain(input(name + \"> \")) => t\n    if t == \"\":\n        return []\n    quad.parse_coef(t) => c\n    if len(c) == 0:\n        \"Type a number with at most 9 digits before the point and 3 after, such as -2 or 0.125.\" ^0\n        return [\"!\"]\n    return [c[0], quad.trimmed(quad.scaled_text(c[0], 3))]\n\ndef equation(a, b, c):\n    \"\" => out\n    for part in [[a, \"x^2\"], [b, \"x\"], [c, \"\"]]:\n        part[0] => v\n        if v[0] != 0:\n            v[1] => t\n            \"+\" => sign\n            if t[0] == \"-\":\n                \"-\" => sign\n                t[1:len(t)] => t\n            if (t == \"1\") and part[1] != \"\":\n                \"\" => t\n            if out == \"\":\n                if sign == \"-\":\n                    \"-\" => out\n                out + t + part[1] => out\n            else:\n                out + \" \" + sign + \" \" + t + part[1] => out\n    if out == \"\":\n        \"0\" => out\n    return out + \" = 0\"\n\ndef solve_menu():\n    ask_coef(\"a\") => a\n    if len(a) == 0:\n        \"Cancelled.\" ^0\n        return\n    if a[0] == \"!\":\n        return\n    ask_coef(\"b\") => b\n    if len(b) == 0:\n        \"Cancelled.\" ^0\n        return\n    if b[0] == \"!\":\n        return\n    ask_coef(\"c\") => c\n    if len(c) == 0:\n        \"Cancelled.\" ^0\n        return\n    if c[0] == \"!\":\n        return\n    equation(a, b, c) ^0\n    quad.solve(a[0], b[0], c[0]) => r\n    if r[0] == \"all\":\n        \"Every x solves it: all three coefficients are zero.\" ^0\n        return\n    if r[0] == \"none\":\n        \"No x solves it: it says a number other than zero equals zero.\" ^0\n        return\n    if r[0] == \"linear\":\n        (\"With a = 0 it is not quadratic: one root, x = \" + r[1] + \".\") ^0\n        return\n    b[0] * b[0] - 4 * a[0] * c[0] => D\n    (\"Discriminant b^2 - 4ac = \" + quad.trimmed(quad.scaled_text(D, 6)) + \".\") ^0\n    if r[0] == \"double\":\n        (\"It is zero: one double root, x = \" + r[1] + \".\") ^0\n    elif r[0] == \"two rational\":\n        (\"It is the square of a fraction: two rational roots, x = \" + r[1] + \" and x = \" + r[2] + \".\") ^0\n    elif r[0] == \"two real\":\n        \"It is positive and not the square of a fraction: two irrational roots, x = (-b - sqrt(D)) / 2a and (-b + sqrt(D)) / 2a:\" ^0\n        (\"  x = \" + r[2]) ^0\n        (\"  x = \" + r[3]) ^0\n        \"to 12 places, each digit worked out exactly.\" ^0\n    else:\n        \"It is negative: two complex roots, x = (-b / 2a) - i (sqrt(-D) / 2a) and its conjugate:\" ^0\n        (\"  x = \" + r[1] + \" - \" + r[2] + \"i\") ^0\n        (\"  x = \" + r[1] + \" + \" + r[2] + \"i\") ^0\n        \"the imaginary part to 12 places.\" ^0\n\ndef printed_value(x):\n    # [M, e] with the printed float equal to M x 10^e exactly, M >= 0\n    str(x) => t\n    if t[0] == \"-\":\n        t[1:len(t)] => t\n    0 => e\n    -1 => at\n    for i in [0:len(t) - 1]:\n        if at == -1 and t[i] == \"e\":\n            i => at\n    if at >= 0:\n        t[at + 1:len(t)] => es\n        1 => sign\n        if es[0] == \"-\":\n            0 - 1 => sign\n            es[1:len(es)] => es\n        elif es[0] == \"+\":\n            es[1:len(es)] => es\n        sign * int(es) => e\n        t[0:at] => t\n    \"\" => ds\n    0 => after\n    False => dot\n    for c in t:\n        if c == \".\":\n            True => dot\n        else:\n            ds + c => ds\n            if dot:\n                after + 1 => after\n    return [int(ds), e - after]\n\ndef ten_to(k):\n    \"1\" => t\n    for i in [1:k]:\n        t + \"0\" => t\n    return int(t)\n\ndef quality(x, ex):\n    # how close the printed float x is to the exact root, -digits x\n    # 10^(lead - 19): [k, -1] with k the largest up to 17 whose relative\n    # error is at most 10^-k, or [0, whole percent] when it is over 10%\n    printed_value(x) => p\n    int(ex[0]) => m\n    ex[1] - 19 => e\n    p[1] => lo\n    if e < lo:\n        e => lo\n    p[0] * ten_to(p[1] - lo) => xv\n    if x < 0.0:\n        0 - xv => xv\n    0 - m * ten_to(e - lo) => ev\n    xv - ev => diff\n    if diff < 0:\n        0 - diff => diff\n    0 - ev => size\n    0 => k\n    while k < 17 and diff * ten_to(k + 1) <= size:\n        k + 1 => k\n    if k >= 1:\n        return [k, 0 - 1]\n    return [0, quad.quotient_digits(diff * 100, size, 0)]\n\ndef verdict(q):\n    if q[1] >= 0:\n        return \"off by \" + str(q[1]) + \"%\"\n    if q[0] >= 17:\n        return \"right to 17 significant digits or more\"\n    return \"right to about \" + str(q[0]) + \" significant digits\"\n\ndef cancellation():\n    plain(input(\"b, a whole number from 3 to 1000000000000> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return\n    number(t, 13) => b\n    if b < 3 or b > 1000000000000:\n        \"Type a whole number from 3 to 1000000000000.\" ^0\n        return\n    quad.small_root(b) => r\n    quad.exact_small(b, 20) => ex\n    (\"x^2 + \" + str(b) + \"x + 1 = 0, its root nearest zero:\") ^0\n    (\"  exact, 20 digits:  -\" + ex[0][0:1] + \".\" + ex[0][1:len(ex[0])] + \"e\" + str(ex[1])) ^0\n    (\"  textbook formula:  \" + str(r[0]) + \"   (\" + verdict(quality(r[0], ex)) + \")\") ^0\n    (\"  stable formula:    \" + str(r[1]) + \"   (\" + verdict(quality(r[1], ex)) + \")\") ^0\n    \"The textbook formula takes -b + sqrt(b^2 - 4) when the two are nearly equal, and the digits they share cancel. The stable one works out the big root, where nothing cancels, and divides: the product of the roots is c / a = 1.\" ^0\n\n\"== Quadratic solver ==\" ^0\n\"ax^2 + bx + c = 0, solved exactly - and the same algebra in floats losing its small root.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) solve  2) floats and cancellation  3) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        solve_menu()\n    elif choice == \"2\":\n        cancellation()\n    elif choice == \"3\":\n        False => running\n    else:\n        \"Pick a number from 1 to 3.\" ^0\n\"Bye.\" ^0\n",
      "python": "import quad\n\ndef plain(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\ndef number(s, most):\n    if len(s) == 0 or len(s) > most:\n        return -1\n    n = 0\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n = n * 10 + int(c)\n    return n\n\ndef ask_coef(name):\n    t = plain(input(name + \"> \"))\n    if t == \"\":\n        return []\n    c = quad.parse_coef(t)\n    if len(c) == 0:\n        print(\"Type a number with at most 9 digits before the point and 3 after, such as -2 or 0.125.\")\n        return [\"!\"]\n    return [c[0], quad.trimmed(quad.scaled_text(c[0], 3))]\n\ndef equation(a, b, c):\n    out = \"\"\n    for part in [[a, \"x^2\"], [b, \"x\"], [c, \"\"]]:\n        v = part[0]\n        if v[0] != 0:\n            t = v[1]\n            sign = \"+\"\n            if t[0] == \"-\":\n                sign = \"-\"\n                t = t[1:len(t)]\n            if t == \"1\" and part[1] != \"\":\n                t = \"\"\n            if out == \"\":\n                if sign == \"-\":\n                    out = \"-\"\n                out = out + t + part[1]\n            else:\n                out = out + \" \" + sign + \" \" + t + part[1]\n    if out == \"\":\n        out = \"0\"\n    return out + \" = 0\"\n\ndef solve_menu():\n    a = ask_coef(\"a\")\n    if len(a) == 0:\n        print(\"Cancelled.\")\n        return\n    if a[0] == \"!\":\n        return\n    b = ask_coef(\"b\")\n    if len(b) == 0:\n        print(\"Cancelled.\")\n        return\n    if b[0] == \"!\":\n        return\n    c = ask_coef(\"c\")\n    if len(c) == 0:\n        print(\"Cancelled.\")\n        return\n    if c[0] == \"!\":\n        return\n    print(equation(a, b, c))\n    r = quad.solve(a[0], b[0], c[0])\n    if r[0] == \"all\":\n        print(\"Every x solves it: all three coefficients are zero.\")\n        return\n    if r[0] == \"none\":\n        print(\"No x solves it: it says a number other than zero equals zero.\")\n        return\n    if r[0] == \"linear\":\n        print(\"With a = 0 it is not quadratic: one root, x = \" + r[1] + \".\")\n        return\n    D = b[0] * b[0] - 4 * a[0] * c[0]\n    print(\"Discriminant b^2 - 4ac = \" + quad.trimmed(quad.scaled_text(D, 6)) + \".\")\n    if r[0] == \"double\":\n        print(\"It is zero: one double root, x = \" + r[1] + \".\")\n    elif r[0] == \"two rational\":\n        print(\"It is the square of a fraction: two rational roots, x = \" + r[1] + \" and x = \" + r[2] + \".\")\n    elif r[0] == \"two real\":\n        print(\"It is positive and not the square of a fraction: two irrational roots, x = (-b - sqrt(D)) / 2a and (-b + sqrt(D)) / 2a:\")\n        print(\"  x = \" + r[2])\n        print(\"  x = \" + r[3])\n        print(\"to 12 places, each digit worked out exactly.\")\n    else:\n        print(\"It is negative: two complex roots, x = (-b / 2a) - i (sqrt(-D) / 2a) and its conjugate:\")\n        print(\"  x = \" + r[1] + \" - \" + r[2] + \"i\")\n        print(\"  x = \" + r[1] + \" + \" + r[2] + \"i\")\n        print(\"the imaginary part to 12 places.\")\n\ndef printed_value(x):\n    t = str(x)\n    if t[0] == \"-\":\n        t = t[1:len(t)]\n    e = 0\n    at = -1\n    for i in range(0, len(t)):\n        if at == -1 and t[i] == \"e\":\n            at = i\n    if at >= 0:\n        es = t[at + 1:len(t)]\n        sign = 1\n        if es[0] == \"-\":\n            sign = 0 - 1\n            es = es[1:len(es)]\n        elif es[0] == \"+\":\n            es = es[1:len(es)]\n        e = sign * int(es)\n        t = t[0:at]\n    ds = \"\"\n    after = 0\n    dot = False\n    for c in t:\n        if c == \".\":\n            dot = True\n        else:\n            ds = ds + c\n            if dot:\n                after = after + 1\n    return [int(ds), e - after]\n\ndef ten_to(k):\n    t = \"1\"\n    for i in range(1, k+1):\n        t = t + \"0\"\n    return int(t)\n\ndef quality(x, ex):\n    p = printed_value(x)\n    m = int(ex[0])\n    e = ex[1] - 19\n    lo = p[1]\n    if e < lo:\n        lo = e\n    xv = p[0] * ten_to(p[1] - lo)\n    if x < 0.0:\n        xv = 0 - xv\n    ev = 0 - m * ten_to(e - lo)\n    diff = xv - ev\n    if diff < 0:\n        diff = 0 - diff\n    size = 0 - ev\n    k = 0\n    while k < 17 and diff * ten_to(k + 1) <= size:\n        k = k + 1\n    if k >= 1:\n        return [k, 0 - 1]\n    return [0, quad.quotient_digits(diff * 100, size, 0)]\n\ndef verdict(q):\n    if q[1] >= 0:\n        return \"off by \" + str(q[1]) + \"%\"\n    if q[0] >= 17:\n        return \"right to 17 significant digits or more\"\n    return \"right to about \" + str(q[0]) + \" significant digits\"\n\ndef cancellation():\n    t = plain(input(\"b, a whole number from 3 to 1000000000000> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return\n    b = number(t, 13)\n    if b < 3 or b > 1000000000000:\n        print(\"Type a whole number from 3 to 1000000000000.\")\n        return\n    r = quad.small_root(b)\n    ex = quad.exact_small(b, 20)\n    print(\"x^2 + \" + str(b) + \"x + 1 = 0, its root nearest zero:\")\n    print(\"  exact, 20 digits:  -\" + ex[0][0:1] + \".\" + ex[0][1:len(ex[0])] + \"e\" + str(ex[1]))\n    print(\"  textbook formula:  \" + str(r[0]) + \"   (\" + verdict(quality(r[0], ex)) + \")\")\n    print(\"  stable formula:    \" + str(r[1]) + \"   (\" + verdict(quality(r[1], ex)) + \")\")\n    print(\"The textbook formula takes -b + sqrt(b^2 - 4) when the two are nearly equal, and the digits they share cancel. The stable one works out the big root, where nothing cancels, and divides: the product of the roots is c / a = 1.\")\n\nprint(\"== Quadratic solver ==\")\nprint(\"ax^2 + bx + c = 0, solved exactly - and the same algebra in floats losing its small root.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) solve  2) floats and cancellation  3) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        solve_menu()\n    elif choice == \"2\":\n        cancellation()\n    elif choice == \"3\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 3.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "quad.eml",
      "eml": "# P114 quadratic solver - ax^2 + bx + c = 0 with coefficients of at most\n# three decimals, scaled by 1000 to whole numbers so the discriminant\n# b^2 - 4ac is exact (the corpus case quadratic-solver, here with complex\n# roots too). Square roots and divisions are done digit by digit on whole\n# numbers - adding, subtracting, multiplying and comparing only - so every\n# printed digit is exact. And as in the corpus case\n# catastrophic-cancellation, the textbook formula in floats loses the small\n# root of x^2 + bx + 1 when b is large, while the stable one keeps it.\n\ndef gcd(a, b):\n    if a < 0:\n        0 - a => a\n    if b < 0:\n        0 - b => b\n    while b != 0:\n        a % b => r\n        b => a\n        r => b\n    return a\n\ndef parse_coef(t):\n    # the coefficient times 1000, or [] (at most 9 digits before the point\n    # and 3 after)\n    if len(t) == 0:\n        return []\n    1 => sign\n    t => s\n    if s[0] == \"-\":\n        0 - 1 => sign\n        s[1:len(s)] => s\n    \"\" => whole\n    \"\" => frac\n    False => dot\n    for c in s:\n        if c == \".\":\n            if dot:\n                return []\n            True => dot\n        elif c >= \"0\" and c <= \"9\":\n            if dot:\n                frac + c => frac\n            else:\n                whole + c => whole\n        else:\n            return []\n    if whole == \"\" or len(frac) > 3 or (dot and frac == \"\") or len(whole) > 9:\n        return []\n    while len(frac) < 3:\n        frac + \"0\" => frac\n    return [sign * (int(whole) * 1000 + int(frac))]\n\ndef root_digits(n, k):\n    # floor(sqrt(n) x 10^k) for n >= 0, by the digit-pair method\n    str(n) => s\n    for i in [1:k]:\n        s + \"00\" => s\n    if len(s) % 2 == 1:\n        \"0\" + s => s\n    0 => p\n    0 => r\n    0 => i\n    while i < len(s):\n        r * 100 + int(s[i:i + 2]) => r\n        0 => d\n        while (20 * p + d + 1) * (d + 1) <= r:\n            d + 1 => d\n        r - (20 * p + d) * d => r\n        p * 10 + d => p\n        i + 2 => i\n    return p\n\ndef is_square(n):\n    root_digits(n, 0) => s\n    return s * s == n\n\ndef quotient_digits(num, den, k):\n    # floor(num / den x 10^k) for num >= 0, den > 0, by long division\n    str(num) => s\n    for i in [1:k]:\n        s + \"0\" => s\n    0 => q\n    0 => r\n    for c in s:\n        r * 10 + int(c) => r\n        0 => d\n        while r >= den:\n            r - den => r\n            d + 1 => d\n        q * 10 + d => q\n    return q\n\ndef scaled_text(n, k):\n    # n / 10^k as a decimal with k places\n    \"\" => sign\n    if n < 0:\n        \"-\" => sign\n        0 - n => n\n    str(n) => s\n    while len(s) <= k:\n        \"0\" + s => s\n    if k == 0:\n        return sign + s\n    return sign + s[0:len(s) - k] + \".\" + s[len(s) - k:len(s)]\n\ndef trimmed(t):\n    # drop trailing zeros after the point, and the point if nothing follows\n    t => s\n    False => dot\n    for c in s:\n        if c == \".\":\n            True => dot\n    if not dot:\n        return s\n    while s[len(s) - 1] == \"0\":\n        s[0:len(s) - 1] => s\n    if s[len(s) - 1] == \".\":\n        s[0:len(s) - 1] => s\n    return s\n\ndef abs_(n):\n    if n < 0:\n        return 0 - n\n    return n\n\ndef fraction_text(num, den):\n    # num / den reduced, the sign in front; den is not 0\n    if den < 0:\n        0 - num => num\n        0 - den => den\n    gcd(num, den) => g\n    if g == 0:\n        1 => g\n    quotient_digits(abs_(num), g, 0) => n\n    quotient_digits(den, g, 0) => d\n    \"\" => sign\n    if num < 0:\n        \"-\" => sign\n    if d == 1:\n        return sign + str(n)\n    return sign + str(n) + \"/\" + str(d)\n\ndef decimal12(top, root_sign, d, den):\n    # (top + root_sign x sqrt(d)) / den to 12 places, rounded half away from\n    # zero; den > 0 and d >= 0. Worked to 18 places first: the floor of the\n    # root is off by less than one unit there, far below the 12th place.\n    root_digits(d, 18) => s\n    \"1\" => t\n    for i in [1:18]:\n        t + \"0\" => t\n    top * int(t) + root_sign * s => num\n    \"\" => sign\n    if num < 0:\n        \"-\" => sign\n        0 - num => num\n    str(quotient_digits(num, den, 0)) => q\n    while len(q) < 7:\n        \"0\" + q => q\n    int(q[0:len(q) - 6]) => whole\n    if int(q[len(q) - 6:len(q)]) >= 500000:\n        whole + 1 => whole\n    if whole == 0:\n        \"\" => sign\n    return sign + scaled_text(whole, 12)\n\ndef solve(A, B, C):\n    # [kind, ...] for whole coefficients (already scaled):\n    #   [\"all\"] or [\"none\"] when A = B = 0; [\"linear\", root]\n    #   [\"double\", root]; [\"two rational\", root, root]\n    #   [\"two real\", D, root, root] with D not a square, roots to 12 places\n    #   [\"complex\", real part, imaginary part]\n    if A == 0:\n        if B == 0:\n            if C == 0:\n                return [\"all\"]\n            return [\"none\"]\n        return [\"linear\", fraction_text(0 - C, B)]\n    if A < 0:\n        # the same roots, with a positive leading coefficient\n        0 - A => A\n        0 - B => B\n        0 - C => C\n    B * B - 4 * A * C => D\n    2 * A => den\n    if D == 0:\n        return [\"double\", fraction_text(0 - B, den)]\n    if D > 0 and is_square(D):\n        root_digits(D, 0) => s\n        return [\"two rational\", fraction_text(0 - B - s, den), fraction_text(0 - B + s, den)]\n    if D > 0:\n        return [\"two real\", D, decimal12(0 - B, 0 - 1, D, den), decimal12(0 - B, 1, D, den)]\n    return [\"complex\", fraction_text(0 - B, den), decimal12(0, 1, 0 - D, den)]\n\ndef fsqrt(x):\n    # a float square root by Newton's method from above, using only\n    # adding, dividing and comparing - the same in every Python\n    if x <= 0.0:\n        return 0.0\n    x => r\n    if r < 1.0:\n        1.0 => r\n    for i in [1:200]:\n        (r + x / r) / 2.0 => y\n        if y >= r:\n            return r\n        y => r\n    return r\n\ndef small_root(b):\n    # x^2 + bx + 1 = 0 for whole b >= 3: [textbook, stable] in floats,\n    # the root nearer zero\n    b * 1.0 => fb\n    fsqrt(fb * fb - 4.0) => s\n    (0.0 - fb + s) / 2.0 => naive\n    (0.0 - fb - s) / 2.0 => big\n    1.0 / big => stable\n    return [naive, stable]\n\ndef exact_small(b, n):\n    # the root of x^2 + bx + 1 nearest zero is -2 / (b + sqrt(b^2 - 4));\n    # [its first n significant digits, the power of ten of the first one]\n    n + len(str(b)) + 5 => k\n    \"1\" => t\n    for i in [1:k]:\n        t + \"0\" => t\n    int(t) => ten\n    root_digits(b * b - 4, k) => s\n    str(quotient_digits(2 * ten * ten, b * ten + s, 0)) => q\n    return [q[0:n], len(q) - k - 1]\n",
      "python": "def gcd(a, b):\n    if a < 0:\n        a = 0 - a\n    if b < 0:\n        b = 0 - b\n    while b != 0:\n        r = a % b\n        a = b\n        b = r\n    return a\n\ndef parse_coef(t):\n    if len(t) == 0:\n        return []\n    sign = 1\n    s = t\n    if s[0] == \"-\":\n        sign = 0 - 1\n        s = s[1:len(s)]\n    whole = \"\"\n    frac = \"\"\n    dot = False\n    for c in s:\n        if c == \".\":\n            if dot:\n                return []\n            dot = True\n        elif c >= \"0\" and c <= \"9\":\n            if dot:\n                frac = frac + c\n            else:\n                whole = whole + c\n        else:\n            return []\n    if whole == \"\" or len(frac) > 3 or dot and frac == \"\" or len(whole) > 9:\n        return []\n    while len(frac) < 3:\n        frac = frac + \"0\"\n    return [sign * (int(whole) * 1000 + int(frac))]\n\ndef root_digits(n, k):\n    s = str(n)\n    for i in range(1, k+1):\n        s = s + \"00\"\n    if len(s) % 2 == 1:\n        s = \"0\" + s\n    p = 0\n    r = 0\n    i = 0\n    while i < len(s):\n        r = r * 100 + int(s[i:i + 2])\n        d = 0\n        while (20 * p + d + 1) * (d + 1) <= r:\n            d = d + 1\n        r = r - (20 * p + d) * d\n        p = p * 10 + d\n        i = i + 2\n    return p\n\ndef is_square(n):\n    s = root_digits(n, 0)\n    return s * s == n\n\ndef quotient_digits(num, den, k):\n    s = str(num)\n    for i in range(1, k+1):\n        s = s + \"0\"\n    q = 0\n    r = 0\n    for c in s:\n        r = r * 10 + int(c)\n        d = 0\n        while r >= den:\n            r = r - den\n            d = d + 1\n        q = q * 10 + d\n    return q\n\ndef scaled_text(n, k):\n    sign = \"\"\n    if n < 0:\n        sign = \"-\"\n        n = 0 - n\n    s = str(n)\n    while len(s) <= k:\n        s = \"0\" + s\n    if k == 0:\n        return sign + s\n    return sign + s[0:len(s) - k] + \".\" + s[len(s) - k:len(s)]\n\ndef trimmed(t):\n    s = t\n    dot = False\n    for c in s:\n        if c == \".\":\n            dot = True\n    if not dot:\n        return s\n    while s[len(s) - 1] == \"0\":\n        s = s[0:len(s) - 1]\n    if s[len(s) - 1] == \".\":\n        s = s[0:len(s) - 1]\n    return s\n\ndef abs_(n):\n    if n < 0:\n        return 0 - n\n    return n\n\ndef fraction_text(num, den):\n    if den < 0:\n        num = 0 - num\n        den = 0 - den\n    g = gcd(num, den)\n    if g == 0:\n        g = 1\n    n = quotient_digits(abs_(num), g, 0)\n    d = quotient_digits(den, g, 0)\n    sign = \"\"\n    if num < 0:\n        sign = \"-\"\n    if d == 1:\n        return sign + str(n)\n    return sign + str(n) + \"/\" + str(d)\n\ndef decimal12(top, root_sign, d, den):\n    s = root_digits(d, 18)\n    t = \"1\"\n    for i in range(1, 19):\n        t = t + \"0\"\n    num = top * int(t) + root_sign * s\n    sign = \"\"\n    if num < 0:\n        sign = \"-\"\n        num = 0 - num\n    q = str(quotient_digits(num, den, 0))\n    while len(q) < 7:\n        q = \"0\" + q\n    whole = int(q[0:len(q) - 6])\n    if int(q[len(q) - 6:len(q)]) >= 500000:\n        whole = whole + 1\n    if whole == 0:\n        sign = \"\"\n    return sign + scaled_text(whole, 12)\n\ndef solve(A, B, C):\n    if A == 0:\n        if B == 0:\n            if C == 0:\n                return [\"all\"]\n            return [\"none\"]\n        return [\"linear\", fraction_text(0 - C, B)]\n    if A < 0:\n        A = 0 - A\n        B = 0 - B\n        C = 0 - C\n    D = B * B - 4 * A * C\n    den = 2 * A\n    if D == 0:\n        return [\"double\", fraction_text(0 - B, den)]\n    if D > 0 and is_square(D):\n        s = root_digits(D, 0)\n        return [\"two rational\", fraction_text(0 - B - s, den), fraction_text(0 - B + s, den)]\n    if D > 0:\n        return [\"two real\", D, decimal12(0 - B, 0 - 1, D, den), decimal12(0 - B, 1, D, den)]\n    return [\"complex\", fraction_text(0 - B, den), decimal12(0, 1, 0 - D, den)]\n\ndef fsqrt(x):\n    if x <= 0.0:\n        return 0.0\n    r = x\n    if r < 1.0:\n        r = 1.0\n    for i in range(1, 201):\n        y = (r + x / r) / 2.0\n        if y >= r:\n            return r\n        r = y\n    return r\n\ndef small_root(b):\n    fb = b * 1.0\n    s = fsqrt(fb * fb - 4.0)\n    naive = (0.0 - fb + s) / 2.0\n    big = (0.0 - fb - s) / 2.0\n    stable = 1.0 / big\n    return [naive, stable]\n\ndef exact_small(b, n):\n    k = n + len(str(b)) + 5\n    t = \"1\"\n    for i in range(1, k+1):\n        t = t + \"0\"\n    ten = int(t)\n    s = root_digits(b * b - 4, k)\n    q = str(quotient_digits(2 * ten * ten, b * ten + s, 0))\n    return [q[0:n], len(q) - k - 1]\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n1\n\n1\nabc\n1\n1.2345\n1\n1\n\n1\n1\n2\n\n1\n1\n2\n1,5\n1\n9999999999\n2\n\n2\n2\n2\n1000000000001\n2\nx\n3\n",
      "screen": "== Quadratic solver ==\nax^2 + bx + c = 0, solved exactly - and the same algebra in floats losing its small root.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 0\nPick a number from 1 to 3.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> x\nPick a number from 1 to 3.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> \nCancelled.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> abc\nType a number with at most 9 digits before the point and 3 after, such as -2 or 0.125.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> 1.2345\nType a number with at most 9 digits before the point and 3 after, such as -2 or 0.125.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> 1\nb> \nCancelled.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> 1\nb> 2\nc> \nCancelled.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> 1\nb> 2\nc> 1,5\nType a number with at most 9 digits before the point and 3 after, such as -2 or 0.125.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> 9999999999\nType a number with at most 9 digits before the point and 3 after, such as -2 or 0.125.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 2\nb, a whole number from 3 to 1000000000000> \nCancelled.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 2\nb, a whole number from 3 to 1000000000000> 2\nType a whole number from 3 to 1000000000000.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 2\nb, a whole number from 3 to 1000000000000> 1000000000001\nType a whole number from 3 to 1000000000000.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 2\nb, a whole number from 3 to 1000000000000> x\nType a whole number from 3 to 1000000000000.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 3\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n1\n-3\n2\n1\n2\n-7\n3\n1\n1\n0\n-2\n1\n1\n2\n5\n1\n1\n-2\n1\n1\n0\n2\n-4\n1\n0\n0\n0\n1\n0\n0\n5\n1\n-1\n0\n2\n1\n0.5\n-0.125\n0.007\n2\n3\n2\n100000000\n2\n1000000000000\n3\n",
      "screen": "== Quadratic solver ==\nax^2 + bx + c = 0, solved exactly - and the same algebra in floats losing its small root.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> 1\nb> -3\nc> 2\nx^2 - 3x + 2 = 0\nDiscriminant b^2 - 4ac = 1.\nIt is the square of a fraction: two rational roots, x = 1 and x = 2.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> 2\nb> -7\nc> 3\n2x^2 - 7x + 3 = 0\nDiscriminant b^2 - 4ac = 25.\nIt is the square of a fraction: two rational roots, x = 1/2 and x = 3.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> 1\nb> 0\nc> -2\nx^2 - 2 = 0\nDiscriminant b^2 - 4ac = 8.\nIt is positive and not the square of a fraction: two irrational roots, x = (-b - sqrt(D)) / 2a and (-b + sqrt(D)) / 2a:\n  x = -1.414213562373\n  x = 1.414213562373\nto 12 places, each digit worked out exactly.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> 1\nb> 2\nc> 5\nx^2 + 2x + 5 = 0\nDiscriminant b^2 - 4ac = -16.\nIt is negative: two complex roots, x = (-b / 2a) - i (sqrt(-D) / 2a) and its conjugate:\n  x = -1 - 2.000000000000i\n  x = -1 + 2.000000000000i\nthe imaginary part to 12 places.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> 1\nb> -2\nc> 1\nx^2 - 2x + 1 = 0\nDiscriminant b^2 - 4ac = 0.\nIt is zero: one double root, x = 1.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> 0\nb> 2\nc> -4\n2x - 4 = 0\nWith a = 0 it is not quadratic: one root, x = 2.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> 0\nb> 0\nc> 0\n0 = 0\nEvery x solves it: all three coefficients are zero.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> 0\nb> 0\nc> 5\n5 = 0\nNo x solves it: it says a number other than zero equals zero.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> -1\nb> 0\nc> 2\n-x^2 + 2 = 0\nDiscriminant b^2 - 4ac = 8.\nIt is positive and not the square of a fraction: two irrational roots, x = (-b - sqrt(D)) / 2a and (-b + sqrt(D)) / 2a:\n  x = -1.414213562373\n  x = 1.414213562373\nto 12 places, each digit worked out exactly.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 1\na> 0.5\nb> -0.125\nc> 0.007\n0.5x^2 - 0.125x + 0.007 = 0\nDiscriminant b^2 - 4ac = 0.001625.\nIt is positive and not the square of a fraction: two irrational roots, x = (-b - sqrt(D)) / 2a and (-b + sqrt(D)) / 2a:\n  x = 0.084688711259\n  x = 0.165311288741\nto 12 places, each digit worked out exactly.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 2\nb, a whole number from 3 to 1000000000000> 3\nx^2 + 3x + 1 = 0, its root nearest zero:\n  exact, 20 digits:  -3.8196601125010515179e-1\n  textbook formula:  -0.3819660112501051   (right to about 15 significant digits)\n  stable formula:    -0.38196601125010515   (right to 17 significant digits or more)\nThe textbook formula takes -b + sqrt(b^2 - 4) when the two are nearly equal, and the digits they share cancel. The stable one works out the big root, where nothing cancels, and divides: the product of the roots is c / a = 1.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 2\nb, a whole number from 3 to 1000000000000> 100000000\nx^2 + 100000000x + 1 = 0, its root nearest zero:\n  exact, 20 digits:  -1.0000000000000001000e-8\n  textbook formula:  -1.4901161193847656e-08   (off by 49%)\n  stable formula:    -1.0000000000000002e-08   (right to about 16 significant digits)\nThe textbook formula takes -b + sqrt(b^2 - 4) when the two are nearly equal, and the digits they share cancel. The stable one works out the big root, where nothing cancels, and divides: the product of the roots is c / a = 1.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 2\nb, a whole number from 3 to 1000000000000> 1000000000000\nx^2 + 1000000000000x + 1 = 0, its root nearest zero:\n  exact, 20 digits:  -1.0000000000000000000e-12\n  textbook formula:  0.0   (off by 100%)\n  stable formula:    -1e-12   (right to 17 significant digits or more)\nThe textbook formula takes -b + sqrt(b^2 - 4) when the two are nearly equal, and the digits they share cancel. The stable one works out the big root, where nothing cancels, and divides: the product of the roots is c / a = 1.\n\n1) solve  2) floats and cancellation  3) quit\nchoice> 3\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "quadratic-solver",
      "caseId": "034-quadratic-solver",
      "title": "Quadratic equation solver"
    },
    {
      "slug": "catastrophic-cancellation",
      "caseId": "234-catastrophic-cancellation",
      "title": "Same algebra, and one arrangement keeps its digits"
    }
  ],
  "updated": "2026-10-16"
}
