CSV viewer
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.
Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.
About
Paste a CSV and see it as a table. Sort it by a column, filter it, and get a column's statistics. Quoted fields work: a comma inside quotes is part of the field, and a doubled quote inside quotes is one quote character.
main.eml- the menu, loading, the questions and their checks, and what the screen showstable.eml- reading a CSV line, column kinds, sorting, filtering, statistics and drawing the tablefigures.eml- numbers read exactly, and how they are shown
How each part works:
- A line is read one character at a time, with one piece of state: whether the reader is inside quotes (the state machine of the corpus case
manual-csv-parser). A quote left open at the end of a line is an error. - The first line is the header. A row with a different number of fields, or with a quote never closed, is skipped with its row number and the reason; the rest are loaded. Each row keeps its number from the CSV, shown in the
#column, so it can be found again after sorting. - A column holds numbers when every field that is not empty is a number (such as
42,-3.25or0.5, at most 4 decimals). Numbers are kept as whole numbers of ten-thousandths, so sums and comparisons are exact; the mean is rounded once, to 2 decimals, halves away from zero. Other columns are text, compared without regard to case. - Sorting is an insertion sort, so rows that compare equal keep their order; empty fields go last, whichever way the sort runs.
- A filter on a number column takes
=,!=,<,<=,>or>=and a number; on a text column it keeps the rows whose field contains the text, in any case. Empty fields never pass. - Statistics: for numbers, the count, the empty fields, the smallest, the largest, the sum and the mean; for text, the count, the empty fields, how many different values, the most common (all of them when several tie), and the shortest and longest (the first row wins a tie).
- The table aligns numbers right and text left; a field longer than 20 characters is cut to 17 and
....
What is checked: a CSV is a header of 1 to 8 columns, each with a name of its own (in any case), and 1 to 30 rows that fit it; an empty line ends it, and the 31st line ends it by itself. A column is chosen by its number or its name; an order is a or d. An empty answer cancels.
Sessions: sessions/basic.in loads five people with a quoted name holding a comma, a quoted name holding doubled quotes, an empty age and an empty score, shows the table, sorts by age descending (the empty age last), filters score >= 80 and city containing tai, gives the statistics of score (mean 85.19), city (by number) and age (by name in capitals), and sorts by city - the two Taipei rows keep their order; sessions/bad-input.in uses the menu before anything is loaded, loads nothing, a header with the same name twice, rows that all fail, a mix of good rows, short rows and an unclosed quote with a note longer than 20 characters, asks for column 9 and a column that does not exist, order x, a filter that matches nothing, the statistics of an empty column, 31 lines, conditions big and > x, and != 0 over 30 rows.
Built on the verified corpus case manual-csv-parser (a CSV line split by a state machine that knows whether it is inside quotes, with doubled quotes).
Recorded sessions
What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.
bad-input
interpreter: byte-equal
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 7
Pick a number from 1 to 6.
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> x
Pick a number from 1 to 6.
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 2
No table yet; load a CSV first (1).
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 3
No table yet; load a CSV first (1).
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 4
No table yet; load a CSV first (1).
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 5
No table yet; load a CSV first (1).
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 1
Type or paste the CSV: the header first, then one row per line (at most 30 rows); an empty line ends it.
>
Nothing was loaded.
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 1
Type or paste the CSV: the header first, then one row per line (at most 30 rows); an empty line ends it.
> id,name,ID
> 1,a,2
>
The header needs 1 to 8 columns, each with a name of its own; nothing was loaded.
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 1
Type or paste the CSV: the header first, then one row per line (at most 30 rows); an empty line ends it.
> a,b
> 1,2,3
> "open,4
>
Row 1 was skipped: it has 3 fields, the header has 2.
Row 2 was skipped: a quote is never closed.
The CSV has no rows that fit the header; nothing was loaded.
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 1
Type or paste the CSV: the header first, then one row per line (at most 30 rows); an empty line ends it.
> code,note,blank
> A1,a very long note that goes on and on,
> B2,"short, quoted",
> C3
> D4,"never closed,
> E5,plain,
>
Row 3 was skipped: it has 1 field, the header has 3.
Row 4 was skipped: a quote is never closed.
Loaded 3 rows and 3 columns: code, note and blank.
# code note blank
- ---- -------------------- -----
1 A1 a very long note ...
2 B2 short, quoted
5 E5 plain
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 3
column (number or name)> 9
Type a column number from 1 to 3 or a column name.
column (number or name)> nosuch
Type a column number from 1 to 3 or a column name.
column (number or name)>
Cancelled.
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 3
column (number or name)> note
order (a = ascending, d = descending)> x
Type a or d.
order (a = ascending, d = descending)>
Cancelled.
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 3
column (number or name)> note
order (a = ascending, d = descending)> a
Sorted by note, ascending (text, any case; empty fields last).
# code note blank
- ---- -------------------- -----
1 A1 a very long note ...
5 E5 plain
2 B2 short, quoted
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 4
column (number or name)> code
text to look for (any case)> B2
1 of 3 rows has 'B2' in code:
# code note blank
- ---- ------------- -----
2 B2 short, quoted
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 4
column (number or name)> note
text to look for (any case)> zzz
No row has 'zzz' in note.
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 5
column (number or name)> blank
blank: every field is empty.
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 5
column (number or name)>
Cancelled.
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 1
Type or paste the CSV: the header first, then one row per line (at most 30 rows); an empty line ends it.
> n,item,qty
> 1,item 1,7
> 2,item 2,4
> 3,item 3,1
> 4,item 4,8
> 5,item 5,5
> 6,item 6,2
> 7,item 7,9
> 8,item 8,6
> 9,item 9,3
> 10,item 10,0
> 11,item 11,7
> 12,item 12,4
> 13,item 13,1
> 14,item 14,8
> 15,item 15,5
> 16,item 16,2
> 17,item 17,9
> 18,item 18,6
> 19,item 19,3
> 20,item 20,0
> 21,item 21,7
> 22,item 22,4
> 23,item 23,1
> 24,item 24,8
> 25,item 25,5
> 26,item 26,2
> 27,item 27,9
> 28,item 28,6
> 29,item 29,3
> 30,item 30,0
That is 30 rows, the most a table can have.
Loaded 30 rows and 3 columns: n (numbers), item and qty (numbers).
# n item qty
-- -- ------- ---
1 1 item 1 7
2 2 item 2 4
3 3 item 3 1
4 4 item 4 8
5 5 item 5 5
6 6 item 6 2
7 7 item 7 9
8 8 item 8 6
9 9 item 9 3
10 10 item 10 0
11 11 item 11 7
12 12 item 12 4
13 13 item 13 1
14 14 item 14 8
15 15 item 15 5
16 16 item 16 2
17 17 item 17 9
18 18 item 18 6
19 19 item 19 3
20 20 item 20 0
21 21 item 21 7
22 22 item 22 4
23 23 item 23 1
24 24 item 24 8
25 25 item 25 5
26 26 item 26 2
27 27 item 27 9
28 28 item 28 6
29 29 item 29 3
30 30 item 30 0
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 4
column (number or name)> qty
condition (such as > 30, = 42 or != 0)> big
Type =, !=, <, <=, > or >= and a number, such as >= 30.
condition (such as > 30, = 42 or != 0)> > x
Type =, !=, <, <=, > or >= and a number, such as >= 30.
condition (such as > 30, = 42 or != 0)>
Cancelled.
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 4
column (number or name)> qty
condition (such as > 30, = 42 or != 0)> != 0
27 of 30 rows have qty != 0:
# n item qty
-- -- ------- ---
1 1 item 1 7
2 2 item 2 4
3 3 item 3 1
4 4 item 4 8
5 5 item 5 5
6 6 item 6 2
7 7 item 7 9
8 8 item 8 6
9 9 item 9 3
11 11 item 11 7
12 12 item 12 4
13 13 item 13 1
14 14 item 14 8
15 15 item 15 5
16 16 item 16 2
17 17 item 17 9
18 18 item 18 6
19 19 item 19 3
21 21 item 21 7
22 22 item 22 4
23 23 item 23 1
24 24 item 24 8
25 25 item 25 5
26 26 item 26 2
27 27 item 27 9
28 28 item 28 6
29 29 item 29 3
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 5
column (number or name)> qty
qty: numbers - 30 values, 0 empty
smallest 0, largest 9, sum 135, mean 4.50
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 6
Bye.
What was typed (89 lines)
7
x
2
3
4
5
1
1
id,name,ID
1,a,2
1
a,b
1,2,3
"open,4
1
code,note,blank
A1,a very long note that goes on and on,
B2,"short, quoted",
C3
D4,"never closed,
E5,plain,
3
9
nosuch
3
note
x
3
note
a
4
code
B2
4
note
zzz
5
blank
5
1
n,item,qty
1,item 1,7
2,item 2,4
3,item 3,1
4,item 4,8
5,item 5,5
6,item 6,2
7,item 7,9
8,item 8,6
9,item 9,3
10,item 10,0
11,item 11,7
12,item 12,4
13,item 13,1
14,item 14,8
15,item 15,5
16,item 16,2
17,item 17,9
18,item 18,6
19,item 19,3
20,item 20,0
21,item 21,7
22,item 22,4
23,item 23,1
24,item 24,8
25,item 25,5
26,item 26,2
27,item 27,9
28,item 28,6
29,item 29,3
30,item 30,0
4
qty
big
> x
4
qty
!= 0
5
qty
6
basic
interpreter: byte-equal
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 1
Type or paste the CSV: the header first, then one row per line (at most 30 rows); an empty line ends it.
> name,age,city,score
> "Smith, John",42,Taipei,88.5
> Ada,36,London,91
> "Chen ""CJ"" Jie",29,Taipei,77.25
> Bo,,Kaohsiung,84
> Eve,51,Rome,
>
Loaded 5 rows and 4 columns: name, age (numbers), city and score (numbers).
# name age city score
- ------------- --- --------- -----
1 Smith, John 42 Taipei 88.5
2 Ada 36 London 91
3 Chen "CJ" Jie 29 Taipei 77.25
4 Bo Kaohsiung 84
5 Eve 51 Rome
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 2
# name age city score
- ------------- --- --------- -----
1 Smith, John 42 Taipei 88.5
2 Ada 36 London 91
3 Chen "CJ" Jie 29 Taipei 77.25
4 Bo Kaohsiung 84
5 Eve 51 Rome
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 3
column (number or name)> age
order (a = ascending, d = descending)> d
Sorted by age, descending (numbers; empty fields last).
# name age city score
- ------------- --- --------- -----
5 Eve 51 Rome
1 Smith, John 42 Taipei 88.5
2 Ada 36 London 91
3 Chen "CJ" Jie 29 Taipei 77.25
4 Bo Kaohsiung 84
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 4
column (number or name)> score
condition (such as > 30, = 42 or != 0)> >= 80
3 of 5 rows have score >= 80:
# name age city score
- ----------- --- --------- -----
1 Smith, John 42 Taipei 88.5
2 Ada 36 London 91
4 Bo Kaohsiung 84
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 4
column (number or name)> city
text to look for (any case)> tai
2 of 5 rows have 'tai' in city:
# name age city score
- ------------- --- ------ -----
1 Smith, John 42 Taipei 88.5
3 Chen "CJ" Jie 29 Taipei 77.25
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 5
column (number or name)> score
score: numbers - 4 values, 1 empty
smallest 77.25, largest 91.00, sum 340.75, mean 85.19
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 5
column (number or name)> 3
city: text - 5 values, 0 empty, 4 different
most common: Taipei (2 times)
shortest: Rome (4 characters), longest: Kaohsiung (9 characters)
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 5
column (number or name)> AGE
age: numbers - 4 values, 1 empty
smallest 29, largest 51, sum 158, mean 39.50
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 3
column (number or name)> city
order (a = ascending, d = descending)> a
Sorted by city, ascending (text, any case; empty fields last).
# name age city score
- ------------- --- --------- -----
4 Bo Kaohsiung 84
2 Ada 36 London 91
5 Eve 51 Rome
1 Smith, John 42 Taipei 88.5
3 Chen "CJ" Jie 29 Taipei 77.25
== CSV viewer ==
1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit
choice> 6
Bye.
What was typed (28 lines)
1
name,age,city,score
"Smith, John",42,Taipei,88.5
Ada,36,London,91
"Chen ""CJ"" Jie",29,Taipei,77.25
Bo,,Kaohsiung,84
Eve,51,Rome,
2
3
age
d
4
score
>= 80
4
city
tai
5
score
5
3
5
AGE
3
city
a
6
Modules
The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.
main.eml(entry)
eml# P027 CSV viewer: paste a CSV - quoted fields included - and see it as a
# table, sort it by a column, filter it, and get a column's statistics.
import table
import figures
30 => most_rows
8 => most_columns
def trim(s):
0 => i
len(s) => j
while i < j and s[i] == " ":
i + 1 => i
while j > i and s[j - 1] == " ":
j - 1 => j
return s[i:j]
def all_digits(s):
if s == "" or len(s) > 3:
return False
for c in s:
if not (c in "0123456789"):
return False
return True
def listed(items):
"" => out
0 => i
while i < len(items):
if i > 0 and i == len(items) - 1:
out + " and " => out
elif i > 0:
out + ", " => out
out + items[i] => out
i + 1 => i
return out
def plural(n, word):
if n == 1:
return "1 " + word
return str(n) + " " + word + "s"
def load():
# [header, rows, kinds] from typed lines, or [] when nothing was loaded.
("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
[] => lines
True => reading
while reading and len(lines) < most_rows + 1:
input("> ") => line
if trim(line) == "":
False => reading
else:
lines + [line] => lines
if reading:
("That is " + str(most_rows) + " rows, the most a table can have.") ^0
if len(lines) == 0:
"Nothing was loaded." ^0
return []
table.parse_line(lines[0]) => h
[] => header
True => good
if not h[0] or len(h[1]) > most_columns:
False => good
else:
for name in h[1]:
trim(name) => name
for other in header:
if table.lower(other) == table.lower(name):
False => good
if name == "":
False => good
header + [name] => header
if not good:
("The header needs 1 to " + str(most_columns) + " columns, each with a name of its own; nothing was loaded.") ^0
return []
[] => rows
for i in [1:len(lines) - 1]:
table.parse_line(lines[i]) => p
if not p[0]:
("Row " + str(i) + " was skipped: " + p[1] + ".") ^0
elif len(p[1]) != len(header):
("Row " + str(i) + " was skipped: it has " + plural(len(p[1]), "field") + ", the header has " + str(len(header)) + ".") ^0
else:
rows + [[i, p[1]]] => rows
if len(rows) == 0:
"The CSV has no rows that fit the header; nothing was loaded." ^0
return []
[] => kinds
[] => names
for k in [0:len(header) - 1]:
table.kind_of(rows, k) => kind
kinds + [kind] => kinds
if kind[0] == "number":
names + [header[k] + " (numbers)"] => names
else:
names + [header[k]] => names
("Loaded " + plural(len(rows), "row") + " and " + plural(len(header), "column") + ": " + listed(names) + ".") ^0
return [header, rows, kinds]
def ask_column(header):
# A column's index, or -1 when the answer is empty, which cancels.
while True:
trim(input("column (number or name)> ")) => answer
if answer == "":
return 0 - 1
if all_digits(answer) and int(answer) >= 1 and int(answer) <= len(header):
return int(answer) - 1
for k in [0:len(header) - 1]:
if table.lower(header[k]) == table.lower(answer):
return k
("Type a column number from 1 to " + str(len(header)) + " or a column name.") ^0
def show(data, rows):
for line in table.draw(data[0], rows, data[2]):
line ^0
def sort(data):
# Returns the data with its rows in the new order, or unchanged.
ask_column(data[0]) => k
if k == 0 - 1:
"Cancelled." ^0
return data
"" => order
while order == "":
table.lower(trim(input("order (a = ascending, d = descending)> "))) => answer
if answer == "":
"Cancelled." ^0
return data
if answer == "a" or answer == "d":
answer => order
else:
"Type a or d." ^0
data[2][k] => kind
table.sorted_rows(data[1], k, kind[0], order == "d") => rows
"ascending" => way
if order == "d":
"descending" => way
"text, any case" => how
if kind[0] == "number":
"numbers" => how
("Sorted by " + data[0][k] + ", " + way + " (" + how + "; empty fields last).") ^0
[data[0], rows, data[2]] => sorted_data
show(sorted_data, rows)
return sorted_data
def filter_rows(data):
ask_column(data[0]) => k
if k == 0 - 1:
"Cancelled." ^0
return
data[2][k] => kind
"" => op
"" => target
"" => wording
if kind[0] == "number":
while op == "":
trim(input("condition (such as > 30, = 42 or != 0)> ")) => answer
if answer == "":
"Cancelled." ^0
return
table.condition(answer) => c
if c[0]:
c[1] => op
c[2] => target
(data[0][k] + " " + op + " " + c[3]) => wording
else:
"Type =, !=, <, <=, > or >= and a number, such as >= 30." ^0
else:
while op == "":
trim(input("text to look for (any case)> ")) => answer
if answer == "":
"Cancelled." ^0
return
"has" => op
table.lower(answer) => target
("'" + answer + "' in " + data[0][k]) => wording
[] => kept
for r in data[1]:
if table.keeps(r[1][k], kind[0], op, target):
kept + [r] => kept
if len(kept) == 0:
("No row has " + wording + ".") ^0
else:
"have" => verb
if len(kept) == 1:
"has" => verb
(str(len(kept)) + " of " + plural(len(data[1]), "row") + " " + verb + " " + wording + ":") ^0
show(data, kept)
def stats(data):
ask_column(data[0]) => k
if k == 0 - 1:
"Cancelled." ^0
return
data[0][k] => name
data[2][k] => kind
if kind[0] == "number":
table.number_stats(data[1], k) => s
(name + ": numbers - " + plural(s[0], "value") + ", " + str(s[1]) + " empty") ^0
(" 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
return
table.text_stats(data[1], k) => s
if s[0] == 0:
(name + ": every field is empty.") ^0
return
(name + ": text - " + plural(s[0], "value") + ", " + str(s[1]) + " empty, " + str(len(s[2])) + " different") ^0
0 => most
for c in s[3]:
if c > most:
c => most
if most == 1:
" every value appears once" ^0
else:
[] => top
for i in [0:len(s[2]) - 1]:
if s[3][i] == most:
top + [s[2][i]] => top
if len(top) == 1:
(" most common: " + top[0] + " (" + str(most) + " times)") ^0
else:
(" most common: " + listed(top) + " (" + str(most) + " times each)") ^0
(" shortest: " + s[4] + " (" + plural(len(s[4]), "character") + "), longest: " + s[5] + " (" + plural(len(s[5]), "character") + ")") ^0
[] => data
True => running
while running:
"" ^0
"== CSV viewer ==" ^0
"1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit" ^0
trim(input("choice> ")) => choice
if choice == "1":
load() => loaded
if len(loaded) > 0:
loaded => data
show(data, data[1])
elif choice == "6":
False => running
elif choice == "2" or choice == "3" or choice == "4" or choice == "5":
if len(data) == 0:
"No table yet; load a CSV first (1)." ^0
elif choice == "2":
show(data, data[1])
elif choice == "3":
sort(data) => data
elif choice == "4":
filter_rows(data)
else:
stats(data)
else:
"Pick a number from 1 to 6." ^0
"Bye." ^0
Python projection (main.py)
import table
import figures
most_rows = 30
most_columns = 8
def trim(s):
i = 0
j = len(s)
while i < j and s[i] == " ":
i = i + 1
while j > i and s[j - 1] == " ":
j = j - 1
return s[i:j]
def all_digits(s):
if s == "" or len(s) > 3:
return False
for c in s:
if not c in "0123456789":
return False
return True
def listed(items):
out = ""
i = 0
while i < len(items):
if i > 0 and i == len(items) - 1:
out = out + " and "
elif i > 0:
out = out + ", "
out = out + items[i]
i = i + 1
return out
def plural(n, word):
if n == 1:
return "1 " + word
return str(n) + " " + word + "s"
def load():
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.")
lines = []
reading = True
while reading and len(lines) < most_rows + 1:
line = input("> ")
if trim(line) == "":
reading = False
else:
lines = lines + [line]
if reading:
print("That is " + str(most_rows) + " rows, the most a table can have.")
if len(lines) == 0:
print("Nothing was loaded.")
return []
h = table.parse_line(lines[0])
header = []
good = True
if not h[0] or len(h[1]) > most_columns:
good = False
else:
for name in h[1]:
name = trim(name)
for other in header:
if table.lower(other) == table.lower(name):
good = False
if name == "":
good = False
header = header + [name]
if not good:
print("The header needs 1 to " + str(most_columns) + " columns, each with a name of its own; nothing was loaded.")
return []
rows = []
for i in range(1, len(lines)):
p = table.parse_line(lines[i])
if not p[0]:
print("Row " + str(i) + " was skipped: " + p[1] + ".")
elif len(p[1]) != len(header):
print("Row " + str(i) + " was skipped: it has " + plural(len(p[1]), "field") + ", the header has " + str(len(header)) + ".")
else:
rows = rows + [[i, p[1]]]
if len(rows) == 0:
print("The CSV has no rows that fit the header; nothing was loaded.")
return []
kinds = []
names = []
for k in range(0, len(header)):
kind = table.kind_of(rows, k)
kinds = kinds + [kind]
if kind[0] == "number":
names = names + [header[k] + " (numbers)"]
else:
names = names + [header[k]]
print("Loaded " + plural(len(rows), "row") + " and " + plural(len(header), "column") + ": " + listed(names) + ".")
return [header, rows, kinds]
def ask_column(header):
while True:
answer = trim(input("column (number or name)> "))
if answer == "":
return 0 - 1
if all_digits(answer) and int(answer) >= 1 and int(answer) <= len(header):
return int(answer) - 1
for k in range(0, len(header)):
if table.lower(header[k]) == table.lower(answer):
return k
print("Type a column number from 1 to " + str(len(header)) + " or a column name.")
def show(data, rows):
for line in table.draw(data[0], rows, data[2]):
print(line)
def sort(data):
k = ask_column(data[0])
if k == 0 - 1:
print("Cancelled.")
return data
order = ""
while order == "":
answer = table.lower(trim(input("order (a = ascending, d = descending)> ")))
if answer == "":
print("Cancelled.")
return data
if answer == "a" or answer == "d":
order = answer
else:
print("Type a or d.")
kind = data[2][k]
rows = table.sorted_rows(data[1], k, kind[0], order == "d")
way = "ascending"
if order == "d":
way = "descending"
how = "text, any case"
if kind[0] == "number":
how = "numbers"
print("Sorted by " + data[0][k] + ", " + way + " (" + how + "; empty fields last).")
sorted_data = [data[0], rows, data[2]]
show(sorted_data, rows)
return sorted_data
def filter_rows(data):
k = ask_column(data[0])
if k == 0 - 1:
print("Cancelled.")
return
kind = data[2][k]
op = ""
target = ""
wording = ""
if kind[0] == "number":
while op == "":
answer = trim(input("condition (such as > 30, = 42 or != 0)> "))
if answer == "":
print("Cancelled.")
return
c = table.condition(answer)
if c[0]:
op = c[1]
target = c[2]
wording = data[0][k] + " " + op + " " + c[3]
else:
print("Type =, !=, <, <=, > or >= and a number, such as >= 30.")
else:
while op == "":
answer = trim(input("text to look for (any case)> "))
if answer == "":
print("Cancelled.")
return
op = "has"
target = table.lower(answer)
wording = "'" + answer + "' in " + data[0][k]
kept = []
for r in data[1]:
if table.keeps(r[1][k], kind[0], op, target):
kept = kept + [r]
if len(kept) == 0:
print("No row has " + wording + ".")
else:
verb = "have"
if len(kept) == 1:
verb = "has"
print(str(len(kept)) + " of " + plural(len(data[1]), "row") + " " + verb + " " + wording + ":")
show(data, kept)
def stats(data):
k = ask_column(data[0])
if k == 0 - 1:
print("Cancelled.")
return
name = data[0][k]
kind = data[2][k]
if kind[0] == "number":
s = table.number_stats(data[1], k)
print(name + ": numbers - " + plural(s[0], "value") + ", " + str(s[1]) + " empty")
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]))
return
s = table.text_stats(data[1], k)
if s[0] == 0:
print(name + ": every field is empty.")
return
print(name + ": text - " + plural(s[0], "value") + ", " + str(s[1]) + " empty, " + str(len(s[2])) + " different")
most = 0
for c in s[3]:
if c > most:
most = c
if most == 1:
print(" every value appears once")
else:
top = []
for i in range(0, len(s[2])):
if s[3][i] == most:
top = top + [s[2][i]]
if len(top) == 1:
print(" most common: " + top[0] + " (" + str(most) + " times)")
else:
print(" most common: " + listed(top) + " (" + str(most) + " times each)")
print(" shortest: " + s[4] + " (" + plural(len(s[4]), "character") + "), longest: " + s[5] + " (" + plural(len(s[5]), "character") + ")")
data = []
running = True
while running:
print("")
print("== CSV viewer ==")
print("1) load CSV 2) show table 3) sort 4) filter 5) column statistics 6) quit")
choice = trim(input("choice> "))
if choice == "1":
loaded = load()
if len(loaded) > 0:
data = loaded
show(data, data[1])
elif choice == "6":
running = False
elif choice == "2" or choice == "3" or choice == "4" or choice == "5":
if len(data) == 0:
print("No table yet; load a CSV first (1).")
elif choice == "2":
show(data, data[1])
elif choice == "3":
data = sort(data)
elif choice == "4":
filter_rows(data)
else:
stats(data)
else:
print("Pick a number from 1 to 6.")
print("Bye.")
table.eml
eml# P027 CSV viewer - reading CSV rows and what the table can do: column
# kinds, sorting, filtering, statistics and drawing. A row is
# [row number, fields]; the row number is its place in the CSV as loaded.
import figures
def parse_line(line):
# [True, fields] or [False, reason]. A field in double quotes may hold
# commas, and "" inside quotes is one quote character - the state
# machine of the corpus case manual-csv-parser.
[] => fields
"" => current
False => quoted
0 => i
while i < len(line):
line[i] => c
if c == "\"":
if quoted and i + 1 < len(line) and line[i + 1] == "\"":
current + "\"" => current
i + 1 => i
else:
not quoted => quoted
elif c == "," and not quoted:
fields + [current] => fields
"" => current
else:
current + c => current
i + 1 => i
if quoted:
return [False, "a quote is never closed"]
fields + [current] => fields
return [True, fields]
def lower(s):
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" => upper
"abcdefghijklmnopqrstuvwxyz" => small
"" => out
for c in s:
0 => k
while k < 26 and upper[k] != c:
k + 1 => k
if k < 26:
out + small[k] => out
else:
out + c => out
return out
def contains(s, part):
# True when part occurs in s.
0 => i
while i + len(part) <= len(s):
if s[i:i + len(part)] == part:
return True
i + 1 => i
return False
def kind_of(rows, k):
# ["number", decimals] when every non-empty field of column k is a number
# and at least one is; ["text", 0] otherwise.
0 => places
0 => seen
for r in rows:
r[1][k] => f
if f != "":
figures.value_of(f) => v
if not v[0]:
return ["text", 0]
if v[2] > places:
v[2] => places
seen + 1 => seen
if seen == 0:
return ["text", 0]
return ["number", places]
def goes_before(a, b, k, kind, descending):
# True when row a belongs strictly before row b. Empty fields always go
# last; equal fields keep their order.
a[1][k] => x
b[1][k] => y
if x == "" or y == "":
return x != "" and y == ""
if kind == "number":
figures.value_of(x)[1] => vx
figures.value_of(y)[1] => vy
else:
lower(x) => vx
lower(y) => vy
if descending:
return vx > vy
return vx < vy
def sorted_rows(rows, k, kind, descending):
# Insertion sort: each row goes in after every row it does not belong
# before, so the sort is stable.
[] => out
for r in rows:
len(out) => j
while j > 0 and goes_before(r, out[j - 1], k, kind, descending):
j - 1 => j
out[0:j] + [r] + out[j:len(out)] => out
return out
def condition(s):
# [True, operator, ten-thousandths, the number as typed] for "> 30",
# "<=2.5", "!= 0" ...; [False, "", 0, ""] otherwise.
0 => i
while i < len(s) and s[i] == " ":
i + 1 => i
s[i:len(s)] => s
"" => op
for o in [">=", "<=", "!=", ">", "<", "="]:
if op == "" and s[0:len(o)] == o:
o => op
if op == "":
return [False, "", 0, ""]
s[len(op):len(s)] => rest
0 => j
while j < len(rest) and rest[j] == " ":
j + 1 => j
len(rest) => e
while e > j and rest[e - 1] == " ":
e - 1 => e
figures.value_of(rest[j:e]) => v
if not v[0]:
return [False, "", 0, ""]
return [True, op, v[1], rest[j:e]]
def keeps(field, kind, op, target):
# Whether a field passes the filter. Empty fields never pass.
if field == "":
return False
if kind == "text":
return contains(lower(field), target)
figures.value_of(field)[1] => v
if op == "=":
return v == target
if op == "!=":
return v != target
if op == "<":
return v < target
if op == "<=":
return v <= target
if op == ">":
return v > target
return v >= target
def number_stats(rows, k):
# [count, empty, smallest, its text, largest, its text, total]; the
# first row wins a tie for smallest or largest.
0 => count
0 => empty
0 => total
0 => low
"" => low_text
0 => high
"" => high_text
for r in rows:
r[1][k] => f
if f == "":
empty + 1 => empty
else:
figures.value_of(f)[1] => v
if count == 0 or v < low:
v => low
f => low_text
if count == 0 or v > high:
v => high
f => high_text
total + v => total
count + 1 => count
return [count, empty, low, low_text, high, high_text, total]
def text_stats(rows, k):
# [count, empty, distinct values in first-seen order, their counts,
# shortest, longest]; the first row wins a tie for shortest or longest.
0 => count
0 => empty
[] => seen
{} => times
"" => shortest
"" => longest
for r in rows:
r[1][k] => f
if f == "":
empty + 1 => empty
else:
if f in times:
times[f] + 1 => times[f]
else:
1 => times[f]
seen + [f] => seen
if count == 0 or len(f) < len(shortest):
f => shortest
if count == 0 or len(f) > len(longest):
f => longest
count + 1 => count
[] => counts
for v in seen:
counts + [times[v]] => counts
return [count, empty, seen, counts, shortest, longest]
def cell(s):
# A field as shown: at most 20 characters, longer ones cut with "...".
if len(s) > 20:
return s[0:17] + "..."
return s
def pad(s, width, right):
while len(s) < width:
if right:
" " + s => s
else:
s + " " => s
return s
def without_trailing(s):
len(s) => e
while e > 0 and s[e - 1] == " ":
e - 1 => e
return s[0:e]
def draw(header, rows, kinds):
# The rows as lines of text: a # column with each row's number, then
# the columns, numbers aligned right and text left.
1 => num_width
for r in rows:
if len(str(r[0])) > num_width:
len(str(r[0])) => num_width
[] => widths
for k in [0:len(header) - 1]:
len(header[k]) => w
for r in rows:
if len(cell(r[1][k])) > w:
len(cell(r[1][k])) => w
widths + [w] => widths
pad("#", num_width, True) => top
"-" * num_width => rule
for k in [0:len(header) - 1]:
top + " " + pad(header[k], widths[k], kinds[k][0] == "number") => top
rule + " " + "-" * widths[k] => rule
[without_trailing(top), rule] => lines
for r in rows:
pad(str(r[0]), num_width, True) => line
for k in [0:len(header) - 1]:
line + " " + pad(cell(r[1][k]), widths[k], kinds[k][0] == "number") => line
lines + [without_trailing(line)] => lines
return lines
Python projection (table.py)
import figures
def parse_line(line):
fields = []
current = ""
quoted = False
i = 0
while i < len(line):
c = line[i]
if c == "\"":
if quoted and i + 1 < len(line) and line[i + 1] == "\"":
current = current + "\""
i = i + 1
else:
quoted = not quoted
elif c == "," and not quoted:
fields = fields + [current]
current = ""
else:
current = current + c
i = i + 1
if quoted:
return [False, "a quote is never closed"]
fields = fields + [current]
return [True, fields]
def lower(s):
upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
small = "abcdefghijklmnopqrstuvwxyz"
out = ""
for c in s:
k = 0
while k < 26 and upper[k] != c:
k = k + 1
if k < 26:
out = out + small[k]
else:
out = out + c
return out
def contains(s, part):
i = 0
while i + len(part) <= len(s):
if s[i:i + len(part)] == part:
return True
i = i + 1
return False
def kind_of(rows, k):
places = 0
seen = 0
for r in rows:
f = r[1][k]
if f != "":
v = figures.value_of(f)
if not v[0]:
return ["text", 0]
if v[2] > places:
places = v[2]
seen = seen + 1
if seen == 0:
return ["text", 0]
return ["number", places]
def goes_before(a, b, k, kind, descending):
x = a[1][k]
y = b[1][k]
if x == "" or y == "":
return x != "" and y == ""
if kind == "number":
vx = figures.value_of(x)[1]
vy = figures.value_of(y)[1]
else:
vx = lower(x)
vy = lower(y)
if descending:
return vx > vy
return vx < vy
def sorted_rows(rows, k, kind, descending):
out = []
for r in rows:
j = len(out)
while j > 0 and goes_before(r, out[j - 1], k, kind, descending):
j = j - 1
out = out[0:j] + [r] + out[j:len(out)]
return out
def condition(s):
i = 0
while i < len(s) and s[i] == " ":
i = i + 1
s = s[i:len(s)]
op = ""
for o in [">=", "<=", "!=", ">", "<", "="]:
if op == "" and s[0:len(o)] == o:
op = o
if op == "":
return [False, "", 0, ""]
rest = s[len(op):len(s)]
j = 0
while j < len(rest) and rest[j] == " ":
j = j + 1
e = len(rest)
while e > j and rest[e - 1] == " ":
e = e - 1
v = figures.value_of(rest[j:e])
if not v[0]:
return [False, "", 0, ""]
return [True, op, v[1], rest[j:e]]
def keeps(field, kind, op, target):
if field == "":
return False
if kind == "text":
return contains(lower(field), target)
v = figures.value_of(field)[1]
if op == "=":
return v == target
if op == "!=":
return v != target
if op == "<":
return v < target
if op == "<=":
return v <= target
if op == ">":
return v > target
return v >= target
def number_stats(rows, k):
count = 0
empty = 0
total = 0
low = 0
low_text = ""
high = 0
high_text = ""
for r in rows:
f = r[1][k]
if f == "":
empty = empty + 1
else:
v = figures.value_of(f)[1]
if count == 0 or v < low:
low = v
low_text = f
if count == 0 or v > high:
high = v
high_text = f
total = total + v
count = count + 1
return [count, empty, low, low_text, high, high_text, total]
def text_stats(rows, k):
count = 0
empty = 0
seen = []
times = {}
shortest = ""
longest = ""
for r in rows:
f = r[1][k]
if f == "":
empty = empty + 1
else:
if f in times:
times[f] = times[f] + 1
else:
times[f] = 1
seen = seen + [f]
if count == 0 or len(f) < len(shortest):
shortest = f
if count == 0 or len(f) > len(longest):
longest = f
count = count + 1
counts = []
for v in seen:
counts = counts + [times[v]]
return [count, empty, seen, counts, shortest, longest]
def cell(s):
if len(s) > 20:
return s[0:17] + "..."
return s
def pad(s, width, right):
while len(s) < width:
if right:
s = " " + s
else:
s = s + " "
return s
def without_trailing(s):
e = len(s)
while e > 0 and s[e - 1] == " ":
e = e - 1
return s[0:e]
def draw(header, rows, kinds):
num_width = 1
for r in rows:
if len(str(r[0])) > num_width:
num_width = len(str(r[0]))
widths = []
for k in range(0, len(header)):
w = len(header[k])
for r in rows:
if len(cell(r[1][k])) > w:
w = len(cell(r[1][k]))
widths = widths + [w]
top = pad("#", num_width, True)
rule = "-" * num_width
for k in range(0, len(header)):
top = top + " " + pad(header[k], widths[k], kinds[k][0] == "number")
rule = rule + " " + "-" * widths[k]
lines = [without_trailing(top), rule]
for r in rows:
line = pad(str(r[0]), num_width, True)
for k in range(0, len(header)):
line = line + " " + pad(cell(r[1][k]), widths[k], kinds[k][0] == "number")
lines = lines + [without_trailing(line)]
return lines
figures.eml
eml# P027 CSV viewer - numbers in a column, read exactly. A number such as
# -12.5 is kept as a whole number of ten-thousandths (-125000), so sums and
# comparisons are exact, and a mean is rounded once, when it is shown.
"0123456789" => digits
10000 => scale
def quotient(a, b):
# a divided by b, rounded down, for a >= 0 and b > 0.
return int((a - a % b) / b)
def rounded(a, b):
# a / b rounded to a whole number, halves away from zero; b > 0.
if a < 0:
return 0 - quotient(0 - 2 * a + b, 2 * b)
return quotient(2 * a + b, 2 * b)
def value_of(s):
# [True, ten-thousandths, decimals] for "42", "-3.25" or "0.5" (at most
# 4 decimals and 12 digits before the point); [False, 0, 0] otherwise.
1 => sign
0 => i
if len(s) > 0 and s[0] == "-":
0 - 1 => sign
1 => i
0 => whole
i => start
while i < len(s) and s[i] in digits:
whole * 10 + int(s[i]) => whole
i + 1 => i
if i == start or i - start > 12:
return [False, 0, 0]
0 => frac
0 => places
if i < len(s):
if s[i] != ".":
return [False, 0, 0]
i + 1 => i
while i < len(s) and s[i] in digits:
frac * 10 + int(s[i]) => frac
places + 1 => places
i + 1 => i
if i < len(s) or places == 0 or places > 4:
return [False, 0, 0]
places => p
while p < 4:
frac * 10 => frac
p + 1 => p
return [True, sign * (whole * scale + frac), places]
def with_places(n, places):
# A whole number of units of 10^-places as a decimal: 1250, 2 -> "12.50".
"" => sign
n => a
if a < 0:
"-" => sign
0 - a => a
1 => one
0 => p
while p < places:
one * 10 => one
p + 1 => p
str(quotient(a, one)) => out
if places > 0:
str(a % one) => f
while len(f) < places:
"0" + f => f
out + "." + f => out
if a == 0:
return out
return sign + out
def shown(v, places):
# Ten-thousandths shown with `places` decimals (0 to 4); the values a
# column holds always fit its own number of places exactly.
1 => unit
places => p
while p < 4:
unit * 10 => unit
p + 1 => p
return with_places(rounded(v, unit), places)
def mean_text(total, count):
# The mean of values adding up to `total` ten-thousandths, to 2 decimals,
# rounded once, halves away from zero.
return with_places(rounded(total, 100 * count), 2)
Python projection (figures.py)
digits = "0123456789"
scale = 10000
def quotient(a, b):
return int((a - a % b) / b)
def rounded(a, b):
if a < 0:
return 0 - quotient(0 - 2 * a + b, 2 * b)
return quotient(2 * a + b, 2 * b)
def value_of(s):
sign = 1
i = 0
if len(s) > 0 and s[0] == "-":
sign = 0 - 1
i = 1
whole = 0
start = i
while i < len(s) and s[i] in digits:
whole = whole * 10 + int(s[i])
i = i + 1
if i == start or i - start > 12:
return [False, 0, 0]
frac = 0
places = 0
if i < len(s):
if s[i] != ".":
return [False, 0, 0]
i = i + 1
while i < len(s) and s[i] in digits:
frac = frac * 10 + int(s[i])
places = places + 1
i = i + 1
if i < len(s) or places == 0 or places > 4:
return [False, 0, 0]
p = places
while p < 4:
frac = frac * 10
p = p + 1
return [True, sign * (whole * scale + frac), places]
def with_places(n, places):
sign = ""
a = n
if a < 0:
sign = "-"
a = 0 - a
one = 1
p = 0
while p < places:
one = one * 10
p = p + 1
out = str(quotient(a, one))
if places > 0:
f = str(a % one)
while len(f) < places:
f = "0" + f
out = out + "." + f
if a == 0:
return out
return sign + out
def shown(v, places):
unit = 1
p = places
while p < 4:
unit = unit * 10
p = p + 1
return with_places(rounded(v, unit), places)
def mean_text(total, count):
return with_places(rounded(total, 100 * count), 2)