Basis¶
Het burgerservicenummer¶
Opdracht: Het burgerservicenummer
from string import digits
def is_digit(c):
"""Geeft True als het teken een cijfer is"""
return c in digits
assert is_digit("7") is True
assert is_digit("0") is True
assert is_digit("x") is False
digits uit de module string is niets anders dan de string "0123456789". De
operator in kijkt of een teken daarin voorkomt, en geeft dus meteen een True
of False terug.
Dit is ook de reden dat je een variabele nooit string moet noemen: dan kun je
de module niet meer importeren.
def digit_at(bsn, i):
"""Geeft het cijfer op index i terug als getal"""
return int(bsn[i])
assert digit_at("900000132", 0) == 9
assert digit_at("900000132", 7) == 3
assert digit_at("900000132", 8) == 2
Eén regel, maar hij geeft de bewerking een naam. Dat scheelt hieronder negen keer
int(bsn[...]) schrijven, en als er ooit iets aan die omzetting verandert hoeft
het maar op één plek.
def weighted_sum(bsn):
"""Geeft de som van de cijfers maal het gewicht van hun index"""
result = 0
result = result + digit_at(bsn, 0) * 9
result = result + digit_at(bsn, 1) * 8
result = result + digit_at(bsn, 2) * 7
result = result + digit_at(bsn, 3) * 6
result = result + digit_at(bsn, 4) * 5
result = result + digit_at(bsn, 5) * 4
result = result + digit_at(bsn, 6) * 3
result = result + digit_at(bsn, 7) * 2
result = result + digit_at(bsn, 8) * -1
return result
assert weighted_sum("900000132") == 88
assert weighted_sum("900000123") == 85
assert weighted_sum("123456789") == 147
result begint op nul en krijgt er per regel één term bij. Dat is één regel meer
dan één lange optelling, en daar staat wat tegenover.
Alle negen regels zien er hetzelfde uit; alleen de index en het gewicht verschillen. Een fout in een gewicht doorbreekt die rij en valt daardoor op. In één expressie van negen termen staat dezelfde regelmaat er wel, maar tussen de haakjes en de plussen is zij lastiger te zien.
De nul op de eerste regel hoort erbij. Zonder startwaarde is er niets om de eerste term bij op te tellen, en zou die eerste regel anders moeten zijn dan de acht erna. Volgende week vervang je deze negen regels door een lus die negen keer hetzelfde doet, en dan is die startwaarde het eerste wat je opschrijft.
def has_nine_digits(bsn):
"""Geeft True als het nummer uit precies negen cijfers bestaat"""
return (
len(bsn) == 9
and is_digit(bsn[0])
and is_digit(bsn[1])
and is_digit(bsn[2])
and is_digit(bsn[3])
and is_digit(bsn[4])
and is_digit(bsn[5])
and is_digit(bsn[6])
and is_digit(bsn[7])
and is_digit(bsn[8])
)
assert has_nine_digits("900000132") is True
assert has_nine_digits("90000013") is False
assert has_nine_digits("90000013x") is False
Beide voorwaarden zijn nodig. Alleen op lengte controleren laat "90000013x"
door, en alleen op cijfers controleren laat een nummer van acht cijfers door.
De lengtecontrole staat vooraan en dat is geen kwestie van smaak: staat hij
achteraan, dan wordt bsn[8] opgevraagd op een string die maar acht tekens
heeft. Python stopt dan met een foutmelding in plaats van False terug te geven.
def is_valid(bsn):
"""Geeft True als het nummer negen cijfers heeft en de elfproef doorstaat"""
if not has_nine_digits(bsn):
return False
return weighted_sum(bsn) % 11 == 0
assert is_valid("900000132") is True
assert is_valid("999999990") is True
assert is_valid("900000123") is False
assert is_valid("123456789") is False
assert is_valid("90000013") is False
Om dezelfde reden staat de vormcontrole hier vooraan en geeft hij meteen False
terug: weighted_sum leest alle negen cijfers, dus op een korter nummer loopt
hij vast.
def check(bsn):
"""Geeft een omschrijving van de uitkomst van de controle"""
if not has_nine_digits(bsn):
return "geen negen cijfers"
if not is_valid(bsn):
return "mislukt op de elfproef"
return "geldig"
assert check("900000132") == "geldig"
assert check("90000013") == "geen negen cijfers"
assert check("900000123") == "mislukt op de elfproef"
Deze functie beslist niets zelf; hij vertaalt alleen wat de andere functies al vaststellen. Daardoor staat de rekenregel op precies één plek, en verandert er verder niets als die regel ooit wordt aangepast.
Dat is wat het opdelen in zes functies oplevert. Elke functie is in één zin uit te leggen en apart te testen, en de laatste maakt er een antwoord van.
De parkeerautomaat¶
De opgave vroeg om de opdeling zelf te maken. Deze uitwerking gebruikt drie functies, en de knip zit op de plekken waar de vraag verandert: eerst is het gratis, dan hoeveel uren zijn er begonnen, en pas dan wat kost dat. Een andere verdeling kan ook goed zijn, zolang elke functie één ding doet.
def is_free(minutes):
"""Geeft True als het parkeren binnen de gratis periode valt"""
return minutes <= 15
assert is_free(0) is True
assert is_free(15) is True
assert is_free(16) is False
Vijftien minuten is nog gratis en zestien niet, dus de vergelijking is <= en
niet <. Beide kanten van die grens staan als assertion in de cel: dat is het
soort fout dat je later niet meer terugvindt.
def started_hours(minutes):
"""Geeft het aantal begonnen uren in minutes"""
if minutes % 60 == 0:
return minutes // 60
return minutes // 60 + 1
assert started_hours(60) == 1
assert started_hours(61) == 2
assert started_hours(0) == 0
assert started_hours(1) == 1
Een begonnen uur is niet hetzelfde als een heel uur. Zestig minuten is één uur, maar eenenzestig minuten zijn er twee, want het tweede uur is begonnen.
minutes // 60 telt de hele uren. Blijft er iets over, dan is er nog een uur
begonnen en komt er één bij. Nul minuten is het randgeval: er is dan niets
begonnen, en dus is het antwoord nul en niet één.
def parking_fee(minutes):
"""Geeft het te betalen bedrag in hele euro's voor minutes parkeren"""
if is_free(minutes):
return 0
fee = started_hours(minutes) * 2
if fee > 12:
return 12
return fee
assert parking_fee(0) == 0
assert parking_fee(15) == 0
assert parking_fee(16) == 2
assert parking_fee(60) == 2
assert parking_fee(61) == 4
assert parking_fee(359) == 12
assert parking_fee(600) == 12
Deze functie rekent bijna niets zelf uit. Ze stelt de drie regels uit de opgave op volgorde: eerst de gratis periode, dan het tarief, dan het maximum.
Dat de volgorde ertoe doet zie je aan parking_fee(0). Zonder de eerste if
zou er 0 // 60 * 2 uitkomen, wat toevallig ook 0 is - maar bij twaalf minuten
gaat het al mis, want dan rekent hij twee euro voor iets dat gratis is.
Het maximum staat achteraan en niet in started_hours, want dat is een regel
over het bedrag en niet over de tijd. Wordt het dagtarief ooit dertien euro, dan
verandert er precies één regel.