{
  "id": "P027",
  "slug": "csv-viewer",
  "key": "P027-csv-viewer",
  "title": "CSV viewer",
  "summary": "Paste a CSV of up to 30 rows and 8 columns - quoted fields with commas and doubled quotes included - and see it as a table; sort it by a column (numbers or text, stable, empty fields last), filter it by a condition, and get a column's statistics, with numbers kept exact.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P027 - CSV viewer\n\nPaste a CSV and see it as a table. Sort it by a column, filter it, and get a\ncolumn's statistics. Quoted fields work: a comma inside quotes is part of the\nfield, and a doubled quote inside quotes is one quote character.\n\n- `main.eml` - the menu, loading, the questions and their checks, and what\n  the screen shows\n- `table.eml` - reading a CSV line, column kinds, sorting, filtering,\n  statistics and drawing the table\n- `figures.eml` - numbers read exactly, and how they are shown\n\nHow each part works:\n\n- A line is read one character at a time, with one piece of state: whether\n  the reader is inside quotes (the state machine of the corpus case\n  `manual-csv-parser`). A quote left open at the end of a line is an error.\n- The first line is the header. A row with a different number of fields, or\n  with a quote never closed, is skipped with its row number and the reason;\n  the rest are loaded. Each row keeps its number from the CSV, shown in the\n  `#` column, so it can be found again after sorting.\n- A column holds numbers when every field that is not empty is a number\n  (such as `42`, `-3.25` or `0.5`, at most 4 decimals). Numbers are kept as\n  whole numbers of ten-thousandths, so sums and comparisons are exact; the\n  mean is rounded once, to 2 decimals, halves away from zero. Other columns\n  are text, compared without regard to case.\n- Sorting is an insertion sort, so rows that compare equal keep their order;\n  empty fields go last, whichever way the sort runs.\n- A filter on a number column takes `=`, `!=`, `<`, `<=`, `>` or `>=` and a\n  number; on a text column it keeps the rows whose field contains the text,\n  in any case. Empty fields never pass.\n- Statistics: for numbers, the count, the empty fields, the smallest, the\n  largest, the sum and the mean; for text, the count, the empty fields, how\n  many different values, the most common (all of them when several tie), and\n  the shortest and longest (the first row wins a tie).\n- The table aligns numbers right and text left; a field longer than 20\n  characters is cut to 17 and `...`.\n\nWhat is checked: a CSV is a header of 1 to 8 columns, each with a name of its\nown (in any case), and 1 to 30 rows that fit it; an empty line ends it, and\nthe 31st line ends it by itself. A column is chosen by its number or its\nname; an order is `a` or `d`. An empty answer cancels.\n\nSessions: `sessions/basic.in` loads five people with a quoted name holding a\ncomma, a quoted name holding doubled quotes, an empty age and an empty score,\nshows the table, sorts by age descending (the empty age last), filters score\n>= 80 and city containing `tai`, gives the statistics of score (mean 85.19),\ncity (by number) and age (by name in capitals), and sorts by city - the two\nTaipei rows keep their order; `sessions/bad-input.in` uses the menu before\nanything is loaded, loads nothing, a header with the same name twice, rows\nthat all fail, a mix of good rows, short rows and an unclosed quote with a\nnote longer than 20 characters, asks for column 9 and a column that does not\nexist, order `x`, a filter that matches nothing, the statistics of an empty\ncolumn, 31 lines, conditions `big` and `> x`, and `!= 0` over 30 rows.\n\nBuilt on the verified corpus case `manual-csv-parser` (a CSV line split by a\nstate machine that knows whether it is inside quotes, with doubled quotes).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P027 CSV viewer: paste a CSV - quoted fields included - and see it as a\n# table, sort it by a column, filter it, and get a column's statistics.\nimport table\nimport figures\n\n30 => most_rows\n8 => most_columns\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\ndef all_digits(s):\n    if s == \"\" or len(s) > 3:\n        return False\n    for c in s:\n        if not (c in \"0123456789\"):\n            return False\n    return True\n\ndef listed(items):\n    \"\" => out\n    0 => i\n    while i < len(items):\n        if i > 0 and i == len(items) - 1:\n            out + \" and \" => out\n        elif i > 0:\n            out + \", \" => out\n        out + items[i] => out\n        i + 1 => i\n    return out\n\ndef plural(n, word):\n    if n == 1:\n        return \"1 \" + word\n    return str(n) + \" \" + word + \"s\"\n\ndef load():\n    # [header, rows, kinds] from typed lines, or [] when nothing was loaded.\n    (\"Type or paste the CSV: the header first, then one row per line (at most \" + str(most_rows) + \" rows); an empty line ends it.\") ^0\n    [] => lines\n    True => reading\n    while reading and len(lines) < most_rows + 1:\n        input(\"> \") => line\n        if trim(line) == \"\":\n            False => reading\n        else:\n            lines + [line] => lines\n    if reading:\n        (\"That is \" + str(most_rows) + \" rows, the most a table can have.\") ^0\n    if len(lines) == 0:\n        \"Nothing was loaded.\" ^0\n        return []\n    table.parse_line(lines[0]) => h\n    [] => header\n    True => good\n    if not h[0] or len(h[1]) > most_columns:\n        False => good\n    else:\n        for name in h[1]:\n            trim(name) => name\n            for other in header:\n                if table.lower(other) == table.lower(name):\n                    False => good\n            if name == \"\":\n                False => good\n            header + [name] => header\n    if not good:\n        (\"The header needs 1 to \" + str(most_columns) + \" columns, each with a name of its own; nothing was loaded.\") ^0\n        return []\n    [] => rows\n    for i in [1:len(lines) - 1]:\n        table.parse_line(lines[i]) => p\n        if not p[0]:\n            (\"Row \" + str(i) + \" was skipped: \" + p[1] + \".\") ^0\n        elif len(p[1]) != len(header):\n            (\"Row \" + str(i) + \" was skipped: it has \" + plural(len(p[1]), \"field\") + \", the header has \" + str(len(header)) + \".\") ^0\n        else:\n            rows + [[i, p[1]]] => rows\n    if len(rows) == 0:\n        \"The CSV has no rows that fit the header; nothing was loaded.\" ^0\n        return []\n    [] => kinds\n    [] => names\n    for k in [0:len(header) - 1]:\n        table.kind_of(rows, k) => kind\n        kinds + [kind] => kinds\n        if kind[0] == \"number\":\n            names + [header[k] + \" (numbers)\"] => names\n        else:\n            names + [header[k]] => names\n    (\"Loaded \" + plural(len(rows), \"row\") + \" and \" + plural(len(header), \"column\") + \": \" + listed(names) + \".\") ^0\n    return [header, rows, kinds]\n\ndef ask_column(header):\n    # A column's index, or -1 when the answer is empty, which cancels.\n    while True:\n        trim(input(\"column (number or name)> \")) => answer\n        if answer == \"\":\n            return 0 - 1\n        if all_digits(answer) and int(answer) >= 1 and int(answer) <= len(header):\n            return int(answer) - 1\n        for k in [0:len(header) - 1]:\n            if table.lower(header[k]) == table.lower(answer):\n                return k\n        (\"Type a column number from 1 to \" + str(len(header)) + \" or a column name.\") ^0\n\ndef show(data, rows):\n    for line in table.draw(data[0], rows, data[2]):\n        line ^0\n\ndef sort(data):\n    # Returns the data with its rows in the new order, or unchanged.\n    ask_column(data[0]) => k\n    if k == 0 - 1:\n        \"Cancelled.\" ^0\n        return data\n    \"\" => order\n    while order == \"\":\n        table.lower(trim(input(\"order (a = ascending, d = descending)> \"))) => answer\n        if answer == \"\":\n            \"Cancelled.\" ^0\n            return data\n        if answer == \"a\" or answer == \"d\":\n            answer => order\n        else:\n            \"Type a or d.\" ^0\n    data[2][k] => kind\n    table.sorted_rows(data[1], k, kind[0], order == \"d\") => rows\n    \"ascending\" => way\n    if order == \"d\":\n        \"descending\" => way\n    \"text, any case\" => how\n    if kind[0] == \"number\":\n        \"numbers\" => how\n    (\"Sorted by \" + data[0][k] + \", \" + way + \" (\" + how + \"; empty fields last).\") ^0\n    [data[0], rows, data[2]] => sorted_data\n    show(sorted_data, rows)\n    return sorted_data\n\ndef filter_rows(data):\n    ask_column(data[0]) => k\n    if k == 0 - 1:\n        \"Cancelled.\" ^0\n        return\n    data[2][k] => kind\n    \"\" => op\n    \"\" => target\n    \"\" => wording\n    if kind[0] == \"number\":\n        while op == \"\":\n            trim(input(\"condition (such as > 30, = 42 or != 0)> \")) => answer\n            if answer == \"\":\n                \"Cancelled.\" ^0\n                return\n            table.condition(answer) => c\n            if c[0]:\n                c[1] => op\n                c[2] => target\n                (data[0][k] + \" \" + op + \" \" + c[3]) => wording\n            else:\n                \"Type =, !=, <, <=, > or >= and a number, such as >= 30.\" ^0\n    else:\n        while op == \"\":\n            trim(input(\"text to look for (any case)> \")) => answer\n            if answer == \"\":\n                \"Cancelled.\" ^0\n                return\n            \"has\" => op\n            table.lower(answer) => target\n            (\"'\" + answer + \"' in \" + data[0][k]) => wording\n    [] => kept\n    for r in data[1]:\n        if table.keeps(r[1][k], kind[0], op, target):\n            kept + [r] => kept\n    if len(kept) == 0:\n        (\"No row has \" + wording + \".\") ^0\n    else:\n        \"have\" => verb\n        if len(kept) == 1:\n            \"has\" => verb\n        (str(len(kept)) + \" of \" + plural(len(data[1]), \"row\") + \" \" + verb + \" \" + wording + \":\") ^0\n        show(data, kept)\n\ndef stats(data):\n    ask_column(data[0]) => k\n    if k == 0 - 1:\n        \"Cancelled.\" ^0\n        return\n    data[0][k] => name\n    data[2][k] => kind\n    if kind[0] == \"number\":\n        table.number_stats(data[1], k) => s\n        (name + \": numbers - \" + plural(s[0], \"value\") + \", \" + str(s[1]) + \" empty\") ^0\n        (\"  smallest \" + figures.shown(s[2], kind[1]) + \", largest \" + figures.shown(s[4], kind[1]) + \", sum \" + figures.shown(s[6], kind[1]) + \", mean \" + figures.mean_text(s[6], s[0])) ^0\n        return\n    table.text_stats(data[1], k) => s\n    if s[0] == 0:\n        (name + \": every field is empty.\") ^0\n        return\n    (name + \": text - \" + plural(s[0], \"value\") + \", \" + str(s[1]) + \" empty, \" + str(len(s[2])) + \" different\") ^0\n    0 => most\n    for c in s[3]:\n        if c > most:\n            c => most\n    if most == 1:\n        \"  every value appears once\" ^0\n    else:\n        [] => top\n        for i in [0:len(s[2]) - 1]:\n            if s[3][i] == most:\n                top + [s[2][i]] => top\n        if len(top) == 1:\n            (\"  most common: \" + top[0] + \" (\" + str(most) + \" times)\") ^0\n        else:\n            (\"  most common: \" + listed(top) + \" (\" + str(most) + \" times each)\") ^0\n    (\"  shortest: \" + s[4] + \" (\" + plural(len(s[4]), \"character\") + \"), longest: \" + s[5] + \" (\" + plural(len(s[5]), \"character\") + \")\") ^0\n\n[] => data\nTrue => running\nwhile running:\n    \"\" ^0\n    \"== CSV viewer ==\" ^0\n    \"1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\" ^0\n    trim(input(\"choice> \")) => choice\n    if choice == \"1\":\n        load() => loaded\n        if len(loaded) > 0:\n            loaded => data\n            show(data, data[1])\n    elif choice == \"6\":\n        False => running\n    elif choice == \"2\" or choice == \"3\" or choice == \"4\" or choice == \"5\":\n        if len(data) == 0:\n            \"No table yet; load a CSV first (1).\" ^0\n        elif choice == \"2\":\n            show(data, data[1])\n        elif choice == \"3\":\n            sort(data) => data\n        elif choice == \"4\":\n            filter_rows(data)\n        else:\n            stats(data)\n    else:\n        \"Pick a number from 1 to 6.\" ^0\n\"Bye.\" ^0\n",
      "python": "import table\nimport figures\nmost_rows = 30\nmost_columns = 8\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\ndef all_digits(s):\n    if s == \"\" or len(s) > 3:\n        return False\n    for c in s:\n        if not c in \"0123456789\":\n            return False\n    return True\n\ndef listed(items):\n    out = \"\"\n    i = 0\n    while i < len(items):\n        if i > 0 and i == len(items) - 1:\n            out = out + \" and \"\n        elif i > 0:\n            out = out + \", \"\n        out = out + items[i]\n        i = i + 1\n    return out\n\ndef plural(n, word):\n    if n == 1:\n        return \"1 \" + word\n    return str(n) + \" \" + word + \"s\"\n\ndef load():\n    print(\"Type or paste the CSV: the header first, then one row per line (at most \" + str(most_rows) + \" rows); an empty line ends it.\")\n    lines = []\n    reading = True\n    while reading and len(lines) < most_rows + 1:\n        line = input(\"> \")\n        if trim(line) == \"\":\n            reading = False\n        else:\n            lines = lines + [line]\n    if reading:\n        print(\"That is \" + str(most_rows) + \" rows, the most a table can have.\")\n    if len(lines) == 0:\n        print(\"Nothing was loaded.\")\n        return []\n    h = table.parse_line(lines[0])\n    header = []\n    good = True\n    if not h[0] or len(h[1]) > most_columns:\n        good = False\n    else:\n        for name in h[1]:\n            name = trim(name)\n            for other in header:\n                if table.lower(other) == table.lower(name):\n                    good = False\n            if name == \"\":\n                good = False\n            header = header + [name]\n    if not good:\n        print(\"The header needs 1 to \" + str(most_columns) + \" columns, each with a name of its own; nothing was loaded.\")\n        return []\n    rows = []\n    for i in range(1, len(lines)):\n        p = table.parse_line(lines[i])\n        if not p[0]:\n            print(\"Row \" + str(i) + \" was skipped: \" + p[1] + \".\")\n        elif len(p[1]) != len(header):\n            print(\"Row \" + str(i) + \" was skipped: it has \" + plural(len(p[1]), \"field\") + \", the header has \" + str(len(header)) + \".\")\n        else:\n            rows = rows + [[i, p[1]]]\n    if len(rows) == 0:\n        print(\"The CSV has no rows that fit the header; nothing was loaded.\")\n        return []\n    kinds = []\n    names = []\n    for k in range(0, len(header)):\n        kind = table.kind_of(rows, k)\n        kinds = kinds + [kind]\n        if kind[0] == \"number\":\n            names = names + [header[k] + \" (numbers)\"]\n        else:\n            names = names + [header[k]]\n    print(\"Loaded \" + plural(len(rows), \"row\") + \" and \" + plural(len(header), \"column\") + \": \" + listed(names) + \".\")\n    return [header, rows, kinds]\n\ndef ask_column(header):\n    while True:\n        answer = trim(input(\"column (number or name)> \"))\n        if answer == \"\":\n            return 0 - 1\n        if all_digits(answer) and int(answer) >= 1 and int(answer) <= len(header):\n            return int(answer) - 1\n        for k in range(0, len(header)):\n            if table.lower(header[k]) == table.lower(answer):\n                return k\n        print(\"Type a column number from 1 to \" + str(len(header)) + \" or a column name.\")\n\ndef show(data, rows):\n    for line in table.draw(data[0], rows, data[2]):\n        print(line)\n\ndef sort(data):\n    k = ask_column(data[0])\n    if k == 0 - 1:\n        print(\"Cancelled.\")\n        return data\n    order = \"\"\n    while order == \"\":\n        answer = table.lower(trim(input(\"order (a = ascending, d = descending)> \")))\n        if answer == \"\":\n            print(\"Cancelled.\")\n            return data\n        if answer == \"a\" or answer == \"d\":\n            order = answer\n        else:\n            print(\"Type a or d.\")\n    kind = data[2][k]\n    rows = table.sorted_rows(data[1], k, kind[0], order == \"d\")\n    way = \"ascending\"\n    if order == \"d\":\n        way = \"descending\"\n    how = \"text, any case\"\n    if kind[0] == \"number\":\n        how = \"numbers\"\n    print(\"Sorted by \" + data[0][k] + \", \" + way + \" (\" + how + \"; empty fields last).\")\n    sorted_data = [data[0], rows, data[2]]\n    show(sorted_data, rows)\n    return sorted_data\n\ndef filter_rows(data):\n    k = ask_column(data[0])\n    if k == 0 - 1:\n        print(\"Cancelled.\")\n        return\n    kind = data[2][k]\n    op = \"\"\n    target = \"\"\n    wording = \"\"\n    if kind[0] == \"number\":\n        while op == \"\":\n            answer = trim(input(\"condition (such as > 30, = 42 or != 0)> \"))\n            if answer == \"\":\n                print(\"Cancelled.\")\n                return\n            c = table.condition(answer)\n            if c[0]:\n                op = c[1]\n                target = c[2]\n                wording = data[0][k] + \" \" + op + \" \" + c[3]\n            else:\n                print(\"Type =, !=, <, <=, > or >= and a number, such as >= 30.\")\n    else:\n        while op == \"\":\n            answer = trim(input(\"text to look for (any case)> \"))\n            if answer == \"\":\n                print(\"Cancelled.\")\n                return\n            op = \"has\"\n            target = table.lower(answer)\n            wording = \"'\" + answer + \"' in \" + data[0][k]\n    kept = []\n    for r in data[1]:\n        if table.keeps(r[1][k], kind[0], op, target):\n            kept = kept + [r]\n    if len(kept) == 0:\n        print(\"No row has \" + wording + \".\")\n    else:\n        verb = \"have\"\n        if len(kept) == 1:\n            verb = \"has\"\n        print(str(len(kept)) + \" of \" + plural(len(data[1]), \"row\") + \" \" + verb + \" \" + wording + \":\")\n        show(data, kept)\n\ndef stats(data):\n    k = ask_column(data[0])\n    if k == 0 - 1:\n        print(\"Cancelled.\")\n        return\n    name = data[0][k]\n    kind = data[2][k]\n    if kind[0] == \"number\":\n        s = table.number_stats(data[1], k)\n        print(name + \": numbers - \" + plural(s[0], \"value\") + \", \" + str(s[1]) + \" empty\")\n        print(\"  smallest \" + figures.shown(s[2], kind[1]) + \", largest \" + figures.shown(s[4], kind[1]) + \", sum \" + figures.shown(s[6], kind[1]) + \", mean \" + figures.mean_text(s[6], s[0]))\n        return\n    s = table.text_stats(data[1], k)\n    if s[0] == 0:\n        print(name + \": every field is empty.\")\n        return\n    print(name + \": text - \" + plural(s[0], \"value\") + \", \" + str(s[1]) + \" empty, \" + str(len(s[2])) + \" different\")\n    most = 0\n    for c in s[3]:\n        if c > most:\n            most = c\n    if most == 1:\n        print(\"  every value appears once\")\n    else:\n        top = []\n        for i in range(0, len(s[2])):\n            if s[3][i] == most:\n                top = top + [s[2][i]]\n        if len(top) == 1:\n            print(\"  most common: \" + top[0] + \" (\" + str(most) + \" times)\")\n        else:\n            print(\"  most common: \" + listed(top) + \" (\" + str(most) + \" times each)\")\n    print(\"  shortest: \" + s[4] + \" (\" + plural(len(s[4]), \"character\") + \"), longest: \" + s[5] + \" (\" + plural(len(s[5]), \"character\") + \")\")\n\ndata = []\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"== CSV viewer ==\")\n    print(\"1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\")\n    choice = trim(input(\"choice> \"))\n    if choice == \"1\":\n        loaded = load()\n        if len(loaded) > 0:\n            data = loaded\n            show(data, data[1])\n    elif choice == \"6\":\n        running = False\n    elif choice == \"2\" or choice == \"3\" or choice == \"4\" or choice == \"5\":\n        if len(data) == 0:\n            print(\"No table yet; load a CSV first (1).\")\n        elif choice == \"2\":\n            show(data, data[1])\n        elif choice == \"3\":\n            data = sort(data)\n        elif choice == \"4\":\n            filter_rows(data)\n        else:\n            stats(data)\n    else:\n        print(\"Pick a number from 1 to 6.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "table.eml",
      "eml": "# P027 CSV viewer - reading CSV rows and what the table can do: column\n# kinds, sorting, filtering, statistics and drawing. A row is\n# [row number, fields]; the row number is its place in the CSV as loaded.\nimport figures\n\ndef parse_line(line):\n    # [True, fields] or [False, reason]. A field in double quotes may hold\n    # commas, and \"\" inside quotes is one quote character - the state\n    # machine of the corpus case manual-csv-parser.\n    [] => fields\n    \"\" => current\n    False => quoted\n    0 => i\n    while i < len(line):\n        line[i] => c\n        if c == \"\\\"\":\n            if quoted and i + 1 < len(line) and line[i + 1] == \"\\\"\":\n                current + \"\\\"\" => current\n                i + 1 => i\n            else:\n                not quoted => quoted\n        elif c == \",\" and not quoted:\n            fields + [current] => fields\n            \"\" => current\n        else:\n            current + c => current\n        i + 1 => i\n    if quoted:\n        return [False, \"a quote is never closed\"]\n    fields + [current] => fields\n    return [True, fields]\n\ndef lower(s):\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\" => upper\n    \"abcdefghijklmnopqrstuvwxyz\" => small\n    \"\" => out\n    for c in s:\n        0 => k\n        while k < 26 and upper[k] != c:\n            k + 1 => k\n        if k < 26:\n            out + small[k] => out\n        else:\n            out + c => out\n    return out\n\ndef contains(s, part):\n    # True when part occurs in s.\n    0 => i\n    while i + len(part) <= len(s):\n        if s[i:i + len(part)] == part:\n            return True\n        i + 1 => i\n    return False\n\ndef kind_of(rows, k):\n    # [\"number\", decimals] when every non-empty field of column k is a number\n    # and at least one is; [\"text\", 0] otherwise.\n    0 => places\n    0 => seen\n    for r in rows:\n        r[1][k] => f\n        if f != \"\":\n            figures.value_of(f) => v\n            if not v[0]:\n                return [\"text\", 0]\n            if v[2] > places:\n                v[2] => places\n            seen + 1 => seen\n    if seen == 0:\n        return [\"text\", 0]\n    return [\"number\", places]\n\ndef goes_before(a, b, k, kind, descending):\n    # True when row a belongs strictly before row b. Empty fields always go\n    # last; equal fields keep their order.\n    a[1][k] => x\n    b[1][k] => y\n    if x == \"\" or y == \"\":\n        return x != \"\" and y == \"\"\n    if kind == \"number\":\n        figures.value_of(x)[1] => vx\n        figures.value_of(y)[1] => vy\n    else:\n        lower(x) => vx\n        lower(y) => vy\n    if descending:\n        return vx > vy\n    return vx < vy\n\ndef sorted_rows(rows, k, kind, descending):\n    # Insertion sort: each row goes in after every row it does not belong\n    # before, so the sort is stable.\n    [] => out\n    for r in rows:\n        len(out) => j\n        while j > 0 and goes_before(r, out[j - 1], k, kind, descending):\n            j - 1 => j\n        out[0:j] + [r] + out[j:len(out)] => out\n    return out\n\ndef condition(s):\n    # [True, operator, ten-thousandths, the number as typed] for \"> 30\",\n    # \"<=2.5\", \"!= 0\" ...; [False, \"\", 0, \"\"] otherwise.\n    0 => i\n    while i < len(s) and s[i] == \" \":\n        i + 1 => i\n    s[i:len(s)] => s\n    \"\"  => op\n    for o in [\">=\", \"<=\", \"!=\", \">\", \"<\", \"=\"]:\n        if op == \"\" and s[0:len(o)] == o:\n            o => op\n    if op == \"\":\n        return [False, \"\", 0, \"\"]\n    s[len(op):len(s)] => rest\n    0 => j\n    while j < len(rest) and rest[j] == \" \":\n        j + 1 => j\n    len(rest) => e\n    while e > j and rest[e - 1] == \" \":\n        e - 1 => e\n    figures.value_of(rest[j:e]) => v\n    if not v[0]:\n        return [False, \"\", 0, \"\"]\n    return [True, op, v[1], rest[j:e]]\n\ndef keeps(field, kind, op, target):\n    # Whether a field passes the filter. Empty fields never pass.\n    if field == \"\":\n        return False\n    if kind == \"text\":\n        return contains(lower(field), target)\n    figures.value_of(field)[1] => v\n    if op == \"=\":\n        return v == target\n    if op == \"!=\":\n        return v != target\n    if op == \"<\":\n        return v < target\n    if op == \"<=\":\n        return v <= target\n    if op == \">\":\n        return v > target\n    return v >= target\n\ndef number_stats(rows, k):\n    # [count, empty, smallest, its text, largest, its text, total]; the\n    # first row wins a tie for smallest or largest.\n    0 => count\n    0 => empty\n    0 => total\n    0 => low\n    \"\" => low_text\n    0 => high\n    \"\" => high_text\n    for r in rows:\n        r[1][k] => f\n        if f == \"\":\n            empty + 1 => empty\n        else:\n            figures.value_of(f)[1] => v\n            if count == 0 or v < low:\n                v => low\n                f => low_text\n            if count == 0 or v > high:\n                v => high\n                f => high_text\n            total + v => total\n            count + 1 => count\n    return [count, empty, low, low_text, high, high_text, total]\n\ndef text_stats(rows, k):\n    # [count, empty, distinct values in first-seen order, their counts,\n    # shortest, longest]; the first row wins a tie for shortest or longest.\n    0 => count\n    0 => empty\n    [] => seen\n    {} => times\n    \"\" => shortest\n    \"\" => longest\n    for r in rows:\n        r[1][k] => f\n        if f == \"\":\n            empty + 1 => empty\n        else:\n            if f in times:\n                times[f] + 1 => times[f]\n            else:\n                1 => times[f]\n                seen + [f] => seen\n            if count == 0 or len(f) < len(shortest):\n                f => shortest\n            if count == 0 or len(f) > len(longest):\n                f => longest\n            count + 1 => count\n    [] => counts\n    for v in seen:\n        counts + [times[v]] => counts\n    return [count, empty, seen, counts, shortest, longest]\n\ndef cell(s):\n    # A field as shown: at most 20 characters, longer ones cut with \"...\".\n    if len(s) > 20:\n        return s[0:17] + \"...\"\n    return s\n\ndef pad(s, width, right):\n    while len(s) < width:\n        if right:\n            \" \" + s => s\n        else:\n            s + \" \" => s\n    return s\n\ndef without_trailing(s):\n    len(s) => e\n    while e > 0 and s[e - 1] == \" \":\n        e - 1 => e\n    return s[0:e]\n\ndef draw(header, rows, kinds):\n    # The rows as lines of text: a # column with each row's number, then\n    # the columns, numbers aligned right and text left.\n    1 => num_width\n    for r in rows:\n        if len(str(r[0])) > num_width:\n            len(str(r[0])) => num_width\n    [] => widths\n    for k in [0:len(header) - 1]:\n        len(header[k]) => w\n        for r in rows:\n            if len(cell(r[1][k])) > w:\n                len(cell(r[1][k])) => w\n        widths + [w] => widths\n    pad(\"#\", num_width, True) => top\n    \"-\" * num_width => rule\n    for k in [0:len(header) - 1]:\n        top + \"  \" + pad(header[k], widths[k], kinds[k][0] == \"number\") => top\n        rule + \"  \" + \"-\" * widths[k] => rule\n    [without_trailing(top), rule] => lines\n    for r in rows:\n        pad(str(r[0]), num_width, True) => line\n        for k in [0:len(header) - 1]:\n            line + \"  \" + pad(cell(r[1][k]), widths[k], kinds[k][0] == \"number\") => line\n        lines + [without_trailing(line)] => lines\n    return lines\n",
      "python": "import figures\n\ndef parse_line(line):\n    fields = []\n    current = \"\"\n    quoted = False\n    i = 0\n    while i < len(line):\n        c = line[i]\n        if c == \"\\\"\":\n            if quoted and i + 1 < len(line) and line[i + 1] == \"\\\"\":\n                current = current + \"\\\"\"\n                i = i + 1\n            else:\n                quoted = not quoted\n        elif c == \",\" and not quoted:\n            fields = fields + [current]\n            current = \"\"\n        else:\n            current = current + c\n        i = i + 1\n    if quoted:\n        return [False, \"a quote is never closed\"]\n    fields = fields + [current]\n    return [True, fields]\n\ndef lower(s):\n    upper = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n    small = \"abcdefghijklmnopqrstuvwxyz\"\n    out = \"\"\n    for c in s:\n        k = 0\n        while k < 26 and upper[k] != c:\n            k = k + 1\n        if k < 26:\n            out = out + small[k]\n        else:\n            out = out + c\n    return out\n\ndef contains(s, part):\n    i = 0\n    while i + len(part) <= len(s):\n        if s[i:i + len(part)] == part:\n            return True\n        i = i + 1\n    return False\n\ndef kind_of(rows, k):\n    places = 0\n    seen = 0\n    for r in rows:\n        f = r[1][k]\n        if f != \"\":\n            v = figures.value_of(f)\n            if not v[0]:\n                return [\"text\", 0]\n            if v[2] > places:\n                places = v[2]\n            seen = seen + 1\n    if seen == 0:\n        return [\"text\", 0]\n    return [\"number\", places]\n\ndef goes_before(a, b, k, kind, descending):\n    x = a[1][k]\n    y = b[1][k]\n    if x == \"\" or y == \"\":\n        return x != \"\" and y == \"\"\n    if kind == \"number\":\n        vx = figures.value_of(x)[1]\n        vy = figures.value_of(y)[1]\n    else:\n        vx = lower(x)\n        vy = lower(y)\n    if descending:\n        return vx > vy\n    return vx < vy\n\ndef sorted_rows(rows, k, kind, descending):\n    out = []\n    for r in rows:\n        j = len(out)\n        while j > 0 and goes_before(r, out[j - 1], k, kind, descending):\n            j = j - 1\n        out = out[0:j] + [r] + out[j:len(out)]\n    return out\n\ndef condition(s):\n    i = 0\n    while i < len(s) and s[i] == \" \":\n        i = i + 1\n    s = s[i:len(s)]\n    op = \"\"\n    for o in [\">=\", \"<=\", \"!=\", \">\", \"<\", \"=\"]:\n        if op == \"\" and s[0:len(o)] == o:\n            op = o\n    if op == \"\":\n        return [False, \"\", 0, \"\"]\n    rest = s[len(op):len(s)]\n    j = 0\n    while j < len(rest) and rest[j] == \" \":\n        j = j + 1\n    e = len(rest)\n    while e > j and rest[e - 1] == \" \":\n        e = e - 1\n    v = figures.value_of(rest[j:e])\n    if not v[0]:\n        return [False, \"\", 0, \"\"]\n    return [True, op, v[1], rest[j:e]]\n\ndef keeps(field, kind, op, target):\n    if field == \"\":\n        return False\n    if kind == \"text\":\n        return contains(lower(field), target)\n    v = figures.value_of(field)[1]\n    if op == \"=\":\n        return v == target\n    if op == \"!=\":\n        return v != target\n    if op == \"<\":\n        return v < target\n    if op == \"<=\":\n        return v <= target\n    if op == \">\":\n        return v > target\n    return v >= target\n\ndef number_stats(rows, k):\n    count = 0\n    empty = 0\n    total = 0\n    low = 0\n    low_text = \"\"\n    high = 0\n    high_text = \"\"\n    for r in rows:\n        f = r[1][k]\n        if f == \"\":\n            empty = empty + 1\n        else:\n            v = figures.value_of(f)[1]\n            if count == 0 or v < low:\n                low = v\n                low_text = f\n            if count == 0 or v > high:\n                high = v\n                high_text = f\n            total = total + v\n            count = count + 1\n    return [count, empty, low, low_text, high, high_text, total]\n\ndef text_stats(rows, k):\n    count = 0\n    empty = 0\n    seen = []\n    times = {}\n    shortest = \"\"\n    longest = \"\"\n    for r in rows:\n        f = r[1][k]\n        if f == \"\":\n            empty = empty + 1\n        else:\n            if f in times:\n                times[f] = times[f] + 1\n            else:\n                times[f] = 1\n                seen = seen + [f]\n            if count == 0 or len(f) < len(shortest):\n                shortest = f\n            if count == 0 or len(f) > len(longest):\n                longest = f\n            count = count + 1\n    counts = []\n    for v in seen:\n        counts = counts + [times[v]]\n    return [count, empty, seen, counts, shortest, longest]\n\ndef cell(s):\n    if len(s) > 20:\n        return s[0:17] + \"...\"\n    return s\n\ndef pad(s, width, right):\n    while len(s) < width:\n        if right:\n            s = \" \" + s\n        else:\n            s = s + \" \"\n    return s\n\ndef without_trailing(s):\n    e = len(s)\n    while e > 0 and s[e - 1] == \" \":\n        e = e - 1\n    return s[0:e]\n\ndef draw(header, rows, kinds):\n    num_width = 1\n    for r in rows:\n        if len(str(r[0])) > num_width:\n            num_width = len(str(r[0]))\n    widths = []\n    for k in range(0, len(header)):\n        w = len(header[k])\n        for r in rows:\n            if len(cell(r[1][k])) > w:\n                w = len(cell(r[1][k]))\n        widths = widths + [w]\n    top = pad(\"#\", num_width, True)\n    rule = \"-\" * num_width\n    for k in range(0, len(header)):\n        top = top + \"  \" + pad(header[k], widths[k], kinds[k][0] == \"number\")\n        rule = rule + \"  \" + \"-\" * widths[k]\n    lines = [without_trailing(top), rule]\n    for r in rows:\n        line = pad(str(r[0]), num_width, True)\n        for k in range(0, len(header)):\n            line = line + \"  \" + pad(cell(r[1][k]), widths[k], kinds[k][0] == \"number\")\n        lines = lines + [without_trailing(line)]\n    return lines\n"
    },
    {
      "name": "figures.eml",
      "eml": "# P027 CSV viewer - numbers in a column, read exactly. A number such as\n# -12.5 is kept as a whole number of ten-thousandths (-125000), so sums and\n# comparisons are exact, and a mean is rounded once, when it is shown.\n\n\"0123456789\" => digits\n10000 => scale\n\ndef quotient(a, b):\n    # a divided by b, rounded down, for a >= 0 and b > 0.\n    return int((a - a % b) / b)\n\ndef rounded(a, b):\n    # a / b rounded to a whole number, halves away from zero; b > 0.\n    if a < 0:\n        return 0 - quotient(0 - 2 * a + b, 2 * b)\n    return quotient(2 * a + b, 2 * b)\n\ndef value_of(s):\n    # [True, ten-thousandths, decimals] for \"42\", \"-3.25\" or \"0.5\" (at most\n    # 4 decimals and 12 digits before the point); [False, 0, 0] otherwise.\n    1 => sign\n    0 => i\n    if len(s) > 0 and s[0] == \"-\":\n        0 - 1 => sign\n        1 => i\n    0 => whole\n    i => start\n    while i < len(s) and s[i] in digits:\n        whole * 10 + int(s[i]) => whole\n        i + 1 => i\n    if i == start or i - start > 12:\n        return [False, 0, 0]\n    0 => frac\n    0 => places\n    if i < len(s):\n        if s[i] != \".\":\n            return [False, 0, 0]\n        i + 1 => i\n        while i < len(s) and s[i] in digits:\n            frac * 10 + int(s[i]) => frac\n            places + 1 => places\n            i + 1 => i\n        if i < len(s) or places == 0 or places > 4:\n            return [False, 0, 0]\n    places => p\n    while p < 4:\n        frac * 10 => frac\n        p + 1 => p\n    return [True, sign * (whole * scale + frac), places]\n\ndef with_places(n, places):\n    # A whole number of units of 10^-places as a decimal: 1250, 2 -> \"12.50\".\n    \"\" => sign\n    n => a\n    if a < 0:\n        \"-\" => sign\n        0 - a => a\n    1 => one\n    0 => p\n    while p < places:\n        one * 10 => one\n        p + 1 => p\n    str(quotient(a, one)) => out\n    if places > 0:\n        str(a % one) => f\n        while len(f) < places:\n            \"0\" + f => f\n        out + \".\" + f => out\n    if a == 0:\n        return out\n    return sign + out\n\ndef shown(v, places):\n    # Ten-thousandths shown with `places` decimals (0 to 4); the values a\n    # column holds always fit its own number of places exactly.\n    1 => unit\n    places => p\n    while p < 4:\n        unit * 10 => unit\n        p + 1 => p\n    return with_places(rounded(v, unit), places)\n\ndef mean_text(total, count):\n    # The mean of values adding up to `total` ten-thousandths, to 2 decimals,\n    # rounded once, halves away from zero.\n    return with_places(rounded(total, 100 * count), 2)\n",
      "python": "digits = \"0123456789\"\nscale = 10000\n\ndef quotient(a, b):\n    return int((a - a % b) / b)\n\ndef rounded(a, b):\n    if a < 0:\n        return 0 - quotient(0 - 2 * a + b, 2 * b)\n    return quotient(2 * a + b, 2 * b)\n\ndef value_of(s):\n    sign = 1\n    i = 0\n    if len(s) > 0 and s[0] == \"-\":\n        sign = 0 - 1\n        i = 1\n    whole = 0\n    start = i\n    while i < len(s) and s[i] in digits:\n        whole = whole * 10 + int(s[i])\n        i = i + 1\n    if i == start or i - start > 12:\n        return [False, 0, 0]\n    frac = 0\n    places = 0\n    if i < len(s):\n        if s[i] != \".\":\n            return [False, 0, 0]\n        i = i + 1\n        while i < len(s) and s[i] in digits:\n            frac = frac * 10 + int(s[i])\n            places = places + 1\n            i = i + 1\n        if i < len(s) or places == 0 or places > 4:\n            return [False, 0, 0]\n    p = places\n    while p < 4:\n        frac = frac * 10\n        p = p + 1\n    return [True, sign * (whole * scale + frac), places]\n\ndef with_places(n, places):\n    sign = \"\"\n    a = n\n    if a < 0:\n        sign = \"-\"\n        a = 0 - a\n    one = 1\n    p = 0\n    while p < places:\n        one = one * 10\n        p = p + 1\n    out = str(quotient(a, one))\n    if places > 0:\n        f = str(a % one)\n        while len(f) < places:\n            f = \"0\" + f\n        out = out + \".\" + f\n    if a == 0:\n        return out\n    return sign + out\n\ndef shown(v, places):\n    unit = 1\n    p = places\n    while p < 4:\n        unit = unit * 10\n        p = p + 1\n    return with_places(rounded(v, unit), places)\n\ndef mean_text(total, count):\n    return with_places(rounded(total, 100 * count), 2)\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "7\nx\n2\n3\n4\n5\n1\n\n1\nid,name,ID\n1,a,2\n\n1\na,b\n1,2,3\n\"open,4\n\n1\ncode,note,blank\nA1,a very long note that goes on and on,\nB2,\"short, quoted\",\nC3\nD4,\"never closed,\nE5,plain,\n\n3\n9\nnosuch\n\n3\nnote\nx\n\n3\nnote\na\n4\ncode\nB2\n4\nnote\nzzz\n5\nblank\n5\n\n1\nn,item,qty\n1,item 1,7\n2,item 2,4\n3,item 3,1\n4,item 4,8\n5,item 5,5\n6,item 6,2\n7,item 7,9\n8,item 8,6\n9,item 9,3\n10,item 10,0\n11,item 11,7\n12,item 12,4\n13,item 13,1\n14,item 14,8\n15,item 15,5\n16,item 16,2\n17,item 17,9\n18,item 18,6\n19,item 19,3\n20,item 20,0\n21,item 21,7\n22,item 22,4\n23,item 23,1\n24,item 24,8\n25,item 25,5\n26,item 26,2\n27,item 27,9\n28,item 28,6\n29,item 29,3\n30,item 30,0\n4\nqty\nbig\n> x\n\n4\nqty\n!= 0\n5\nqty\n6\n",
      "screen": "\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 7\nPick a number from 1 to 6.\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> x\nPick a number from 1 to 6.\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 2\nNo table yet; load a CSV first (1).\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 3\nNo table yet; load a CSV first (1).\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 4\nNo table yet; load a CSV first (1).\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 5\nNo table yet; load a CSV first (1).\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 1\nType or paste the CSV: the header first, then one row per line (at most 30 rows); an empty line ends it.\n> \nNothing was loaded.\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 1\nType or paste the CSV: the header first, then one row per line (at most 30 rows); an empty line ends it.\n> id,name,ID\n> 1,a,2\n> \nThe header needs 1 to 8 columns, each with a name of its own; nothing was loaded.\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 1\nType or paste the CSV: the header first, then one row per line (at most 30 rows); an empty line ends it.\n> a,b\n> 1,2,3\n> \"open,4\n> \nRow 1 was skipped: it has 3 fields, the header has 2.\nRow 2 was skipped: a quote is never closed.\nThe CSV has no rows that fit the header; nothing was loaded.\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 1\nType or paste the CSV: the header first, then one row per line (at most 30 rows); an empty line ends it.\n> code,note,blank\n> A1,a very long note that goes on and on,\n> B2,\"short, quoted\",\n> C3\n> D4,\"never closed,\n> E5,plain,\n> \nRow 3 was skipped: it has 1 field, the header has 3.\nRow 4 was skipped: a quote is never closed.\nLoaded 3 rows and 3 columns: code, note and blank.\n#  code  note                  blank\n-  ----  --------------------  -----\n1  A1    a very long note ...\n2  B2    short, quoted\n5  E5    plain\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 3\ncolumn (number or name)> 9\nType a column number from 1 to 3 or a column name.\ncolumn (number or name)> nosuch\nType a column number from 1 to 3 or a column name.\ncolumn (number or name)> \nCancelled.\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 3\ncolumn (number or name)> note\norder (a = ascending, d = descending)> x\nType a or d.\norder (a = ascending, d = descending)> \nCancelled.\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 3\ncolumn (number or name)> note\norder (a = ascending, d = descending)> a\nSorted by note, ascending (text, any case; empty fields last).\n#  code  note                  blank\n-  ----  --------------------  -----\n1  A1    a very long note ...\n5  E5    plain\n2  B2    short, quoted\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 4\ncolumn (number or name)> code\ntext to look for (any case)> B2\n1 of 3 rows has 'B2' in code:\n#  code  note           blank\n-  ----  -------------  -----\n2  B2    short, quoted\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 4\ncolumn (number or name)> note\ntext to look for (any case)> zzz\nNo row has 'zzz' in note.\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 5\ncolumn (number or name)> blank\nblank: every field is empty.\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 5\ncolumn (number or name)> \nCancelled.\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 1\nType or paste the CSV: the header first, then one row per line (at most 30 rows); an empty line ends it.\n> n,item,qty\n> 1,item 1,7\n> 2,item 2,4\n> 3,item 3,1\n> 4,item 4,8\n> 5,item 5,5\n> 6,item 6,2\n> 7,item 7,9\n> 8,item 8,6\n> 9,item 9,3\n> 10,item 10,0\n> 11,item 11,7\n> 12,item 12,4\n> 13,item 13,1\n> 14,item 14,8\n> 15,item 15,5\n> 16,item 16,2\n> 17,item 17,9\n> 18,item 18,6\n> 19,item 19,3\n> 20,item 20,0\n> 21,item 21,7\n> 22,item 22,4\n> 23,item 23,1\n> 24,item 24,8\n> 25,item 25,5\n> 26,item 26,2\n> 27,item 27,9\n> 28,item 28,6\n> 29,item 29,3\n> 30,item 30,0\nThat is 30 rows, the most a table can have.\nLoaded 30 rows and 3 columns: n (numbers), item and qty (numbers).\n #   n  item     qty\n--  --  -------  ---\n 1   1  item 1     7\n 2   2  item 2     4\n 3   3  item 3     1\n 4   4  item 4     8\n 5   5  item 5     5\n 6   6  item 6     2\n 7   7  item 7     9\n 8   8  item 8     6\n 9   9  item 9     3\n10  10  item 10    0\n11  11  item 11    7\n12  12  item 12    4\n13  13  item 13    1\n14  14  item 14    8\n15  15  item 15    5\n16  16  item 16    2\n17  17  item 17    9\n18  18  item 18    6\n19  19  item 19    3\n20  20  item 20    0\n21  21  item 21    7\n22  22  item 22    4\n23  23  item 23    1\n24  24  item 24    8\n25  25  item 25    5\n26  26  item 26    2\n27  27  item 27    9\n28  28  item 28    6\n29  29  item 29    3\n30  30  item 30    0\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 4\ncolumn (number or name)> qty\ncondition (such as > 30, = 42 or != 0)> big\nType =, !=, <, <=, > or >= and a number, such as >= 30.\ncondition (such as > 30, = 42 or != 0)> > x\nType =, !=, <, <=, > or >= and a number, such as >= 30.\ncondition (such as > 30, = 42 or != 0)> \nCancelled.\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 4\ncolumn (number or name)> qty\ncondition (such as > 30, = 42 or != 0)> != 0\n27 of 30 rows have qty != 0:\n #   n  item     qty\n--  --  -------  ---\n 1   1  item 1     7\n 2   2  item 2     4\n 3   3  item 3     1\n 4   4  item 4     8\n 5   5  item 5     5\n 6   6  item 6     2\n 7   7  item 7     9\n 8   8  item 8     6\n 9   9  item 9     3\n11  11  item 11    7\n12  12  item 12    4\n13  13  item 13    1\n14  14  item 14    8\n15  15  item 15    5\n16  16  item 16    2\n17  17  item 17    9\n18  18  item 18    6\n19  19  item 19    3\n21  21  item 21    7\n22  22  item 22    4\n23  23  item 23    1\n24  24  item 24    8\n25  25  item 25    5\n26  26  item 26    2\n27  27  item 27    9\n28  28  item 28    6\n29  29  item 29    3\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 5\ncolumn (number or name)> qty\nqty: numbers - 30 values, 0 empty\n  smallest 0, largest 9, sum 135, mean 4.50\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\nname,age,city,score\n\"Smith, John\",42,Taipei,88.5\nAda,36,London,91\n\"Chen \"\"CJ\"\" Jie\",29,Taipei,77.25\nBo,,Kaohsiung,84\nEve,51,Rome,\n\n2\n3\nage\nd\n4\nscore\n>= 80\n4\ncity\ntai\n5\nscore\n5\n3\n5\nAGE\n3\ncity\na\n6\n",
      "screen": "\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 1\nType or paste the CSV: the header first, then one row per line (at most 30 rows); an empty line ends it.\n> name,age,city,score\n> \"Smith, John\",42,Taipei,88.5\n> Ada,36,London,91\n> \"Chen \"\"CJ\"\" Jie\",29,Taipei,77.25\n> Bo,,Kaohsiung,84\n> Eve,51,Rome,\n> \nLoaded 5 rows and 4 columns: name, age (numbers), city and score (numbers).\n#  name           age  city       score\n-  -------------  ---  ---------  -----\n1  Smith, John     42  Taipei      88.5\n2  Ada             36  London        91\n3  Chen \"CJ\" Jie   29  Taipei     77.25\n4  Bo                  Kaohsiung     84\n5  Eve             51  Rome\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 2\n#  name           age  city       score\n-  -------------  ---  ---------  -----\n1  Smith, John     42  Taipei      88.5\n2  Ada             36  London        91\n3  Chen \"CJ\" Jie   29  Taipei     77.25\n4  Bo                  Kaohsiung     84\n5  Eve             51  Rome\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 3\ncolumn (number or name)> age\norder (a = ascending, d = descending)> d\nSorted by age, descending (numbers; empty fields last).\n#  name           age  city       score\n-  -------------  ---  ---------  -----\n5  Eve             51  Rome\n1  Smith, John     42  Taipei      88.5\n2  Ada             36  London        91\n3  Chen \"CJ\" Jie   29  Taipei     77.25\n4  Bo                  Kaohsiung     84\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 4\ncolumn (number or name)> score\ncondition (such as > 30, = 42 or != 0)> >= 80\n3 of 5 rows have score >= 80:\n#  name         age  city       score\n-  -----------  ---  ---------  -----\n1  Smith, John   42  Taipei      88.5\n2  Ada           36  London        91\n4  Bo                Kaohsiung     84\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 4\ncolumn (number or name)> city\ntext to look for (any case)> tai\n2 of 5 rows have 'tai' in city:\n#  name           age  city    score\n-  -------------  ---  ------  -----\n1  Smith, John     42  Taipei   88.5\n3  Chen \"CJ\" Jie   29  Taipei  77.25\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 5\ncolumn (number or name)> score\nscore: numbers - 4 values, 1 empty\n  smallest 77.25, largest 91.00, sum 340.75, mean 85.19\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 5\ncolumn (number or name)> 3\ncity: text - 5 values, 0 empty, 4 different\n  most common: Taipei (2 times)\n  shortest: Rome (4 characters), longest: Kaohsiung (9 characters)\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 5\ncolumn (number or name)> AGE\nage: numbers - 4 values, 1 empty\n  smallest 29, largest 51, sum 158, mean 39.50\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 3\ncolumn (number or name)> city\norder (a = ascending, d = descending)> a\nSorted by city, ascending (text, any case; empty fields last).\n#  name           age  city       score\n-  -------------  ---  ---------  -----\n4  Bo                  Kaohsiung     84\n2  Ada             36  London        91\n5  Eve             51  Rome\n1  Smith, John     42  Taipei      88.5\n3  Chen \"CJ\" Jie   29  Taipei     77.25\n\n== CSV viewer ==\n1) load CSV  2) show table  3) sort  4) filter  5) column statistics  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "manual-csv-parser",
      "caseId": "114-manual-csv-parser",
      "title": "Manual CSV parser"
    }
  ],
  "updated": "2026-10-06"
}
