You are on page 1of 47

1 Series 01: variables, expressions and statements

1.1 ISBN

# read first nine digits of an ISBN-10 code and convert them to integers
x1 = int(input())
x2 = int(input())
x3 = int(input())
x4 = int(input())
x5 = int(input())
x6 = int(input())
x7 = int(input())
x8 = int(input())
x9 = int(input())

# compute check digit


x10 = (
x1 + 2 * x2 + 3 * x3 + 4 * x4 + 5 * x5 + 6 * x6 + 7 * x7 + 8 * x8 + 9 * x9
) % 11

# print check digit


print(x10)

1.2 Sum of two integers

# read two terms and convert them to integers


term1 = int(input(’Give an integer: ’))
term2 = int(input(’Give another integer: ’))

# compute sum of two integers


# NOTE: we do not use the name "sum" for the variable "sum" because this is the
# name of a built-in function in Python
total = term1 + term2

# write sum to output


print(total)

1.3 The cricket as a thermometer

# read number of chirps per minute


N60 = int(input(’Number of chirps per minute: ’))

# compute and print temperature in degrees Fahrenheit


TF = 50 + (N60 - 40) / 4
print(f’temperature (Fahrenheit): {TF}’)

# compute and print temperature in degrees Celsius


TC = 10 + (N60 - 40) / 7
print(f’temperature (Celsius): {TC}’)

1.4 Where is the father ?

# read values that define the problem


# note: number of years is immediately converted in number of months
a = 12 * int(input())
b = 12 * int(input())
c = int(input())

# calculate age of the son (in months)

1
son = (a + b - b * c) // (c - 1)

# calculate age of the mother (in months)


mother = son + a

# print age of the son and the mother


print(f’The mother is {mother} months old and her son {son} months.’)

1.5 Great-circle navigation

# import trigonometric functions from math module


from math import sin, cos, acos, radians

# read longitude and latitude of two points on Earth


x1 = float(input())
y1 = float(input())
x2 = float(input())
y2 = float(input())

# convert degrees into radians


x1 = radians(x1)
y1 = radians(y1)
x2 = radians(x2)
y2 = radians(y2)

# compute great-circle distance


r = 6371
d = r * acos(sin(x1) * sin(x2) + cos(x1) * cos(x2) * cos(y1 - y2))

# output great-circle distance, rounded to the closest natural number


print(f’The great-circle distance is {round(d)} km.’)

1.6 IEC units

# read number of bytes in SI-units


gigabytes = int(input())
megabytes = int(input())
kilobytes = int(input())
bytes = int(input())

# compute total number of bytes


bytes = ((gigabytes * 1000 + megabytes) * 1000 + kilobytes) * 1000 + bytes

# output total number of bytes


print(f’{bytes}b’)

# convert total number of bytes into IEC-units


kibibytes = bytes // 1024
bytes %= 1024

mebibytes = kibibytes // 1024


kibibytes %= 1024

gibibytes = mebibytes // 1024


mebibytes %= 1024

# output total number of bytes in IEC-units


print(f’{gibibytes}Gib, {mebibytes}Mib, {kibibytes}Kib, {bytes}b’)

2
2 Series 02: conditional statements
2.1 ISBN

# read ten digits of an ISBN-10 code (each on a separate line)


x1 = int(input())
x2 = int(input())
x3 = int(input())
x4 = int(input())
x5 = int(input())
x6 = int(input())
x7 = int(input())
x8 = int(input())
x9 = int(input())
x10 = int(input())

# compute check digit


check_digit = (
x1 + 2 * x2 + 3 * x3 + 4 * x4 + 5 * x5 + 6 * x6 + 7 * x7 + 8 * x8 + 9 * x9
) % 11

# check correctness of check digit


print(’OK’ if x10 == check_digit else ’WRONG’)

"""
alternative solution:

if x10 == check_digit:
print(’OK’)
else:
print(’WRONG’)
"""

2.2 Goldilocks principle

# read the value


value = int(input(’What is the value? ’))

# read the lower bound


lower_bound = int(input(’What is the lower bound? ’))

# read the upper bound


upper_bound = int(input(’What is the upper bound? ’))

# read the property of values that are too low


extremely_low = input(’Low values are ...? ’)

# read the property of values that are too high


extremely_high = input(’High values are ...? ’)

# determine the observation according to the Goldilocks principle


if value < lower_bound:
observation = f’too {extremely_low}’
elif value > upper_bound:
observation = f’too {extremely_high}’
else:
observation = ’just right’

# output the observation


print(observation)

3
2.3 Fizz buzz

# read number
number = int(input())

# determine pronunciation
if not number % 3 and not number % 5:
pronunciation = ’fizz buzz’
elif not number % 3:
pronunciation = ’fizz’
elif not number % 5:
pronunciation = ’buzz’
else:
pronunciation = number

# output pronunciation
print(pronunciation)

2.4 Formula one

import math

# read information about the F1 circuit


location = input()
lap_distance = float(input())
lap_time = float(input())

# compute the number of laps


if location == ’Monaco’:
laps = 78
else:
total_distance = math.ceil(305 / lap_distance)
total_time = math.ceil(120 / lap_time)
laps = min(total_distance, total_time)

# resultaat uitschrijven
print(f’The Grand Prix of {location} runs over {laps} laps ({laps * lap_distance} km).’)

2.5 Hertzsprung-Russell diagram

# read temperature and luminosity of star


temperature = float(input())
luminosity = float(input())

# determine classification of star


if luminosity > 10000:
classification = ’supergiants (a)’
elif luminosity > 1000:
classification = ’supergiants (b)’
elif luminosity > 100 and temperature < 7500:
classification = ’bright giants’
elif luminosity > 10 and temperature < 6000:
classification = ’giants’
elif (
(luminosity < 0.01 and temperature > 5000)
or
(luminosity < 0.0001 and temperature > 3000)
):
classification = ’white dwarfs’
else:
classification = ’main sequence’

# output classification of star


print(classification)

4
2.6 Rock-paper-scissors-lizard-Spock

# read gestures of both players


gesture1 = input()
gesture2 = input()

# determine the outcome of the game


if gesture1 == gesture2:

# draw in case both players show the same gesture


outcome = ’draw’

else:

# determine position of gestures in list of gestures


gestures = [’scissors’, ’paper’, ’rock’, ’lizard’, ’Spock’]
gesture1 = gestures.index(gesture1)
gesture2 = gestures.index(gesture2)

"""
alternative way to determine the position, without using lists

if gesture1 == ’scissors’:
gesture1 = 0
elif gesture1 == ’paper’:
gesture1 = 1
elif gesture1 == ’rock’:
gesture1 = 2
elif gesture1 == ’lizard’:
gesture1 = 3
else:
gesture1 = 4

if gesture2 == ’scissors’:
gesture2 = 0
elif gesture2 == ’paper’:
gesture2 = 1
elif gesture2 == ’rock’:
gesture2 = 2
elif gesture2 == ’lizard’:
gesture2 = 3
else:
gesture2 = 4
"""

# gesture beats other gesture that is one or three positions further down
# the list; modulo operator is used because the list is circular
if (gesture1 + 1) % 5 == gesture2 or (gesture1 + 3) % 5 == gesture2:
outcome = ’player1 wins’
else:
outcome = ’player2 wins’

# output the outcome of the game


print(outcome)

5
3 Series 03: loops
3.1 ISBN

# read first digit of first ISBN-10 code


# NOTE: at this point we cannot assume the first line of the first ISBN-10 code
# contains a digit, since it may also contain the word stop
first_digit = input()

while first_digit != ’stop’:

# read next eight digits and compute check digit


computed_check_digit = int(first_digit)
for index in range(2, 10):
next_digit = int(input())
computed_check_digit += index * next_digit
computed_check_digit %= 11

# read given check digit


given_check_digit = int(input())

# output correctness of given check digit


print(’OK’ if given_check_digit == computed_check_digit else ’WRONG’)

# read first digit of next ISBN-10 code


# NOTE: at this point we cannot assume the first line of the next ISBN-10
# code contains a digit, since it may also contain the word stop
first_digit = input()

3.2 New York Times

# read the increment


increment = int(input())

# read first number from the sequence; for now there is not previous number
previous, current = None, input()

# getallen uit de reeks één voor één inlezen totdat er een regel wordt ingelezen
# die de tekst NYT bevat; het vorige getal uit de reeks wordt telkens
# onthouden zodat kan nagegaan worden of het huidige getal gevormd wordt door
# het increment op te tellen bij het vorige getal
# read numbers from sequence one by one, until a line is read that contains the
# text NYT; previous number in the sequence is remembered so that we can check
# if the current number is equal to the sum of the previous number and the
# increment
while current != ’NYT’:

# convert input to an integer


current = int(current)

# check if current number equals the sum of the increment and the previous
# number
if previous is not None and previous + increment != current:
print(f’{previous} -> {current} ({current - previous:+d})’)

# read next number from sequence and remember the previous number
previous, current = current, input()

3.3 Confederacy of Squares

6
# import module defining data structure to represent dates in Python
import datetime

# read name and birth year of person


name = input()
birth_year = int(input())

# check if person can become x years old in the year x^2


age = 0
while age ** 2 < birth_year + age:
age += 1

# output if person is a member of the Confederacy of Squares


if age ** 2 == birth_year + age:

current_year = datetime.date.today().year
verb = ’was’ if birth_year + age < current_year else ’turns’
print(f’{name} {verb} {age} in {birth_year + age}.’)

else:

print(f’{name} is not a member of the Confederacy of Squares.’)

3.4 All-or-nothing

# read starting capital


capital = int(input())

# read first line of the description of the next turn


bet = input()

# initialize variable that indicates if a player didn’t gamble more money than
# he has left
valid_bet = True

# player continues as long as he keeps winning, until he has no more money to


# play, or until he gambled more money than he has left
while bet != ’stop’ and capital > 0 and valid_bet:

# read second and third line of description of the next turn


choice = input()
outcome = input()

# convert string representation of the bet to a number


bet = capital if bet == ’all’ else int(bet)

# determine outcome of next turn


if bet > capital:
# player gambled more money than he has left
valid_bet = False
else:
if choice == outcome:
# player wins back double of his bet
capital += bet
else:
# player loses his bet
capital -= bet

# read first line of the description of the next turn


bet = input()

# determine if a player is done playing, or if he has gambled more money than


# he had left
if valid_bet:
print(f’You end up with {capital} dollar.’)
else:

7
print(f’You cannot bet {bet} dollar if you only have {capital} dollar.’)

3.5 Canvascrack

# read input data


tables = int(input()) # number of tables played
table_extra = int(input()) # extra value of each successive table
table_double = int(input()) # number of tables played before profit is doubled
lost = input() == ’lost’ # last table was lost ?

# crack hasn’t won anything yet at the start of the game


profit = 0

# determine profit crack has made after each table has been played
for table in range(1, tables + 1):

# adjust profit after table has been won or lost


double = ’’
if table == tables and lost:

# profit is halved if crack has lost the last table played


profit //= 2

else:

# crack earns the value of the current table


profit += table * table_extra

# after a fixed number of tables the profit is doubled


if not table % table_double:
profit *= 2
double = ’ (x2)’

# output total amount won after playing current table


print(f’table #{table}{double}: {profit}’)

3.6 Thoughts that count

# functional representation of the operators less than (<) and greater than (>)
from operator import lt, gt

# read the total number of red and white roses


red_white = int(input())

# read the total number of white and blue roses


white_blue = int(input())

# read the operator (less than or greater than)


operator = lt if input() == ’<’ else gt

# iterate over all possible numbers of white roses: we know there are at least
# 2 roses of each color, so the number of white roses can’t be larger than the
# number of red and white roses minus two and the number of white and blue roses
# minus two
for white in range(2, min(red_white, white_blue) - 1):

# the total number of red and white roses is fixed, so for a given number of
# white roses we can also determine the total number of red roses
red = red_white - white

# the total number of blue and white roses is fixed, so for a given number of
# white roses we can also determine the total number of blue roses
blue = white_blue - white

8
# output the total number of blue, white and red roses if the condition
# imposed on the total number of white and blue roses is fulfilled
if operator(blue + red, white_blue):
print(blue, white, red, sep=’\n’)

9
4 Series 04: strings
4.1 ISBN

# read first ISBN-10 code (or the word stop)


code = input()

# read successive ISBN-10 codes until line containing "stop" is read


while code != ’stop’:

# compute check digit


check_digit = int(code[0])
for i in range(2, 10):
check_digit += i * int(code[i - 1])
check_digit %= 11

"""
# compute check digit: alternative solution using generator expression
check_digit = sum((i + 1) * int(code[i]) for i in range(9)) % 11
"""

# extract check digit from ISBN-10 code


x10 = code[-1]

# check whether computed and extracted check digits are the same
if (check_digit == 10 and x10 == ’X’) or x10 == str(check_digit):
print(’OK’)
else:
print(’WRONG’)

# read next ISBN-10 code (or the word stop)


code = input()

4.2 Atbash

def encoding(character):

if not character.isalpha():
return character

ref = ’A’ if character.isupper() else ’a’


return chr(25 - ord(character) + 2 * ord(ref))

def atbash(message):

translation = ’’
for character in message:
translation += encoding(character)

return translation

"""
# alternative using generator expression
return ’’.join(encoding(character) for character in message)
"""

cases = int(input())
for _ in range(cases):
message = input()
print(atbash(message))

10
4.3 Vampire numbers
def isvampire(number):

"""
>>> isvampire(1260)
True
>>> isvampire(1234)
False
>>> isvampire(1395)
True
"""

# check if number of digits is even


if len(str(number)) % 2:
return False

# determine number of digits of fangs


half = len(str(number)) // 2

# determine sorted list of digits


sorted_digits = sorted(str(number))

# traverse all possible fangs (with the required number of digits)


for fang1 in range(10 ** (half - 1), 10 ** half):
# check if fang is a divisor of the number
if not number % fang1:
# determine second fang
fang2 = number // fang1
if (
# fangs may not both have trailing zeroes (divisible by 10)
(fang1 % 10 or fang2 % 10) and
# sorted list of digits of fangs must be the same as for number
sorted(str(fang1) + str(fang2)) == sorted_digits
):
return True

# no pair of fangs found


return False

# output wether the given number if a vampire number


number = int(input())
print(f’{number} is {"" if isvampire(number) else "not"} a vampire number.’)

4.4 Drawing cubes


# read dimension of cuboid
width = int(input())
height = int(input())
depth = int(input())

# draw top face and part of side face


for index in range(depth):
print(
’ ’ * (depth - index) +
’:’ * (width - 1) +
’/’ +
’+’ * min(index, height - 1)
)

# draw front face and part of side face


for index in range(height):
print(
’#’ * width +
’+’ * min(depth, height - index - 1)
)

11
4.5 The language of science

# read two words


word1 = input()
word2 = input()

# extract longest common prefix


index = 0
while word1[index] == word2[index]:
index += 1
prefix = word1[:index]

# extract longest common suffix


index = -1
while word1[index] == word2[index]:
index -= 1
suffix = ’’ if index == -1 else word1[index + 1:]

# extract stem between prefix and suffix


stem1 = word1[len(prefix):-len(suffix)] if suffix else word1[len(prefix):]
stem2 = word2[len(prefix):-len(suffix)] if suffix else word2[len(prefix):]

# compute maximal length of both stems


stem_width = max(len(stem1), len(stem2))

# output word components


print(f’{" " * len(prefix)}{stem1.center(stem_width)}’)
print(f’{prefix}{" " * stem_width}{suffix}’)
print(f’{" " * len(prefix)}{stem2.center(stem_width)}’)

4.6 Jack jumper ant

# read order of two rows of ants that move in opposite directions


rightwards = input()
leftwards = input()

# define initial order of all ants


previous, current = None, rightwards[::-1] + leftwards

# simulate order of ants until no more changes are observed


while previous != current:

# print current order


print(current)

# remember previous order and initialize new order


previous, current = current, ’’

# determine new order based on previous order


index = 0
while index < len(previous) - 1:

if previous[index] in rightwards and previous[index + 1] in leftwards:


current += previous[index + 1] + previous[index]
index += 2
else:
current += previous[index]
index += 1

if index < len(previous):


current += previous[index]

12
5 Series 05: functions
5.1 ISBN

def isISBN(code):

"""
Return True if the argument is a string that contains a valid ISBN-10 code,
False otherwise.

>>> isISBN(’9971502100’)
True
>>> isISBN(’9971502108’)
False
>>> isISBN(’53WKEFF2C’)
False
>>> isISBN(4378580136)
False
"""

# note: isinstance is a Python built-in function that returns a Boolean


# value that indicates whether the first argument is an object that
# has a data type equal to the second argument
if not (
isinstance(code, str) and # code must be a string
len(code) == 10 and # code must contain 10 characters
code[:9].isdigit() # first nine characters must be digits
):
return False

# check the check digit


return checkdigit(code) == code[-1]

def checkdigit(code):

"""
Computes the check digit for a given string that contains the first nine
digits of an ISBN-10 code. A string representation of the check digit is
returned, with the value 10 represented as the letter X.

>>> checkdigit(’997150210’)
’0’
>>> checkdigit(’938389293’)
’5’
"""

# compute check digit


check = sum((i + 1) * int(code[i]) for i in range(9)) % 11

# convert check digit into string representation


return ’X’ if check == 10 else str(check)

if __name__ == ’__main__’:
import doctest
doctest.testmod()

5.2 123

def evenOdd(number):

"""
>>> evenOdd(886328712442992)
(10, 5)
>>> evenOdd(10515)
(1, 4)

13
>>> evenOdd(145)
(1, 2)
"""

even, odd = 0, 0
for digit in str(number):
digit = int(digit)
if digit % 2:
odd += 1
else:
even += 1

return even, odd

def step(number):

"""
>>> step(886328712442992)
10515
>>> step(10515)
145
>>> step(145)
123
"""

even, odd = evenOdd(number)


return int(str(even) + str(odd) + str(even + odd))

def steps(number):

"""
>>> steps(886328712442992)
3
>>> steps(1217637626188463187643618416764317864)
4
>>> steps(0)
2
>>> steps(1)
5
>>> steps(2)
2
>>> steps(3)
5
"""

steps = 0
while number != 123:
number = step(number)
steps += 1

return steps

if __name__ == ’__main__’:
import doctest
doctest.testmod()

5.3 Numeronym

def numeronym(word):

"""
>>> numeronym(’internationalization’)
’i18n’
>>> numeronym(’TAKEDOWN’)
’T6N’
>>> numeronym(’Random’)
’R4m’

14
>>> numeronym(’DNA’)
’DNA’
"""

# words having less than four letters remain unchanged


if len(word) < 4:
return word

# reduce word by replacing middle letters by a number


return word[0] + str(len(word) - 2) + word[-1]

def template(numeronym):

"""
>>> template(’i18n’)
’i..................n’
>>> template(’TAK3N’)
’TAK...N’
>>> template(’R2D2’)
’R..D..’
>>> template(’se7en’)
’se.......en’
>>> template(’C3PO’)
’C...PO’
"""

# replace each number by the corresponding number of dots


template, number = ’’, ’’
for character in numeronym:
if character.isdigit():
number += character
else:
if number:
template += ’.’ * int(number)
number = ’’
template += character

# process last number (only in case the word ends with a number)
if number:
template += ’.’ * int(number)

return template

def isnumeronym(numeronym, word):

"""
>>> isnumeronym(’i18n’, ’internationalization’)
True
>>> isnumeronym(’TAK3N’, ’TAKEDOWN’)
False
>>> isnumeronym(’R2D2’, ’Random’)
True
>>> isnumeronym(’se7en’, ’semicitizen’)
True
>>> isnumeronym(’C3PO’, ’cuerpo’)
True
"""

# construct template for the numeronym


numeronym = template(numeronym)

# check if word and template have the same length


if len(word) != len(numeronym):
return False

# check if all positions of the word match the corresponding position of the
# template
# NOTE: conversion to uppercase for case insensitive comparisons
for letter1, letter2 in zip(word.upper(), numeronym.upper()):

15
if letter2 not in ’.’ + letter1:
return False

return True

"""
# alternative solution using regular expression
import re
return bool(re.fullmatch(template(numeronym), word, flags=re.IGNORECASE))
"""

if __name__ == ’__main__’:
import doctest
doctest.testmod()

5.4 Divide and conquer

from math import log10

def longestPolydivisiblePrefix(number):

"""
>>> longestPolydivisiblePrefix(1234356789)
123
>>> longestPolydivisiblePrefix(381654729)
381654729
>>> longestPolydivisiblePrefix(381654728)
38165472
"""

assert isinstance(number, int) and number > 0, ’argument must be a strictly positive
integer’

digits = int(log10(number)) + 1
power = pow(10, digits - 1)
for prefix in range(2, digits + 1):
power //= 10
if (number // power) % prefix:
return number // (10 * power)

return number

"""
# alternative solution: works with strings instead of integers
number = str(number)
for digits in range(2, len(number) + 1):
if int(number[:digits]) % digits:
return int(number[:digits - 1])

return int(number)
"""

def isPolydivisible(number):

"""
>>> isPolydivisible(1234356789)
False
>>> isPolydivisible(381654729)
True
>>> isPolydivisible(381654728)
False
"""

return longestPolydivisiblePrefix(number) == number

def polydivisibleExtensions(number):

16
"""
>>> polydivisibleExtensions(12)
4
>>> polydivisibleExtensions(23)
0
>>> polydivisibleExtensions(381654729)
1
"""

if not isPolydivisible(number):
return 0

count = 0
number *= 10
digits = int(log10(number)) + 1
for digit in range(10):
if not (number + digit) % digits:
count += 1

return count

if __name__ == ’__main__’:
import doctest
doctest.testmod()

5.5 Pig Latin

def pigword(word):

"""
>>> pigword(’egg’)
’eggway’
>>> pigword(’trash’)
’ashtray’
>>> pigword(’quit’)
’itquay’
>>> pigword(’Pig’)
’Igpay’
>>> pigword(’Latin’)
’Atinlay’
>>> pigword(’BaNaNa’)
’ANaNabay’
>>> pigword(’DNa’)
’AdNay’
>>> pigword(’plover’)
’overplay’
>>> pigword(’plunder’)
’underplay’
"""

# conversion of empty words


if not word:
return ’’

# definition of vowels
vowels = ’aeiou’

# conversion of words starting with a vowel


if word[0].lower() in vowels:
return word + ’way’

# conversion of words starting with a consonant


# determine position where both parts must be split
pos = 0
while (
pos < len(word) and
word[pos].lower() not in vowels

17
):
if word[pos:].lower().startswith(’qu’):
pos += 2
else:
pos += 1

# split parts
part1 = word[:pos]
part2 = word[pos:]

# set upper case and lower case letters of both parts


if part2 and word[0].isupper():
part2 = part2[0].upper() + part2[1:]

if pos < len(word) and word[pos].islower():


part1 = part1[0].lower() + part1[1:]

# latinize word by switching both parts


return part2 + part1 + ’ay’

def piglatin(text):

"""
>>> piglatin(’And now for something completely different!’)
’Andway ownay orfay omethingsay ompletelycay ifferentday!’
>>> piglatin(’Stwike him, centuwion, stwike him vewy wuffly’)
’Ikestway imhay, entuwioncay, ikestway imhay ewyvay ufflyway’
"""

# split text into word and latinize those words;


# characters that are no letter must be retained
word, pigtext = ’’, ’’
for character in text:

if character.isalpha():

# further complete current word


word += character

else:

# latinize word if there is one, and then start


# with the construction of a new word
if word:
pigtext += pigword(word)
word = ’’

# append non-letter to latinized text


pigtext += character

# last word must be latinized if the given text ends in


# a letter
if word:
pigtext += pigword(word)

return pigtext

if __name__ == ’__main__’:
import doctest
doctest.testmod()

5.6 Geohash

import string

def geo2dec(geohash):

18
"""
>>> geo2dec(’ezs42’)
14672002
>>> geo2dec(’DRUGGED’)
13684424108
>>> geo2dec(’ZUR1CH’)
1068205424
"""

# define character map for base 32 decoding


charmap = (
string.digits +
’’.join(c for c in string.ascii_uppercase if c not in ’AILO’)
)

# base 32 decoding
return sum(
charmap.index(char) * 32 ** index
for index, char in enumerate(geohash.upper()[::-1])
)

def geo2bin(geohash):

"""
>>> geo2bin(’ezs42’)
’0110111111110000010000010’
>>> geo2bin(’DRUGGED’)
’01100101111101001111011110110101100’
>>> geo2bin(’ZUR1CH’)
’111111101010111000010101110000’
"""

# convert decimal representation to bitstring string


bitstring = bin(geo2dec(geohash))[2:]

# add leading zeros such that each character corresponds to 5 bits


return bitstring.zfill(5 * len(geohash))

def unravel(bitstring):

"""
>>> unravel(’0110111111110000010000010’)
(’0111110000000’, ’101111001001’)
>>> unravel(’01100101111101001111011110110101100’)
(’010011001101110010’, ’10111110111101110’)
>>> unravel(’111111101010111000010101110000’)
(’111111110000100’, ’111000100111100’)
"""

return (
’’.join(bitstring[index] for index in range(0, len(bitstring), 2)),
’’.join(bitstring[index] for index in range(1, len(bitstring), 2))
)

def mean(lower, upper):

return (lower + upper) / 2

def bin2coord(bitstring, lower, upper):

"""
>>> bin2coord(’0111110000000’, -180, 180)
(-5.625, -5.5810546875)
>>> bin2coord(’101111001001’, -90, 90)
(42.5830078125, 42.626953125)
>>> bin2coord(’010011001101110010’, -180, 180)
(-71.91375732421875, -71.91238403320312)
>>> bin2coord(’10111110111101110’, -90, 90)
(44.27215576171875, 44.273529052734375)

19
>>> bin2coord(’111111110000100’, -180, 180)
(178.6376953125, 178.648681640625)
>>> bin2coord(’111000100111100’, -90, 90)
(69.23583984375, 69.2413330078125)
"""

for bit in bitstring:


mid = mean(lower, upper)
if bit == ’0’:
upper = mid
else:
lower = mid

return lower, upper

def geo2coord(geohash):

"""
>>> geo2coord(’ezs42’)
(-5.60302734375, 42.60498046875)
>>> geo2coord(’DRUGGED’)
(-71.91307067871094, 44.27284240722656)
>>> geo2coord(’ZUR1CH’)
(178.6431884765625, 69.23858642578125)
"""

# convert geohash to bitstrings for longitude and latitude


long, lat = unravel(geo2bin(geohash))

# convert bitstrings to longitude/latitude pair


return mean(*bin2coord(long, -180, 180)), mean(*bin2coord(lat, -90, 90))

if __name__ == ’__main__’:
import doctest
doctest.testmod()

20
6 Series 06: lists and tuples
6.1 ISBN

def isISBN(code):

"""
Checks if the given ISBN-10 code is valid.

>>> isISBN(’9-9715-0210-0’)
True
>>> isISBN(’997-150-210-0’)
False
>>> isISBN(’9-9715-0210-8’)
False
"""

# check if the given code is a string


if not isinstance(code, str):
return False

# check if dashes are at the correct positions and if each group has the
# correct number of digits
groups = code.split(’-’)
if [len(e) for e in groups] != [1, 4, 4, 1]:
return False

# remove dashes from the given code


code = ’’.join(groups)

# check if all characters (except the final one) are digits


if not code[:-1].isdigit():
return False

# check the check digit of the given code


return checkdigit(code) == code[-1]

def checkdigit(code):

"""
>>> checkdigit(’997150210’)
’0’
>>> checkdigit(’938389293’)
’5’
"""

# compute check digit


check = sum((i + 1) * int(code[i]) for i in range(9)) % 11

# convert check digit into its string representation


return ’X’ if check == 10 else str(check)

if __name__ == ’__main__’:
import doctest
doctest.testmod()

6.2 Will Rogers phenomenon

def average(sequence):

"""
>>> average((5, 6, 7, 8, 9))
7.0
>>> average([1, 2, 3, 4])
2.5

21
"""

# compute average of all numbers in the given sequence


return sum(sequence) / len(sequence) if sequence else None

def move1(list1, list2, numbers):

"""
>>> seq1 = [5, 6, 7, 8, 9]
>>> seq2 = [1, 2, 3, 4]
>>> seq3 = [5]
>>> move1(seq1, seq2, seq3)
>>> seq1
[6, 7, 8, 9]
>>> seq2
[1, 2, 3, 4, 5]
>>> seq3
[5]
"""

# remove first occurrence of all numbers from the first list


for number in numbers:
list1.remove(number)

# append all numbers to the second list (in order)


list2.extend(numbers)

def move2(sequence1, sequence2, numbers):

"""
>>> seq1 = (5, 6, 7, 8, 9)
>>> seq2 = [1, 2, 3, 4]
>>> seq3 = [5]
>>> move2(seq1, seq2, seq3)
([6, 7, 8, 9], [1, 2, 3, 4, 5])
>>> seq1
(5, 6, 7, 8, 9)
>>> seq2
[1, 2, 3, 4]
>>> seq3
[5]
"""

# create two new lists as copies of the numbers of the two given sequences
# NOTE: it’s important to copy the numbers in the given sequences in two
# new lists, in order not to change the given sequences
list1, list2 = list(sequence1), list(sequence2)

# remove numbers from first list and add elements to second list
move1(list1, list2, numbers)

# return newly created list


return list1, list2

def iswillrogers(sequence1, sequence2, numbers):

"""
>>> iswillrogers([5, 6, 7, 8, 9], [1, 2, 3, 4], [5])
True
>>> iswillrogers((5, 6, 7, 8, 9), (1, 2, 3, 4), (7, 9))
False
"""

# compute averages of original sequences


before1, before2 = average(sequence1), average(sequence2)

# compute averages of sequences after moving elements


sequence1, sequence2 = move2(sequence1, sequence2, numbers)
after1, after2 = average(sequence1), average(sequence2)

22
return before1 < after1 and before2 < after2

if __name__ == ’__main__’:
import doctest
doctest.testmod()

6.3 Mutual love

def positions(word):

"""
>>> positions(’LOVE’)
(11, 14, 21, 4)
>>> positions(’mutual’)
(12, 20, 19, 20, 0, 11)
"""

# determine tuple containing alphabet position of each letter in the given


# word; letter A is at position 0 in the alphabet, letter B at position 1,
# ... and letter Z at position 25
return tuple(
ord(letter) - ord(’A’) for letter in word.upper()
if letter.isalpha()
)

def ismutual(positions, count):

"""
>>> ismutual((11, 14, 21, 4), 26)
True
>>> ismutual([12, 20, 19, 20, 0, 11], 26)
False
"""

# number of positions must be even for sequence to be mutual


if len(positions) % 2:
return False

# create new list containing positions in sorted order


# NOTE: important to copy numbers in new list, such that original sequence
# remains unchanged, which enables to pass tuples
positions = sorted(positions)

# check if sum of corresponding positions (first-last, second-penultimate,


# ...) is equal to the given count minus ont
for index in range(len(positions) // 2):
if positions[index] + positions[-index - 1] != count - 1:
return False

# all corresponding positions are mutual


return True

def mutual_love(word):

"""
>>> mutual_love(’LOVE’)
True
>>> mutual_love(’mutual’)
False
"""

# check if letters in the given word have mutual positions in an alphabet


# of 26 letters
return ismutual(positions(word), 26)

if __name__ == ’__main__’:

23
import doctest
doctest.testmod()

6.4 Zap reading

import math

def iszapreadable(page_numbers):

"""
>>> iszapreadable([7, 5, 3, 1, 8, 6, 4, 2])
True
>>> iszapreadable([1, 2, 4, 7, 3, 8, 6, 5])
False
>>> iszapreadable([1, 2, 5, 3, 8, 7, 4, 6])
True
>>> iszapreadable([1, 1, 1, 1, 1, 1, 1, 1])
False
"""

# check if page numbers are a permutation


for page_number in range(len(page_numbers)):
if (page_number + 1) not in page_numbers:
return False

# start at the first page


position = 0

# mark first page as read


isread = [True] + [False] * (len(page_numbers) - 1)

# check if book is zapreadble


for _ in range(len(page_numbers) - 1):

# determine position of next page to be read


position = (position + page_numbers[position]) % len(page_numbers)

# check if we have not read the page before


if isread[position]:
return False

# mark page as read


isread[position] = True

return True

def zapbook(pages):

"""
>>> zapbook(1)
[1]
>>> zapbook(6)
[5, 3, 1, 6, 4, 2]
>>> zapbook(7)
[]
>>> zapbook(8)
[1, 2, 5, 3, 8, 7, 4, 6]
"""

# book with odd number of pages (except 1) is never zapreadable


if pages < 1 or (pages != 1 and pages % 2):

return []

# second construction method


if 2 ** int(math.log(pages, 2)) == pages:

24
# intialize page numbers of the book
page_numbers = [0] * pages

# assign successive page numbers while zap reading the book


position = 0
for page_number in range(pages):
page_numbers[position] = page_number + 1
position = (position + page_number + 1) % pages

# page_numbers teruggeven
return page_numbers

# first construction method


return list(range(pages - 1, 0, -2)) + list(range(pages, 0, -2))

if __name__ == ’__main__’:
import doctest
doctest.testmod()

6.5 Order and chaos

def group(pile, sizes):

"""
>>> cards = [’KS’, ’4H’, ’KH’, ’JD’, ’10S’, ’2D’, ’9C’, ’JH’]
>>> group(cards, 2)
[(’KS’, ’4H’), (’KH’, ’JD’), (’10S’, ’2D’), (’9C’, ’JH’)]
>>> group(cards, 4)
[(’KS’, ’4H’, ’KH’, ’JD’), (’10S’, ’2D’, ’9C’, ’JH’)]
>>> group(cards, 3)
Traceback (most recent call last):
AssertionError: invalid grouping
>>> group(cards, [2, 4, 2])
[(’KS’, ’4H’), (’KH’, ’JD’, ’10S’, ’2D’), (’9C’, ’JH’)]
>>> group(cards, (3, 2, 3))
[(’KS’, ’4H’, ’KH’), (’JD’, ’10S’), (’2D’, ’9C’, ’JH’)]
>>> group(cards, [3, 1, 3])
Traceback (most recent call last):
AssertionError: invalid grouping
"""

if isinstance(sizes, int):

# check if groups of the given size can be formed


assert (
sizes > 0 and
not len(pile) % sizes
), ’invalid grouping’

# make groups that all have the given size


sizes = [sizes] * (len(pile) // sizes)

elif isinstance(sizes, (list, tuple)):

# check if all sizes are integers


assert all(
isinstance(size, int) and size > 0
for size in sizes
), ’invalid grouping’

# check if the sum of all group sizes equals the number of cards in the
# given pile
assert len(pile) == sum(sizes), ’invalid grouping’

else:

# sizes must be described by an int or a list/tuple of ints

25
raise AssertionError(’invalid grouping’)

# split given pile of cards in groups of given sizes


start = 0
groups = []
for size in sizes:
groups.append(tuple(pile[start:start + size]))
start += size

# geef groups van cards terug


return groups

def riffle_shuffle(pile1, pile2):

"""
>>> cards = [’7C’, ’2D’, ’JC’, ’7H’, ’10S’, ’9D’, ’5C’, ’AH’, ’3C’, ’AD’, ’9C’, ’KD’, ’10C
’, ’QH’, ’JS’, ’4D’, ’AS’, ’8D’]
>>> pile1 = group(cards[:9], 3)
>>> pile1
[(’7C’, ’2D’, ’JC’), (’7H’, ’10S’, ’9D’), (’5C’, ’AH’, ’3C’)]
>>> pile2 = group(cards[9:], [2, 4, 3])
>>> pile2
[(’AD’, ’9C’), (’KD’, ’10C’, ’QH’, ’JS’), (’4D’, ’AS’, ’8D’)]
>>> pile3 = group(cards[9:], [1, 3, 2, 3])
>>> pile3
[(’AD’,), (’9C’, ’KD’, ’10C’), (’QH’, ’JS’), (’4D’, ’AS’, ’8D’)]
>>> riffle_shuffle(pile1, pile2)
[’7C’, ’2D’, ’JC’, ’AD’, ’9C’, ’7H’, ’10S’, ’9D’, ’KD’, ’10C’, ’QH’, ’JS’, ’5C’, ’AH’, ’3C
’, ’4D’, ’AS’, ’8D’]
>>> riffle_shuffle(pile1, pile3)
Traceback (most recent call last):
AssertionError: different number of groups

>>> newPile = riffle_shuffle(pile1, pile2)


>>> mixed_pairs(newPile)
True
>>> mixed_pairs([’7C’, ’2D’, ’JC’, ’7H’, ’10S’, ’9D’, ’5C’, ’AH’, ’3C’])
Traceback (most recent call last):
AssertionError: odd number of cards
"""

# check if both pile have the same number of groups


assert len(pile1) == len(pile2), ’different number of groups’

# shuffle the pile according to the riffle-shuffle procedure


pile = []
for group1, group2 in zip(pile1, pile2):
pile.extend(group1)
pile.extend(group2)

# return new pile obtained after shuffling


return pile

def mixed_pairs(pile):

# check if the pile has an even number of cards


assert not len(pile) % 2, ’odd number of cards’

# check if all pairs of successive cards contain one red and one black card
red = ’DH’
return all(
(card1[-1] in red) != (card2[-1] in red)
for card1, card2 in group(pile, 2)
)

if __name__ == ’__main__’:
import doctest
doctest.testmod()

26
6.6 Bitcoins

def profit(values, actions):

"""
>>> profit([5, 11, 4, 2, 8, 10, 7, 4, 3, 6], ’BS-B-S--BS’)
17
>>> profit((4, 2, 5, 11, 10, 4, 11, 7, 4, 11, 3, 11), ’-B-S-BS-BSBS’)
31
>>> profit([10, 9, 9, 10, 10, 9, 1, 4, 9, 3, 5, 6, 10], ’-B-S--B-SB--S’)
16
>>> profit((12, 4, 9, 5, 6, 7, 9, 9, 11, 7, 10), ’-BSB----SBS’)
14
>>> profit([10, 9, 8, 7, 6, 5, 4, 3, 2, 1], ’----------’)
0
>>> profit((10, 4, 2, 4, 8, 12), ’B---SS’)
Traceback (most recent call last):
AssertionError: invalid actions
"""

# NOTE: this should not be tested explicitly


assert (
isinstance(values, (list, tuple)) and
all(isinstance(value, int) and value >= 0 for value in values)
), ’invalid values’

# define error message


message = ’invalid actions’

# actions described as string and samen number of actions and values


assert isinstance(actions, str) and len(actions) == len(values), message

# no profit and no bitcoin at the start of the period


profit, bitcoin = 0, False

# compute profit based on bitcoin values and check actions throughout the
# period
for value, action in zip(values, actions):
if action == ’B’:
assert not bitcoin, message
bitcoin = True
profit -= value
elif action == ’S’:
assert bitcoin, message
bitcoin = False
profit += value
else:
assert action == ’-’, message

# check that we don’t have a bitcoin at the end of the period


assert not bitcoin, message

return profit

def optimisation(values):

"""
>>> optimisation([5, 11, 4, 2, 8, 10, 7, 4, 3, 6])
(17, ’BS-B-S--BV’)
>>> optimisation ((4, 2, 5, 11, 10, 4, 11, 7, 4, 11, 3, 11))
(31, ’-B-S-BS-BSBV’)
>>> optimisation([10, 9, 9, 10, 10, 9, 1, 4, 9, 3, 5, 6, 10])
(16, ’--B-S-B-SB--V’)
>>> optimisation((12, 4, 9, 5, 6, 7, 9, 9, 11, 7, 10))
(14, ’-BSB----SBV’)
>>> optimisation([10, 9, 8, 7, 6, 5, 4, 3, 2, 1])
(0, ’----------’)
>>> optimisation((10, 4, 2, 4, 8, 12))
(10, ’--B--V’)

27
"""

# NOTE: this should not be tested explicitly


assert (
isinstance(values, (list, tuple)) and
all(isinstance(value, int) and value >= 0 for value in values)
), ’invalid values’

maximal_profit = 0
optimal_actions = ’’

bitcoin = False
for index, value in enumerate(values[:-1]):
if bitcoin:
# only sell bitcoin if it becomes cheaper the next day
if value > values[index + 1]:
bitcoin = False
maximal_profit += value
optimal_actions += ’S’
else:
optimal_actions += ’-’
else:
# only buy bitcoin if it becomes more expensive the next day
if value < values[index + 1]:
bitcoin = True
maximal_profit -= value
optimal_actions += ’B’
else:
optimal_actions += ’-’

# sell bitcoin during the last day if you have one


if bitcoin:
maximal_profit += values[-1]
optimal_actions += ’S’
else:
optimal_actions += ’-’

return maximal_profit, optimal_actions

def maximal_profit(values):

"""
>>> maximal_profit([5, 11, 4, 2, 8, 10, 7, 4, 3, 6])
17
>>> maximal_profit((4, 2, 5, 11, 10, 4, 11, 7, 4, 11, 3, 11))
31
>>> maximal_profit([10, 9, 9, 10, 10, 9, 1, 4, 9, 3, 5, 6, 10])
16
>>> maximal_profit((12, 4, 9, 5, 6, 7, 9, 9, 11, 7, 10))
14
>>> maximal_profit([10, 9, 8, 7, 6, 5, 4, 3, 2, 1])
0
>>> maximal_profit((10, 4, 2, 4, 8, 12))
10
"""

return optimisation(values)[0]

def optimal_actions(values):

"""
>>> optimal_actions([5, 11, 4, 2, 8, 10, 7, 4, 3, 6])
’BS-B-S--BV’
>>> optimal_actions((4, 2, 5, 11, 10, 4, 11, 7, 4, 11, 3, 11))
’-B-S-BS-BSBV’
>>> optimal_actions([10, 9, 9, 10, 10, 9, 1, 4, 9, 3, 5, 6, 10])
’--B-S-B-SB--V’
>>> optimal_actions((12, 4, 9, 5, 6, 7, 9, 9, 11, 7, 10))
’-BSB----SBV’

28
>>> optimal_actions([10, 9, 8, 7, 6, 5, 4, 3, 2, 1])
’----------’
>>> optimal_actions((10, 4, 2, 4, 8, 12))
’--B--V’
"""

return optimisation(values)[1]

if __name__ == ’__main__’:
import doctest
doctest.testmod()

29
7 Series 07: more about functions and modules
7.1 ISBN

def isISBN10(code):

"""
Checks whether the given ISBN-10 code is valid.

>>> isISBN10(’9971502100’)
True
>>> isISBN10(’9971502108’)
False
"""

# helper function for computing ISBN-10 check digit


def check_digit(code):

# compute check digit


check = sum((i + 1) * int(code[i]) for i in range(9)) % 11

# convert check digit into its string representation


return ’X’ if check == 10 else str(check)

# check whether given code is a string


if not isinstance(code, str):
return False

# check whether given code contains 10 characters


if len(code) != 10:
return False

# check whether first nine characters of given code are digits


if not code[:9].isdigit():
return False

# check the check digit


return check_digit(code) == code[-1]

def isISBN13(code):

"""
Checks whether the given ISBN-13 code is valid.

>>> isISBN13(’9789743159664’)
True
>>> isISBN13(’9787954527409’)
False
>>> isISBN13(’8799743159665’)
False
"""

# helper function for computing ISBN-10 check digit


def check_digit(code):

# compute check digit


check = sum((3 if i % 2 else 1) * int(code[i]) for i in range(12))

# convert check digit into a single digit


return str((10 - check) % 10)

# check whether given code is a string


if not isinstance(code, str):
return False

# check whether given code contains 10 characters


if len(code) != 13:
return False

30
# check whether first nine characters of given code are digits
if not code[:12].isdigit():
return False

# check the check digit


return check_digit(code) == code[-1]

def isISBN(code, isbn13=True):

"""
>>> isISBN(’9789027439642’, False)
False
>>> isISBN(’9789027439642’, True)
True
>>> isISBN(’9789027439642’)
True
>>> isISBN(’080442957X’)
False
>>> isISBN(’080442957X’, False)
True
"""

return isISBN13(code) if isbn13 else isISBN10(code)

def areISBN(codes, isbn13=None):

"""
>>> codes = [’0012345678’, ’0012345679’, ’9971502100’, ’080442957X’, 5, True, ’The
Practice of Computing Using Python’, ’9789027439642’, ’5486948320146’]
>>> areISBN(codes)
[False, True, True, True, False, False, False, True, False]
>>> areISBN(codes, True)
[False, False, False, False, False, False, False, True, False]
>>> areISBN(codes, False)
[False, True, True, True, False, False, False, False, False]
"""

# initialize list of checks


checks = []

# construct list of checks


for code in codes:

if isinstance(code, str):

if isbn13 is None:
checks.append(isISBN(code, len(code) == 13))
else:
checks.append(isISBN(code, isbn13))

else:

checks.append(False)

# return list of checks


return checks

if __name__ == ’__main__’:
import doctest
doctest.testmod()

7.2 Brush strokes

def floors(apartments):

"""

31
>>> apartments = (1, 4, 3, 2, 3, 1)
>>> floors(apartments)
[[False, True, False, False, False, False], [False, True, True, False, True, False], [
False, True, True, True, True, False], [True, True, True, True, True, True]]
"""

floor_count, apartment_count = max(apartments), len(apartments)


floors = [[False] * apartment_count for _ in range(floor_count)]
for apartment, height in enumerate(apartments):
for floor in range(height):
floors[-floor - 1][apartment] = True

return floors

def front_view(apartments, width=3, distance=1, apartment=’#’, air=’ ’):

"""
>>> apartments = (1, 4, 3, 2, 3, 1)
>>> print(front_view(apartments, air=’~’))
~~~~###~~~~~~~~~~~~~~~~
~~~~###~###~~~~~###~~~~
~~~~###~###~###~###~~~~
###~###~###~###~###~###
>>> print(front_view([1, 4, 3, 2, 3, 1], width=4, distance=0, apartment=’<’, air="-"))
----<<<<----------------
----<<<<<<<<----<<<<----
----<<<<<<<<<<<<<<<<----
<<<<<<<<<<<<<<<<<<<<<<<<
"""

return ’\n’.join(
(distance * air).join(
width * (apartment if color else air)
for color in floor
)
for floor in floors(apartments)
)

def brush_strokes(apartments):

"""
>>> brush_strokes([1, 4, 3, 2, 3, 1])
5
"""

brush_strokes = 0
for floor in floors(apartments):
brush_stroke = False
for apartment in floor:
if apartment and not brush_stroke:
brush_strokes += 1
brush_stroke = apartment

return brush_strokes

if __name__ == ’__main__’:
import doctest
doctest.testmod()

7.3 Timeless

def first_difference(year1, year2):

"""
>>> first_difference(2018, 2019)
datetime.date(2019, 1, 1)
>>> first_difference(2018, 2024)

32
datetime.date(2024, 2, 29)
>>> first_difference(2018, 2029)
"""

from datetime import date, timedelta

date1 = date(day=1, month=1, year=year1)


date2 = date(day=1, month=1, year=year2)

while (
date1.day == date2.day and
date1.month == date2.month and
date1.year == year1 and
date1.weekday() == date2.weekday()
):
date1 += timedelta(1)
date2 += timedelta(1)

if date1.year == year1:
return date2

def reuse_calendar(year, previous=False):

"""
>>> reuse_calendar(2018)
2029
>>> reuse_calendar(2018, True)
2007
>>> reuse_calendar(2019, previous=False)
2030
>>> reuse_calendar(2019, previous=True)
2013
"""

previous = -1 if previous else 1


year2 = year + previous
while first_difference(year, year2) is not None:
year2 += previous

return year2

def reuse_calendars(year, count, previous=False):

"""
>>> reuse_calendars(2018, 10)
[2029, 2035, 2046, 2057, 2063, 2074, 2085, 2091, 2103, 2114]
>>> reuse_calendars(2018, 10, True)
[2007, 2001, 1990, 1979, 1973, 1962, 1951, 1945, 1934, 1923]
>>> reuse_calendars(2019, 10, previous=False)
[2030, 2041, 2047, 2058, 2069, 2075, 2086, 2097, 2109, 2115]
>>> reuse_calendars(2019, 10, previous=True)
[2013, 2002, 1991, 1985, 1974, 1963, 1957, 1946, 1935, 1929]
"""

calendars = [reuse_calendar(year, previous=previous)]


while len(calendars) < count:
calendars.append(reuse_calendar(calendars[-1], previous=previous))
return calendars

if __name__ == ’__main__’:
import doctest
doctest.testmod()

7.4 ESP game

import random

33
def taboo_length(words, minimum=None, maximum=None):

"""
>>> words = [’forest’, ’meadow’, ’scenery’, ’hills’]
>>> taboo_length(words)
3
>>> taboo_length(words, minimum=2)
4
>>> taboo_length(words, maximum=3)
1
>>> taboo_length(words, minimum=2, maximum=3)
3
>>> taboo_length(words, minimum=-2, maximum=6)
1
"""

# set default value for minimum


if minimum is None or not 0 <= minimum <= len(words):
minimum = 0

# set default value for maximum


if maximum is None or not minimum <= maximum <= len(words):
maximum = len(words)

# pick a random integer from the interval [minimum, maximum]


return random.randint(minimum, maximum)

def taboo_words(words, minimum=None, maximum=None):

"""
>>> words = [’forest’, ’meadow’, ’scenery’, ’hills’]
>>> taboo_words(words)
[’forest’, ’hills’, ’meadow’, ’scenery’]
>>> taboo_words(words, minimum=2)
[’forest’, ’meadow’, ’scenery’]
>>> taboo_words(words, maximum=3)
[’forest’, ’hills’, ’meadow’]
>>> taboo_words(words, minimum=2, maximum=3)
[’hills’, ’meadow’, ’scenery’]
>>> taboo_words(words, minimum=-2, maximum=6)
[’forest’, ’hills’, ’meadow’, ’scenery’]
"""

# choose how many taboo words will be selected


count = taboo_length(words, minimum, maximum)

# randomly pick a selection of taboo words


words = random.sample(words, count)

# sort taboo words in alphabetic order (case insensitive)


words.sort(key=str.lower)

# return list of taboo words


return words

if __name__ == ’__main__’:
import doctest
doctest.testmod()

7.5 Unpredictable birthdays

def birthday(birth, date):

"""
>>> from datetime import date
>>> birthday(date(1969, 10, 5), date(2015, 10, 5))
True

34
>>> birthday(date(1969, 10, 5), date(1989, 10, 4))
False
"""

# determine if the month and the day of the month are the same
return (
date.day == birth.day and
date.month == birth.month
)

def sameweekday(birth, date):

"""
>>> from datetime import date
>>> sameweekday(date(1969, 10, 5), date(2002, 5, 5))
True
>>> sameweekday(date(1969, 10, 5), date(1989, 10, 4))
False
"""

# determine if the day of the month and the weekday are the same
return (
date.day == birth.day and
date.weekday() == birth.weekday()
)

def hundredday(birth, date):

"""
>>> from datetime import date
>>> hundredday(date(1969, 10, 5), date(1975, 10, 14))
True
>>> hundredday(date(1969, 10, 5), date(1989, 10, 4))
False
"""

# determine if the number of days since birth is a multiple of 100


return (date - birth).days % 100 == 0

def unbirthday(birth, date):

"""
>>> from datetime import date
>>> unbirthday(date(1969, 10, 5), date(2015, 10, 5))
False
>>> unbirthday(date(1969, 10, 5), date(1989, 10, 4))
True
"""

# all days that are not birthdays are unbirthdays


return not birthday(birth, date)

"""
alternative solution:

# determine of the month or the day of the month are different


return (
date.day != birth.day or
date.month != birth.month
)
"""

def birthdays(
birth,
birthday=birthday,
start=None,
end=None
):

35
"""
>>> from datetime import date
>>> birthdays(date(1969, 10, 5), end=date(1972, 1, 1))
(datetime.date(1969, 10, 5), datetime.date(1970, 10, 5), datetime.date(1971, 10, 5))
>>> birthdays(date(1969, 10, 5), birthday=sameweekday, start=date(2014, 1, 1))
(datetime.date(2014, 1, 5), datetime.date(2014, 10, 5), datetime.date(2015, 4, 5),
datetime.date(2015, 7, 5))
>>> birthdays(date(1969, 10, 5), start=date(1975, 1, 1), end=date(1976, 1, 1), birthday=
hundredday)
(datetime.date(1975, 3, 28), datetime.date(1975, 7, 6), datetime.date(1975, 10, 14))
"""

from datetime import date, timedelta

# default start date is the birth date


if not start:
start = birth

# default end date is today


if not end:
end = date.today()

# traverse all days between the start date and the end date
days = []
date = start
while date <= end:

# check if a birthday is celebrated on the day


if birthday(birth, date):
days.append(date)

# determine the next day


date += timedelta(1)

# return a tuple containing all birthdays in chronological order


return tuple(days)

if __name__ == ’__main__’:
import doctest
doctest.testmod()

7.6 Burnt pancakes

def flip(stack, burnt=False):

"""
>>> flip((9, 2, 7, 5, 8, 1, 4, 6, 3))
(3, 6, 4, 1, 8, 5, 7, 2, 9)
>>> flip((-9, -2, -7, 5, 8, -1, -4, -6, 3), burnt=True)
(-3, 6, 4, 1, -8, -5, 7, 2, 9)
"""

# flip the stack of pancakes


stack = stack[::-1]

# flip the individual pancakes (if they are burnt)


if burnt:
stack = [-pancake for pancake in stack]

# return the flipped stack of pancakes as a tuple


return tuple(stack)

def flip_top(stack, count, burnt=False):

"""
>>> flip_top((1, 4, 6, 3, 5, 2, 7, 8, 9), 3)
(6, 4, 1, 3, 5, 2, 7, 8, 9)

36
>>> flip_top((6, 4, 1, 3, 5, 2, 7, 8, 9), 6)
(2, 5, 3, 1, 4, 6, 7, 8, 9)
>>> flip_top((-1, -4, -6, 3, -5, -2, 7, 8, 9), 3, burnt=True)
(6, 4, 1, 3, -5, -2, 7, 8, 9)
>>> flip_top((6, 4, 1, 3, -5, -2, 7, 8, 9), 1, burnt=True)
(-6, 4, 1, 3, -5, -2, 7, 8, 9)
>>> flip_top((-6, 4, 1, 3, -5, -2, 7, 8, 9), 6, burnt=True)
(2, 5, -3, -1, -4, 6, 7, 8, 9)
"""

return flip(stack[:count], burnt) + stack[count:]

def find_largest(stack, count):

"""
>>> find_largest((1, 4, 6, 3, 5, 2, 7, 8, 9), 6)
3
>>> find_largest((-1, -4, -6, 3, -5, -2, 7, 8, 9), 6)
3
"""

position, largest = 0, abs(stack[0])


for index in range(1, count):
if abs(stack[index]) > largest:
position, largest = index, abs(stack[index])
return position + 1

def sorting_step(stack, count, burnt=False):

"""
>>> sorting_step((1, 4, 6, 3, 5, 2, 7, 8, 9), 6)
(2, 5, 3, 1, 4, 6, 7, 8, 9)
>>> sorting_step((-1, -4, -6, 3, -5, -2, 7, 8, 9), 6, burnt=True)
(2, 5, -3, -1, -4, 6, 7, 8, 9)
"""

position = find_largest(stack, count)


stack = flip_top(stack, position, burnt)
if burnt and stack[0] > 0:
stack = flip_top(stack, 1, burnt)
return flip_top(stack, count, burnt)

def sorting_steps(stack, burnt=False):

"""
>>> sorting_steps((1, 8, 5, 7, 2, 9, 4, 6, 3))
[(1, 8, 5, 7, 2, 9, 4, 6, 3), (3, 6, 4, 1, 8, 5, 7, 2, 9), (2, 7, 5, 3, 6, 4, 1, 8, 9),
(1, 4, 6, 3, 5, 2, 7, 8, 9), (2, 5, 3, 1, 4, 6, 7, 8, 9), (4, 1, 3, 2, 5, 6, 7, 8, 9),
(2, 3, 1, 4, 5, 6, 7, 8, 9), (1, 2, 3, 4, 5, 6, 7, 8, 9)]
>>> sorting_steps((1, -8, -5, 7, 2, 9, -4, -6, 3), burnt=True)
[(1, -8, -5, 7, 2, 9, -4, -6, 3), (-3, 6, 4, 1, -8, -5, 7, 2, 9), (-2, -7, 5, -3, 6, 4, 1,
8, 9), (-1, -4, -6, 3, -5, -2, 7, 8, 9), (2, 5, -3, -1, -4, 6, 7, 8, 9), (4, 1, 3, 2,
5, 6, 7, 8, 9), (-2, -3, -1, 4, 5, 6, 7, 8, 9), (1, -2, 3, 4, 5, 6, 7, 8, 9), (1, 2,
3, 4, 5, 6, 7, 8, 9)]
>>> sorting_steps((-1, -2, -3, -4, -5), burnt=True)
[(-1, -2, -3, -4, -5), (-1, -2, -3, -4, 5), (-1, -2, -3, 4, 5), (-1, -2, 3, 4, 5), (-1, 2,
3, 4, 5), (1, 2, 3, 4, 5)]
"""

steps = [stack]
for count in range(len(stack), 0, -1):
stack = sorting_step(stack, count, burnt)
if stack != steps[-1]:
steps.append(stack)
return steps

if __name__ == ’__main__’:
import doctest
doctest.testmod()

37
8 Series 08: sets and dictionaries
8.1 ISBN

def isISBN13(code):

"""
Checks whether the given ISBN-13 code is valid.

>>> isISBN13(’9789743159664’)
True
>>> isISBN13(’9787954527409’)
False
>>> isISBN13(’8799743159665’)
False
"""

def check_digit(code):

"""
Helper function that computes the ISBN-13 check digit.
"""

# compute the check digit


check = sum((3 if i % 2 else 1) * int(code[i]) for i in range(12))

# convert the check digit into a single digit


return str((10 - check) % 10)

# check whether the given code is a string


if not isinstance(code, str):
return False

# check whether the given code contains 13 characters


if len(code) != 13:
return False

# check prefix of the given code


if code[:3] not in {’978’, ’979’}:
return False

# check whether all characters of the given code are digits


if not code.isdigit():
return False

# check the check digit


return check_digit(code) == code[-1]

def overview(codes):

"""
>>> codes = [
... ’9789743159664’, ’9785301556616’, ’9797668174969’, ’9781787559554’,
... ’9780817481461’, ’9785130738708’, ’9798810365062’, ’9795345206033’,
... ’9792361848797’, ’9785197570819’, ’9786922535370’, ’9791978044523’,
... ’9796357284378’, ’9792982208529’, ’9793509549576’, ’9787954527409’,
... ’9797566046955’, ’9785239955499’, ’9787769276051’, ’9789910855708’,
... ’9783807934891’, ’9788337967876’, ’9786509441823’, ’9795400240705’,
... ’9787509152157’, ’9791478081103’, ’9780488170969’, ’9795755809220’,
... ’9793546666847’, ’9792322242176’, ’9782582638543’, ’9795919445653’,
... ’9796783939729’, ’9782384928398’, ’9787590220100’, ’9797422143460’,
... ’9798853923096’, ’9784177414990’, ’9799562126426’, ’9794732912038’,
... ’9787184435972’, ’9794455619207’, ’9794270312172’, ’9783811648340’,
... ’9799376073039’, ’9798552650309’, ’9798485624965’, ’9780734764010’,
... ’9783635963865’, ’9783246924279’, ’9797449285853’, ’9781631746260’,
... ’9791853742292’, ’9781796458336’, ’9791260591924’, ’9789367398012’
... ]
>>> overview(codes)

38
English speaking countries: 8
French speaking countries: 4
German speaking countries: 6
Japan: 3
Russian speaking countries: 7
China: 8
Other countries: 11
Errors: 9
"""

# construct histogram of registration groups


groups = {group:0 for group in range(11)}
for code in codes:
group = int(code[3]) if isISBN13(code) else 10
groups[group] += 1

# display overview
print(f’English speaking countries: {groups[0] + groups[1]}’)
print(f’French speaking countries: {groups[2]}’)
print(f’German speaking countries: {groups[3]}’)
print(f’Japan: {groups[4]}’)
print(f’Russian speaking countries: {groups[5]}’)
print(f’China: {groups[7]}’)
print(f’Other countries: {groups[6] + groups[8] + groups[9]}’)
print(f’Errors: {groups[10]}’)

if __name__ == ’__main__’:
import doctest
doctest.testmod()

8.2 Autogram

def letter_frequencies(sentence):

"""
>>> frequentie = letter_frequencies("fifteen e’s, seven f’s, four g’s, six h’s, eight i’s,
four n’s, five o’s, six r’s, eighteen s’s, eight t’s, four u’s, three v’s, two w’s,
three x’s")
>>> frequentie[’e’]
15
>>> frequentie[’f’]
7
>>> frequentie[’g’]
4

>>> frequentie = letter_frequencies("sixteen e’s, five f’s, three g’s, six h’s, nine i’s,
five n’s, four o’s, six r’s, eighteen s’s, eight t’s, three u’s, three v’s, two w’s,
four x’s")
>>> frequentie[’e’]
16
>>> frequentie[’f’]
5
>>> frequentie[’g’]
3
"""

# transform dictionary returned by the function letter_positions into a new


# dictionary, whose values do not represent the set of occurrences but the
# number of occurrences
return {
letter:len(positions)
for letter, positions in letter_positions(sentence).items()
}

"""
# alternative solution:

39
freq = {}
for character in sentence.lower():
if character.isalpha():
freq[character] = freq.get(character, 0) + 1

return freq
"""

def letter_positions(sentence):

"""
>>> positions = letter_positions("fifteen e’s, seven f’s, four g’s, six h’s, eight i’s,
four n’s, five o’s, six r’s, eighteen s’s, eight t’s, four u’s, three v’s, two w’s,
three x’s")
>>> positions[’e’]
{97, 67, 4, 5, 8, 43, 141, 14, 142, 16, 83, 88, 89, 121, 122}
>>> positions[’f’]
{0, 64, 2, 108, 19, 54, 24}
>>> positions[’g’]
{99, 29, 45, 85}

>>> positions = letter_positions("sixteen e’s, five f’s, three g’s, six h’s, nine i’s,
five n’s, four o’s, six r’s, eighteen s’s, eight t’s, three u’s, three v’s, two w’s,
four x’s")
>>> positions[’e’]
{96, 4, 5, 8, 46, 110, 16, 111, 82, 87, 56, 88, 26, 27, 121, 122}
>>> positions[’f’]
{18, 138, 13, 53, 63}
>>> positions[’g’]
{98, 84, 29}
"""

# iterate over all characters in the given sentence and add the position of
# each letter to the dictionary
positions = {}
for index, character in enumerate(sentence.lower()):
if character.isalpha():
if character not in positions:
positions[character] = {index}
else:
positions[character].add(index)

# return a dictionary that maps each letter in the sentence onto the set of
# all positions where that letter occurs in the sentence
return positions

if __name__ == ’__main__’:
import doctest
doctest.testmod()

8.3 Code 39

"""
>>> key = {
... ’U’: ’SbSbSbSsS’, ’Z’: ’SsBsSbBsS’, ’P’: ’BbSsSsBsS’, ’R’: ’SsSbBsBsS’,
... ’H’: ’SsBbBsSsS’, ’W’: ’SbBsSsBsS’, ’D’: ’SbBsSsSsB’, ’K’: ’BsSsSsBbS’,
... ’-’: ’SsSbBsSsB’, ’M’: ’SbSsSbSbS’, ’O’: ’SsSbSsBsB’, ’7’: ’SsSbSbSbS’,
... ’+’: ’BsSsBbSsS’, ’1’: ’SsSsBbBsS’, ’ ’: ’SsBsBbSsS’, ’.’: ’SsBbSsBsS’,
... ’/’: ’SsSsBsBbS’, ’V’: ’SbSsBsBsS’, ’X’: ’SbSbSsSbS’, ’C’: ’SbSsBsSsB’,
... ’Y’: ’BsSsSbBsS’, ’G’: ’BsBsSsSbS’, ’4’: ’SsBbSsSsB’, ’Q’: ’SsBsSbSsB’,
... ’J’: ’SsSsSsBbB’, ’F’: ’BsSsSbSsB’, ’A’: ’SsBsSsSbB’, ’6’: ’BsSsSsSbB’,
... ’2’: ’BsBsSbSsS’, ’$’: ’SsSsSbBsB’, ’0’: ’BsSbBsSsS’, ’N’: ’SsBsBsSbS’,
... ’I’: ’BsSbSsSsB’, ’9’: ’BbSsBsSsS’, ’L’: ’BsSbSsBsS’, ’,’: ’SsSsBsSbB’,
... ’5’: ’BsSsBsSbS’, ’B’: ’BbBsSsSsS’, ’%’: ’SsBsSsBbS’, ’S’: ’BsBbSsSsS’,
... ’3’: ’SbBsBsSsS’, ’T’: ’BbSsSsSsB’, ’*’: ’SsSsBbSsB’, ’E’: ’SbSsSsBsB’
... }
>>> reverse(key)

40
{’SbSbSbSsS’: ’U’, ’SsBsSbBsS’: ’Z’, ’BbSsSsBsS’: ’P’, ’SsSbBsBsS’: ’R’, ’SsBbBsSsS’: ’H’, ’
SbBsSsBsS’: ’W’, ’SbBsSsSsB’: ’D’, ’BsSsSsBbS’: ’K’, ’SsSbBsSsB’: ’-’, ’SbSsSbSbS’: ’M’, ’
SsSbSsBsB’: ’O’, ’SsSbSbSbS’: ’7’, ’BsSsBbSsS’: ’+’, ’SsSsBbBsS’: ’1’, ’SsBsBbSsS’: ’ ’, ’
SsBbSsBsS’: ’.’, ’SsSsBsBbS’: ’/’, ’SbSsBsBsS’: ’V’, ’SbSbSsSbS’: ’X’, ’SbSsBsSsB’: ’C’, ’
BsSsSbBsS’: ’Y’, ’BsBsSsSbS’: ’G’, ’SsBbSsSsB’: ’4’, ’SsBsSbSsB’: ’Q’, ’SsSsSsBbB’: ’J’, ’
BsSsSbSsB’: ’F’, ’SsBsSsSbB’: ’A’, ’BsSsSsSbB’: ’6’, ’BsBsSbSsS’: ’2’, ’SsSsSbBsB’: ’$’, ’
BsSbBsSsS’: ’0’, ’SsBsBsSbS’: ’N’, ’BsSbSsSsB’: ’I’, ’BbSsBsSsS’: ’9’, ’BsSbSsBsS’: ’L’, ’
SsSsBsSbB’: ’,’, ’BsSsBsSbS’: ’5’, ’BbBsSsSsS’: ’B’, ’SsBsSsBbS’: ’%’, ’BsBbSsSsS’: ’S’, ’
SbBsBsSsS’: ’3’, ’BbSsSsSsB’: ’T’, ’SsSsBbSsB’: ’*’, ’SbSsSsBsB’: ’E’}
>>> encoded = code39(’Sulfur, so good.’, key)
>>> encoded

BsBbSsSsSsSbSbSbSsSsBsSbSsBsSsBsSsSbSsBsSbSbSbSsSsSsSbBsBsSsSsSsBsSbBsSsBsBbSsSsBsBbSsSsSsSsSbSsBsBsSsB

>>> decode39(encoded, key)
’SULFUR, SO GOOD.’
"""

def reverse(encoding):

return {
code: character
for character, code in encoding.items()
}

def code39(text, encoding):

return ’s’.join(encoding[character] for character in text.upper())

def decode39(code, encoding):

decoding = reverse(encoding)
return ’’.join(
decoding[code[index:index + 9]]
for index in range(0, len(code), 10)
)

if __name__ == ’__main__’:
import doctest
doctest.testmod()

8.4 Antimagic squares

def numbers(square):

"""
>>> numbers([[2, 7, 6], [9, 5, 1], [4, 3, 8]])
{1, 2, 3, 4, 5, 6, 7, 8, 9}
>>> numbers([[2, 1, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]])
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
>>> numbers([[2, 15, 5, 13], [16, 3, 7, 12], [9, 8, 14, 1], [6, 4, 11, 10]])
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
"""

# iterate over the rows of the square and add all numbers on each row to the
# set of all number in the square
numbers = set()
for row in square:
numbers.update(row)

# return set of distinct numbers in the square


return numbers

def sums(square):

"""
>>> sums([[2, 7, 6], [9, 5, 1], [4, 3, 8]])

41
{15}
>>> sums([[2, 1, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]])
{34, 35, 36, 58, 40, 10, 42, 26, 29, 31}
>>> sums([[2, 15, 5, 13], [16, 3, 7, 12], [9, 8, 14, 1], [6, 4, 11, 10]])
{32, 33, 34, 35, 36, 37, 38, 29, 30, 31}
"""

# determine dimension of square


n = len(square)

# initialize set of distinct sums


sums = set()

# determine sums of rows and columns


for i in range(n):

# i-th row
sums.add(sum(square[i]))

# i-th column
sums.add(sum(square[j][i] for j in range(n)))

# determine sum of main diagonal


sums.add(sum(square[i][i] for i in range(n)))

# determine sum of antidiagonal


sums.add(sum(square[i][-i - 1] for i in range(n)))

# return set of distinct sums


return sums

def ismagic(square):

"""
>>> ismagic([[2, 7, 6], [9, 5, 1], [4, 3, 8]])
True
>>> ismagic([[2, 1, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]])
False
>>> ismagic([[2, 15, 5, 13], [16, 3, 7, 12], [9, 8, 14, 1], [6, 4, 11, 10]])
False
"""

# magic square: all numbers distinct and all sums equal


return (
len(numbers(square)) == len(square) ** 2 and
len(sums(square)) == 1
)

def ishetero(square):

"""
>>> ishetero([[2, 7, 6], [9, 5, 1], [4, 3, 8]])
False
>>> ishetero([[2, 1, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]])
True
>>> ishetero([[2, 15, 5, 13], [16, 3, 7, 12], [9, 8, 14, 1], [6, 4, 11, 10]])
True
"""

# heterosquare: all numbers and all sums distinct


return (
len(numbers(square)) == len(square) ** 2 and
len(sums(square)) == 2 * len(square) + 2
)

def isantimagic(square):

"""
>>> isantimagic([[2, 7, 6], [9, 5, 1], [4, 3, 8]])

42
False
>>> isantimagic([[2, 1, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]])
False
>>> isantimagic([[2, 15, 5, 13], [16, 3, 7, 12], [9, 8, 14, 1], [6, 4, 11, 10]])
True
"""

# antimagic square is first of all also heterosquare


if not ishetero(square):
return False

# check if sorted list of sums is a sequence of consecutive integers


ascending = sorted(sums(square))
for i in range(len(ascending) - 1):
if ascending[i] + 1 != ascending[i + 1]:
return False

# all conditions for antimagic squares are fulfilled


return True

if __name__ == ’__main__’:
import doctest
doctest.testmod()

8.5 A reflexive rainbow

def letter_value(letters):

"""
>>> letter_value(’OYCDHBNKEgtvuialwr’)
{’O’: -9, ’Y’: -8, ’C’: -7, ’D’: -6, ’H’: -5, ’B’: -4, ’N’: -3, ’K’: -2, ’E’: -1, ’G’: 1,
’T’: 2, ’V’: 3, ’U’: 4, ’I’: 5, ’A’: 6, ’L’: 7, ’W’: 8, ’R’: 9}
>>> letter_value(list(’oycdhbnkeGTVUIALWR’))
{’O’: -9, ’Y’: -8, ’C’: -7, ’D’: -6, ’H’: -5, ’B’: -4, ’N’: -3, ’K’: -2, ’E’: -1, ’G’: 1,
’T’: 2, ’V’: 3, ’U’: 4, ’I’: 5, ’A’: 6, ’L’: 7, ’W’: 8, ’R’: 9}
>>> letter_value(tuple(’oycdhbnkeGTVUIALWR’))
{’O’: -9, ’Y’: -8, ’C’: -7, ’D’: -6, ’H’: -5, ’B’: -4, ’N’: -3, ’K’: -2, ’E’: -1, ’G’: 1,
’T’: 2, ’V’: 3, ’U’: 4, ’I’: 5, ’A’: 6, ’L’: 7, ’W’: 8, ’R’: 9}
>>> letter_value(’abc’)
Traceback (most recent call last):
AssertionError: invalid letters
>>> letter_value(’abba’)
Traceback (most recent call last):
AssertionError: invalid letters
"""

# define error message


message = ’invalid letters’

# convert given letters into string (check if this is possible)


if isinstance(letters, (list, tuple)):
assert all(isinstance(letter, str) for letter in letters), message
letters = ’’.join(letters)
else:
assert isinstance(letters, str), message

# convert letters to uppercase


letters = letters.upper()

# check if there’s an even number of different letters


assert (
letters.isalpha() and # letters only
not len(letters) % 2 and # even number of letters
len(letters) == len(set(letters)) # no duplicates
), message

# return dictionary that maps uppercase letters onto their letter value

43
return {
letter: (
index - len(letters) // 2
if index < len(letters) // 2
else index - len(letters) // 2 + 1
)
for index, letter in enumerate(letters)
}

def word_value(word, letters):

"""
>>> word_value(’black’, ’OYCDHBNKEGTVUIALWR’)
0
>>> word_value(’BROWN’, ’oycdhbnkeGTVUIALWR’)
1
>>> word_value(’red’, ’OYCDHBNKEgtvuialwr’)
2
>>> word_value(’ORANGE’, ’oycdhbnkegtvuialwr’)
3
>>> word_value(’yellow’, ’OYCDHBNKEGTVUIALWR’)
4
>>> word_value(’GREEN’, ’oycdhbnkeGTVUIALWR’)
5
>>> word_value(’blue’, ’OYCDHBNKEgtvuialwr’)
6
>>> word_value(’VIOLET’, ’oycdhbnkegtvuialwr’)
7
>>> word_value(’GRAY’, ’OYCDHBNKEGTVUIALWR’)
8
>>> word_value(’white’, ’oycdhbnkeGTVUIALWR’)
9
>>> word_value(’SILVER’, ’OYCDHBNKEGTVUIALWR’)
Traceback (most recent call last):
AssertionError: invalid word
"""

# construct dictionary that maps uppercase letters onto their letter value
waarde = letter_value(letters)

# check if given word is valid


assert (
isinstance(word, str) and
all(letter in waarde for letter in word.upper())
), ’invalid word’

# compute word value as the sum of all letter values


return sum(waarde[letter] for letter in word.upper())

def rainbow(colors, letters):

"""
>>> rainbow([’BLACK’, ’brown’, ’RED’, ’orange’, ’YELLOW’, ’green’, ’BLUE’, ’violet’, ’GRAY
’, ’White’], ’OYCDHBNKEgtvuialwr’)
True
>>> rainbow([’BLACK’, ’YELLOW’, ’violet’, ’green’, ’White’, ’orange’, ’GRAY’, ’BLUE’, ’RED
’, ’brown’], ’OYCDHBNKEgtvuialwr’)
False
>>> rainbow((’BLACK’, ’brown’, ’RED’, ’orange’, ’YELLOW’, ’green’, ’BLUE’, ’violet’, ’GRAY
’, ’White’), ’bwdiucankYOGTHELRV’)
False
"""

# check if word value of each word corresponds to position of the word


return all(
index == word_value(color, letters)
for index, color in enumerate(colors)
)

44
# NOTE: this function is not part of the exam
def reflect(colors, letters):

"""
>>> colors = [’BLACK’, ’YELLOW’, ’violet’, ’green’, ’White’, ’orange’, ’GRAY’, ’BLUE’, ’
RED’, ’brown’]
>>> reflect(colors, ’OYCDHBNKEgtvuialwr’)
>>> colors
[’BLACK’, ’brown’, ’RED’, ’orange’, ’YELLOW’, ’green’, ’BLUE’, ’violet’, ’GRAY’, ’White’]

>>> colors = [’BLACK’, ’YELLOW’, ’violet’, ’green’, ’White’, ’orange’, ’GRAY’, ’BLUE’, ’
RED’, ’brown’]
>>> reflect(colors, ’bwdiucankYOGTHELRV’)
>>> colors
[’BLACK’, ’brown’, ’BLUE’, ’White’, ’RED’, ’GRAY’, ’orange’, ’YELLOW’, ’green’, ’violet’]
"""

# in place sorting of colors according to increasing word value; words


# having the same word value are sorted in alphabetic order
colors.sort(key=lambda color: (word_value(color, letters), color.upper()))

def reflected(colors, letters):

"""
>>> colors = [’BLACK’, ’YELLOW’, ’violet’, ’green’, ’White’, ’orange’, ’GRAY’, ’BLUE’, ’
RED’, ’brown’]
>>> reflected(colors, ’OYCDHBNKEgtvuialwr’)
(’BLACK’, ’brown’, ’RED’, ’orange’, ’YELLOW’, ’green’, ’BLUE’, ’violet’, ’GRAY’, ’White’)
>>> colors
[’BLACK’, ’YELLOW’, ’violet’, ’green’, ’White’, ’orange’, ’GRAY’, ’BLUE’, ’RED’, ’brown’]

>>> colors = (’BLACK’, ’YELLOW’, ’violet’, ’green’, ’White’, ’orange’, ’GRAY’, ’BLUE’, ’
RED’, ’brown’)
>>> reflected(colors, ’bwdiucankYOGTHELRV’)
(’BLACK’, ’brown’, ’BLUE’, ’White’, ’RED’, ’GRAY’, ’orange’, ’YELLOW’, ’green’, ’violet’)
"""

# return tuple containing given colors that are sorted in increasing order
# according to their word value; words having the same word value are sorted
# in alphabetic order
colors = list(colors)
reflect(colors, letters)
return tuple(colors)

"""
# alternatieve oplossing
return tuple(
sorted(
colors,
key=lambda color: (word_value(color, letters), color.upper())
)
)
"""

if __name__ == ’__main__’:
import doctest
doctest.testmod()

8.6 Fuse

def list_representation(digits, rows, cols=None):

"""
>>> grid = list_representation(’1221133113322222’, 4)
>>> grid
[[1, 2, 2, 1], [1, 3, 3, 1], [1, 3, 3, 2], [2, 2, 2, 2]]
"""

45
# square grid by default
if cols is None:
cols = rows

# contruct and return the list representation of the grid


return [
[int(digits[row * cols + col]) for col in range(cols)]
for row in range(rows)
]

def string_representation(grid):

"""
>>> grid = [[1, 2, 2, 1], [1, 3, 3, 1], [1, 3, 3, 2], [2, 2, 2, 2]]
>>> string_representation(grid)
(’1221133113322222’, 4, 4)
"""

rows, cols = len(grid), len(grid[0])


string_representation = ’’.join(
’’.join(str(digit) for digit in row)
for row in grid
)

return string_representation, rows, cols

def move(increment, cells, grid):

"""
>>> grid = [[1, 2, 2, 1], [1, 3, 3, 1], [1, 3, 3, 2], [2, 2, 2, 2]]
>>> move(-1, {(1, 2), (1, 1), (2, 1), (2, 2)}, grid)
[[1, 2, 2, 1], [1, 2, 2, 1], [1, 2, 2, 2], [2, 2, 2, 2]]
>>> move(+1, {(0, 3), (1, 3)}, grid)
[[1, 2, 2, 2], [1, 2, 2, 2], [1, 2, 2, 2], [2, 2, 2, 2]]
>>> move(+1, {(2, 0), (1, 0), (0, 0)}, grid)
[[2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2]]
"""

for (r, k) in cells:


grid[r][k] += increment

return grid

def is_solved(grid):

"""
>>> grid = [[2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2]]
>>> is_solved(grid)
True
"""

numbers = set()
for row in grid:
numbers.update(row)

return len(numbers) == 1

def group(position, grid):

"""
>>> grid = [[1, 2, 2, 1], [1, 3, 3, 1], [1, 3, 3, 2], [2, 2, 2, 2]]
>>> group((2, 1), grid)
{(1, 2), (1, 1), (2, 1), (2, 2)}
>>> group((0, 3), grid)
{(0, 3), (1, 3)}
>>> group((1, 0), grid)
{(2, 0), (1, 0), (0, 0)}
"""

46
group, todo = set(), {position}
rows, cols = len(grid), len(grid[0])
number = grid[position[0]][position[1]]
while todo:
r, k = todo.pop()
group.add((r, k))
for dr, dk in ((0, 1), (0, -1), (1, 0), (-1, 0)):
if (
0 <= r + dr < rows and
0 <= k + dk < cols and
(r + dr, k + dk) not in group and
grid[r + dr][k + dk] == number
):
todo.add((r + dr, k + dk))

return group

def is_solution(moves, grid):

"""
>>> grid = [[1, 2, 2, 1], [1, 3, 3, 1], [1, 3, 3, 2], [2, 2, 2, 2]]
>>> is_solution([(1, 1, False), (3, 2, False)], grid)
True
>>> grid
[[1, 2, 2, 1], [1, 3, 3, 1], [1, 3, 3, 2], [2, 2, 2, 2]]
>>> is_solution([(1, 3, True), (3, 2, False), (0, 1, True)], grid)
False
>>> grid
[[1, 2, 2, 1], [1, 3, 3, 1], [1, 3, 3, 2], [2, 2, 2, 2]]
"""

# deep copy of the grid


grid = [row[:] for row in grid]

# perform moves
for (row, col, increment) in moves:
move(
+1 if increment else -1,
group((row, col), grid),
grid
)

return is_solved(grid)

if __name__ == ’__main__’:
import doctest
doctest.testmod()

47

You might also like