Contacts
Add, find, edit and delete contacts, kept in name order by binary search; find by part of a name or part of a number, with every phone number checked - from a text menu.
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
Adds contacts with a name and a phone number, finds them by part of a name or part of a number, edits and deletes them. The book is always in name order, ignoring case. A text menu; the contacts live while the program runs.
main.eml- the menu loop and the checks on what is typedbook.eml- the contact book: a binary search finds where a contact belongs, so a new or renamed contact goes straight into its place and the list never needs sorting; the same search finds a name that is already theretext.eml- trimming, lower case, the digits of a number, and the phone check
Finding: a fragment with a digit in it is looked for in the numbers, by digits only, so 0912 345 finds 0912-345-678; any other fragment is looked for in the names, ignoring case.
What is checked: a name is not empty, has at most 30 characters and is not already in the book in any case; a phone number has only digits, spaces and dashes, a + only at the start, and 7 to 15 digits. When editing, Enter keeps the current name or number, and an edit that fails a check changes nothing.
Sessions: sessions/basic.in adds four contacts out of order, lists them, finds by a name fragment, a spaced number fragment and a fragment two numbers share, renames one (it moves to its new place), changes a number and deletes one; sessions/bad-input.in types an unknown choice, an empty search, an empty and a too-long name, phone numbers with letters, too few and too many digits and a + in the middle, a name that differs only in case, ids that are not there, and an edit with a bad number.
Built on the verified corpus cases contact-book (contacts kept and looked up by name) and binary-search (halving the part of a sorted list the answer can still be in).
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
== Contacts (0) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 9
Pick a number from 1 to 6.
== Contacts (0) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 3
part of a name or number>
Type part of a name or a number.
== Contacts (0) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 1
name>
A contact needs a name.
== Contacts (0) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 1
name> This Name Is Far Too Long For The Book
A name has at most 30 characters.
== Contacts (0) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 1
name> Ann Lee
phone> 0912-abc
A phone number has only digits, spaces and dashes (and a + first).
== Contacts (0) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 1
name> Ann Lee
phone> 12-34
A phone number has 7 to 15 digits; this one has 4.
== Contacts (0) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 1
name> Ann Lee
phone> 0912+345678
A + can only come first.
== Contacts (0) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 1
name> Ann Lee
phone> 1234567890123456
A phone number has 7 to 15 digits; this one has 16.
== Contacts (0) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 1
name> Ann Lee
phone> 0912 345 678
Added #1: Ann Lee.
== Contacts (1) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 1
name> ANN LEE
There is already a contact named Ann Lee.
== Contacts (1) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 3
part of a name or number> zz
-- found: 0 --
(nothing here)
== Contacts (1) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 4
id> x
No contact with that id.
== Contacts (1) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 4
id> 7
No contact with that id.
== Contacts (1) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 4
id> 1
name (Enter keeps Ann Lee)>
phone (Enter keeps 0912 345 678)> 123
A phone number has 7 to 15 digits; this one has 3.
== Contacts (1) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 5
id> 0
No contact with that id.
== Contacts (1) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 2
-- A to Z: 1 --
#1 Ann Lee 0912 345 678
== Contacts (1) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 6
Bye.
What was typed (38 lines)
9
3
1
1
This Name Is Far Too Long For The Book
1
Ann Lee
0912-abc
1
Ann Lee
12-34
1
Ann Lee
0912+345678
1
Ann Lee
1234567890123456
1
Ann Lee
0912 345 678
1
ANN LEE
3
zz
4
x
4
7
4
1
123
5
0
2
6
basic
interpreter: byte-equal
== Contacts (0) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 1
name> Wen Chen
phone> 0912-345-678
Added #1: Wen Chen.
== Contacts (1) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 1
name> ann lee
phone> +886 2 2345 6789
Added #2: ann lee.
== Contacts (2) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 1
name> Bob Marsh
phone> 02-8765-4321
Added #3: Bob Marsh.
== Contacts (3) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 1
name> Carla Diaz
phone> 0933 222 111
Added #4: Carla Diaz.
== Contacts (4) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 2
-- A to Z: 4 --
#2 ann lee +886 2 2345 6789
#3 Bob Marsh 02-8765-4321
#4 Carla Diaz 0933 222 111
#1 Wen Chen 0912-345-678
== Contacts (4) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 3
part of a name or number> an
-- found: 1 --
#2 ann lee +886 2 2345 6789
== Contacts (4) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 3
part of a name or number> 0912 345
-- found: 1 --
#1 Wen Chen 0912-345-678
== Contacts (4) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 3
part of a name or number> 23
-- found: 2 --
#2 ann lee +886 2 2345 6789
#1 Wen Chen 0912-345-678
== Contacts (4) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 4
id> 1
name (Enter keeps Wen Chen)> Anna Wen
phone (Enter keeps 0912-345-678)>
Updated #1.
== Contacts (4) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 4
id> 3
name (Enter keeps Bob Marsh)>
phone (Enter keeps 02-8765-4321)> 02-8765-0000
Updated #3.
== Contacts (4) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 5
id> 4
Deleted #4: Carla Diaz.
== Contacts (3) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 2
-- A to Z: 3 --
#2 ann lee +886 2 2345 6789
#1 Anna Wen 0912-345-678
#3 Bob Marsh 02-8765-0000
== Contacts (3) ==
1) add 2) list 3) find 4) edit 5) delete 6) quit
choice> 6
Bye.
What was typed (31 lines)
1
Wen Chen
0912-345-678
1
ann lee
+886 2 2345 6789
1
Bob Marsh
02-8765-4321
1
Carla Diaz
0933 222 111
2
3
an
3
0912 345
3
23
4
1
Anna Wen
4
3
02-8765-0000
5
4
2
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# P004 contacts: add contacts, find them by part of a name or part of a
# number, edit and delete them; the book is always shown in name order. The
# contacts live while the program runs; the first twenty projects do not
# save to files.
import book
import text
def show(title, contacts):
"" ^0
("-- " + title + ": " + str(len(contacts)) + " --") ^0
if len(contacts) == 0:
" (nothing here)" ^0
for c in contacts:
(" " + ("%-5s" % ("#" + str(c[0]))) + ("%-22s" % c[1]) + c[2]) ^0
def ask_id(contacts):
# The contact whose id is typed, or None once the problem has been shown.
text.trim(input("id> ")) => typed
None => c
if typed != "" and text.digits_of(typed) == typed:
book.find(contacts, int(typed)) => c
if c == None:
"No contact with that id." ^0
return c
def name_problem(contacts, name, own_id):
# What is wrong with name for a contact, or "" if nothing is. own_id is
# the contact being edited (0 for a new one): it may keep its own name.
if name == "":
return "A contact needs a name."
if len(name) > 30:
return "A name has at most 30 characters."
book.named(contacts, name) => other
if other != None and other[0] != own_id:
return "There is already a contact named " + other[1] + "."
return ""
[] => contacts
1 => next_id
True => running
while running:
"" ^0
("== Contacts (" + str(len(contacts)) + ") ==") ^0
"1) add 2) list 3) find 4) edit 5) delete 6) quit" ^0
text.trim(input("choice> ")) => choice
if choice == "1":
text.trim(input("name> ")) => name
name_problem(contacts, name, 0) => problem
if problem != "":
problem ^0
else:
text.trim(input("phone> ")) => phone
text.phone_problem(phone) => problem
if problem != "":
problem ^0
else:
book.insert(contacts, [next_id, name, phone]) => contacts
("Added #" + str(next_id) + ": " + name + ".") ^0
next_id + 1 => next_id
elif choice == "2":
show("A to Z", contacts)
elif choice == "3":
text.trim(input("part of a name or number> ")) => fragment
if fragment == "":
"Type part of a name or a number." ^0
else:
show("found", book.search(contacts, fragment))
elif choice == "4":
ask_id(contacts) => c
if c != None:
text.trim(input("name (Enter keeps " + c[1] + ")> ")) => name
if name == "":
c[1] => name
name_problem(contacts, name, c[0]) => problem
if problem != "":
problem ^0
else:
text.trim(input("phone (Enter keeps " + c[2] + ")> ")) => phone
if phone == "":
c[2] => phone
text.phone_problem(phone) => problem
if problem != "":
problem ^0
else:
# Out and back in, so a new name lands in its place.
book.insert(book.remove(contacts, c[0]), [c[0], name, phone]) => contacts
("Updated #" + str(c[0]) + ".") ^0
elif choice == "5":
ask_id(contacts) => c
if c != None:
book.remove(contacts, c[0]) => contacts
("Deleted #" + str(c[0]) + ": " + c[1] + ".") ^0
elif choice == "6":
False => running
else:
"Pick a number from 1 to 6." ^0
"Bye." ^0
Python projection (main.py)
import book
import text
def show(title, contacts):
print("")
print("-- " + title + ": " + str(len(contacts)) + " --")
if len(contacts) == 0:
print(" (nothing here)")
for c in contacts:
print(" " + "%-5s" % ("#" + str(c[0])) + "%-22s" % c[1] + c[2])
def ask_id(contacts):
typed = text.trim(input("id> "))
c = None
if typed != "" and text.digits_of(typed) == typed:
c = book.find(contacts, int(typed))
if c == None:
print("No contact with that id.")
return c
def name_problem(contacts, name, own_id):
if name == "":
return "A contact needs a name."
if len(name) > 30:
return "A name has at most 30 characters."
other = book.named(contacts, name)
if other != None and other[0] != own_id:
return "There is already a contact named " + other[1] + "."
return ""
contacts = []
next_id = 1
running = True
while running:
print("")
print("== Contacts (" + str(len(contacts)) + ") ==")
print("1) add 2) list 3) find 4) edit 5) delete 6) quit")
choice = text.trim(input("choice> "))
if choice == "1":
name = text.trim(input("name> "))
problem = name_problem(contacts, name, 0)
if problem != "":
print(problem)
else:
phone = text.trim(input("phone> "))
problem = text.phone_problem(phone)
if problem != "":
print(problem)
else:
contacts = book.insert(contacts, [next_id, name, phone])
print("Added #" + str(next_id) + ": " + name + ".")
next_id = next_id + 1
elif choice == "2":
show("A to Z", contacts)
elif choice == "3":
fragment = text.trim(input("part of a name or number> "))
if fragment == "":
print("Type part of a name or a number.")
else:
show("found", book.search(contacts, fragment))
elif choice == "4":
c = ask_id(contacts)
if c != None:
name = text.trim(input("name (Enter keeps " + c[1] + ")> "))
if name == "":
name = c[1]
problem = name_problem(contacts, name, c[0])
if problem != "":
print(problem)
else:
phone = text.trim(input("phone (Enter keeps " + c[2] + ")> "))
if phone == "":
phone = c[2]
problem = text.phone_problem(phone)
if problem != "":
print(problem)
else:
contacts = book.insert(book.remove(contacts, c[0]), [c[0], name, phone])
print("Updated #" + str(c[0]) + ".")
elif choice == "5":
c = ask_id(contacts)
if c != None:
contacts = book.remove(contacts, c[0])
print("Deleted #" + str(c[0]) + ": " + c[1] + ".")
elif choice == "6":
running = False
else:
print("Pick a number from 1 to 6.")
print("Bye.")
book.eml
eml# P004 contacts - the contact book. A contact is a list [id, name, phone].
# The book is kept in name order, ignoring case, and a new or renamed contact
# goes in at the place a binary search finds, so the list never needs sorting.
# Two contacts cannot share a name that differs only in case. Ids are never
# reused.
import text
def position(book, name):
# Where a contact with this name belongs: the index of the first contact
# whose name, ignoring case, is not before it. Binary search: each step
# halves the part of the list the answer can still be in.
text.lower(name) => key
0 => lo
len(book) => hi
while lo < hi:
int((lo + hi) / 2) => mid
if text.lower(book[mid][1]) < key:
mid + 1 => lo
else:
mid => hi
return lo
def named(book, name):
# The contact with this name, ignoring case, or None.
position(book, name) => p
if p < len(book) and text.lower(book[p][1]) == text.lower(name):
return book[p]
return None
def insert(book, contact):
# A new book with the contact in its place.
position(book, contact[1]) => p
return book[0:p] + [contact] + book[p:len(book)]
def find(book, contact_id):
for c in book:
if c[0] == contact_id:
return c
return None
def remove(book, contact_id):
return [c for c in book if c[0] != contact_id]
def search(book, fragment):
# Contacts matching part of a name or part of a number. A fragment with a
# digit in it is looked for in the numbers, digits only - "0912 345"
# finds 0912-345-678 - and any other fragment in the names, ignoring case.
text.digits_of(fragment) => digits
[] => found
for c in book:
if digits != "":
if digits in text.digits_of(c[2]):
found + [c] => found
elif text.lower(fragment) in text.lower(c[1]):
found + [c] => found
return found
Python projection (book.py)
import text
def position(book, name):
key = text.lower(name)
lo = 0
hi = len(book)
while lo < hi:
mid = int((lo + hi) / 2)
if text.lower(book[mid][1]) < key:
lo = mid + 1
else:
hi = mid
return lo
def named(book, name):
p = position(book, name)
if p < len(book) and text.lower(book[p][1]) == text.lower(name):
return book[p]
return None
def insert(book, contact):
p = position(book, contact[1])
return book[0:p] + [contact] + book[p:len(book)]
def find(book, contact_id):
for c in book:
if c[0] == contact_id:
return c
return None
def remove(book, contact_id):
return [c for c in book if c[0] != contact_id]
def search(book, fragment):
digits = text.digits_of(fragment)
found = []
for c in book:
if digits != "":
if digits in text.digits_of(c[2]):
found = found + [c]
elif text.lower(fragment) in text.lower(c[1]):
found = found + [c]
return found
text.eml
eml# P004 contacts - text helpers. The interpreter that checks every session
# does not run string methods yet, so these are written out.
def trim(s):
# s without the spaces at either end.
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 lower(s):
# s with A-Z turned into a-z; everything else as it was.
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" => upper_letters
"abcdefghijklmnopqrstuvwxyz" => lower_letters
"" => out
for c in s:
c => d
for i in [0:25]:
if upper_letters[i] == c:
lower_letters[i] => d
out + d => out
return out
def digits_of(s):
# Only the digits of s, in order.
"" => out
for c in s:
if c in "0123456789":
out + c => out
return out
def phone_problem(phone):
# What is wrong with phone as a phone number, or "" if nothing is: digits,
# spaces and dashes, a + only at the very start, and 7 to 15 digits.
if phone == "":
return "Type a phone number."
0 => i
for c in phone:
if c == "+":
if i != 0:
return "A + can only come first."
elif not (c in "0123456789 -"):
return "A phone number has only digits, spaces and dashes (and a + first)."
i + 1 => i
len(digits_of(phone)) => n
if n < 7 or n > 15:
return "A phone number has 7 to 15 digits; this one has " + str(n) + "."
return ""
Python projection (text.py)
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 lower(s):
upper_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lower_letters = "abcdefghijklmnopqrstuvwxyz"
out = ""
for c in s:
d = c
for i in range(0, 26):
if upper_letters[i] == c:
d = lower_letters[i]
out = out + d
return out
def digits_of(s):
out = ""
for c in s:
if c in "0123456789":
out = out + c
return out
def phone_problem(phone):
if phone == "":
return "Type a phone number."
i = 0
for c in phone:
if c == "+":
if i != 0:
return "A + can only come first."
elif not c in "0123456789 -":
return "A phone number has only digits, spaces and dashes (and a + first)."
i = i + 1
n = len(digits_of(phone))
if n < 7 or n > 15:
return "A phone number has 7 to 15 digits; this one has " + str(n) + "."
return ""