Basis¶
Een eeuwigdurende kalender¶
Een eeuwigdurende kalender vertelt je op welke weekdag een willekeurige datum valt. Wie op 13 juni 1997 is geboren, leest er af dat dat een vrijdag was: vrijdag de dertiende, zelfs.
In deze opgave breid je de klasse Date uit het college uit tot zo’n kalender.
Je objecten van het type Date kunnen straks uitrekenen hoeveel dagen er tussen
twee datums liggen, en op welke weekdag een datum valt. Aan het eind zorg je dat
code van buiten de klasse de datum van een Date niet meer kan veranderen:
alleen de methoden van de klasse doen dat nog.
De klasse Date¶
Je begint met de klasse uit het college. Ze heeft drie attributen: self.day,
self.month en self.year, en naast de constructor en __repr__ één eigen
methode, is_leap_year.
Voer de cel hieronder uit. In deze cel voeg je in elke stap een methode toe. Voer hem daarna steeds opnieuw uit, en dan de testcel van die stap.
In de testcellen staat repr(d). Dat geeft de string die __repr__ teruggeeft:
dezelfde die print(d) afdrukt.
class Date:
"""Een datum: dag, maand en jaar."""
def __init__(self, day, month, year):
"""Maak een datum met de gegeven dag, maand en jaar."""
self.day = day
self.month = month
self.year = year
def __repr__(self):
"""Geeft de datum als string, zoals 02/12/2020."""
return f"{self.day:02d}/{self.month:02d}/{self.year:04d}"
def is_leap_year(self):
"""Geeft True als de datum in een schrikkeljaar valt."""
if self.year % 400 == 0:
return True
elif self.year % 100 == 0:
return False
elif self.year % 4 == 0:
return True
return False
Wat je gaat maken¶
Stap |
Methode |
Doet |
|---|---|---|
1 |
- |
klasse en object uit elkaar houden |
2 |
|
een nieuw object met dezelfde datum maken |
3 |
|
twee datums op waarde vergelijken |
4 |
|
kijken of een datum eerder valt |
5 |
|
kijken of een datum later valt |
6 |
|
een dag vooruit |
7 |
|
een dag terug |
8 |
|
|
9 |
|
|
10 |
|
het aantal dagen tussen twee datums |
11 |
|
de dag van de week |
12 |
- |
beoordelen wat er misgaat als iedereen bij de datum kan |
13 |
|
de datum afschermen |
Stap 1: klasse en object¶
Een klasse is het ontwerp: ze legt vast welke attributen en methoden elk object krijgt. Een object is één exemplaar volgens dat ontwerp, met eigen waarden voor die attributen. Bekijk deze drie regels:
ny = Date(1, 1, 2021)
nc = Date(1, 1, 2100)
d = ny
Beantwoord in de cel hieronder, als commentaar:
Hoeveel klassen en hoeveel objecten komen er in deze drie regels voor?
Welke waarde heeft het attribuut
yearbijny, en welke bijnc?De methode
is_leap_yearstaat één keer in de klasse. Toch geeftny.is_leap_year()een ander antwoord danDate(1, 1, 2024).is_leap_year(). Hoe kan dat?
Hint
Een nieuw object ontstaat alleen als je de klasse aanroept, zoals in
Date(1, 1, 2021). a is b geeft True als a en b hetzelfde object
aanwijzen: twee namen voor één object.
Voer daarna de testcel uit, en kijk of de uitvoer klopt met je antwoorden.
# jouw antwoorden
ny = Date(1, 1, 2021)
nc = Date(1, 1, 2100)
d = ny
print(ny is d)
print(ny is nc)
print(ny.year, nc.year)
print(ny.is_leap_year(), Date(1, 1, 2024).is_leap_year())
Stap 2: copy(self)¶
Geeft een nieuw object terug met dezelfde dag, maand en jaar. Deze methode krijg je cadeau; neem haar over in je klasse:
def copy(self):
"""Geeft een nieuw object met dezelfde dag, maand en jaar."""
return Date(self.day, self.month, self.year)
Om een nieuw object te maken, roep je de constructor aan: Date(...) binnen de
methode doet precies hetzelfde als daarbuiten.
Het verschil met een gewone toekenning:
Code |
Wat er gebeurt |
|---|---|
|
geen nieuw object: |
|
een nieuw object, met dezelfde datum als |
d = Date(1, 1, 2100)
d2 = d
assert d2 is d
d3 = d.copy()
assert d3 is not d
assert repr(d3) == "01/01/2100"
Stap 3: equals(self, d2)¶
Geeft True als self en d2 dezelfde kalenderdatum voorstellen, ook als het
twee verschillende objecten zijn. Ook deze methode krijg je cadeau:
def equals(self, d2):
"""Geeft True als self en d2 dezelfde kalenderdatum voorstellen."""
return self.year == d2.year and self.month == d2.month and self.day == d2.day
Hier zie je het verschil tussen waarde en identiteit. Bij je eigen
objecten vergelijkt == of het hetzelfde object is: de identiteit, net als
d is d2. equals vergelijkt de waarde: dezelfde dag, maand en jaar.
Aanroep |
Resultaat |
|---|---|
|
|
|
|
d = Date(1, 1, 2100)
d2 = d.copy()
assert d.equals(d2)
assert d.equals(Date(1, 1, 2100))
assert not d.equals(Date(2, 1, 2100))
assert not d == d2
Stap 4: is_before(self, d2)¶
Geeft True als self een eerdere datum is dan d2. Bij dezelfde datum, of bij
een latere, geeft de methode False.
Aanroep |
Resultaat |
|---|---|
|
|
|
|
|
|
Hint
Vergelijk eerst de jaren. Vergelijk de maanden alleen als de jaren gelijk zijn, en de dagen alleen als de jaren en de maanden allebei gelijk zijn.
ny = Date(1, 1, 2021)
d = Date(2, 12, 2020)
assert not ny.is_before(d)
assert d.is_before(ny)
assert not d.is_before(d)
assert not Date(1, 3, 2020).is_before(Date(2, 2, 2020))
assert Date(31, 12, 2019).is_before(Date(1, 1, 2020))
Stap 5: is_after(self, d2)¶
Geeft True als self een latere datum is dan d2. Bij dezelfde datum, of bij
een eerdere, geeft de methode False.
Aanroep |
Resultaat |
|---|---|
|
|
|
|
|
|
Hint
Je kunt is_after op dezelfde manier schrijven als is_before. Korter is het om
is_before en equals te gebruiken: een datum die niet eerder is en niet
gelijk, is later.
ny = Date(1, 1, 2021)
d = Date(2, 12, 2020)
assert ny.is_after(d)
assert not d.is_after(ny)
assert not d.is_after(d)
assert Date(1, 3, 2020).is_after(Date(2, 2, 2020))
Stap 6: tomorrow(self)¶
Verandert het object zodat het een dag later voorstelt. De methode geeft
niets terug. self.day verandert altijd; self.month en self.year
soms.
Voor |
Na |
|---|---|
|
|
|
|
|
|
|
|
Hint
Een lijst met het aantal dagen per maand is handig:
dim = [0, 31, fdays, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
fdays is 29 in een schrikkeljaar en anders 28; dat regel je met een gewone
if-else en is_leap_year. Zie je waarom die 0 aan het begin handig is?
Bedenk hoe lijsten geïndexeerd worden.
d = Date(31, 12, 2020)
d.tomorrow()
assert repr(d) == "01/01/2021"
d = Date(28, 2, 2020)
d.tomorrow()
assert repr(d) == "29/02/2020"
d.tomorrow()
assert repr(d) == "01/03/2020"
d = Date(28, 2, 2021)
d.tomorrow()
assert repr(d) == "01/03/2021"
Stap 7: yesterday(self)¶
Verandert het object zodat het een dag eerder voorstelt, en geeft net als
tomorrow niets terug.
Voor |
Na |
|---|---|
|
|
|
|
|
|
|
|
d = Date(1, 1, 2021)
d.yesterday()
assert repr(d) == "31/12/2020"
d = Date(1, 3, 2020)
d.yesterday()
assert repr(d) == "29/02/2020"
d.yesterday()
assert repr(d) == "28/02/2020"
d = Date(1, 3, 2021)
d.yesterday()
assert repr(d) == "28/02/2021"
Stap 8: add_n_days(self, n)¶
Verandert het object zodat het n dagen later voorstelt, en drukt onderweg elke
datum af, de startdatum eerst. n is nooit negatief. De methode geeft niets
terug.
d = Date(2, 12, 2020)
d.add_n_days(4)
drukt af:
02/12/2020
03/12/2020
04/12/2020
05/12/2020
06/12/2020
Met d.add_n_days(1318) kom je van 02/12/2020 uit op 12/07/2024.
Let op
Kopieer hiervoor geen code uit tomorrow. Bedenk in plaats daarvan hoe je
tomorrow kunt aanroepen in een for-lus. Een object drukt zichzelf af met
print(self).
d = Date(2, 12, 2020)
d.add_n_days(4)
assert repr(d) == "06/12/2020"
d.add_n_days(0)
assert repr(d) == "06/12/2020"
Stap 9: sub_n_days(self, n)¶
Het spiegelbeeld van add_n_days: verandert het object zodat het n dagen
eerder voorstelt, en drukt onderweg elke datum af. Gebruik yesterday in een
for-lus.
Voor |
Na |
|---|---|
|
|
d = Date(6, 12, 2020)
d.sub_n_days(4)
assert repr(d) == "02/12/2020"
Stap 10: diff(self, d2)¶
Geeft het aantal dagen tussen self en d2 terug, als integer. Het teken telt:
Geval |
Resultaat |
|---|---|
|
|
|
een negatief aantal dagen |
|
een positief aantal dagen |
Aanroep |
Resultaat |
|---|---|
|
|
|
|
|
|
Belangrijk
diff mag self en d2 niet veranderen. Maak kopieën en werk alleen met
die:
self_copy = self.copy()
d2_copy = d2.copy()
Hint
Trek niet de jaren, maanden en dagen van elkaar af: daar zitten te veel
uitzonderingen in. Gebruik tomorrow of yesterday in een while-lus, met een
conditie als while self_copy.is_before(d2_copy):. Een verzamelvariabele telt
hoe vaak de lus draait; dat is je antwoord, eventueel met een minteken.
Met diff kun je je eigen leeftijd in dagen uitrekenen. Andere verschillen
controleer je op https://www.timeanddate.com/date/duration.html.
d = Date(2, 12, 2020)
d2 = Date(19, 7, 2021)
assert d2.diff(d) == 229
assert d.diff(d2) == -229
assert d.diff(d) == 0
assert repr(d) == "02/12/2020"
assert repr(d2) == "19/07/2021"
assert Date(15, 3, 2020).diff(Date(1, 12, 2019)) == 105
assert d.diff(Date(1, 1, 1899)) == 44530
assert d.diff(Date(1, 1, 2100)) == -28884
Stap 11: dow(self)¶
Geeft de dag van de week (day of week) terug waarop de datum valt, als een van
de strings "Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
"Saturday" of "Sunday".
Aanroep |
Resultaat |
|---|---|
|
|
|
|
|
|
Hint
Neem een datum waarvan je de weekdag kent, zoals zondag 10 oktober 2010, en
bereken met diff hoeveel dagen je datum daarvan af ligt. Hoe helpt de
modulo-operator % je dan verder?
assert Date(7, 12, 1941).dow() == "Sunday"
assert Date(28, 10, 1929).dow() == "Monday"
assert Date(19, 10, 1987).dow() == "Monday"
assert Date(1, 1, 2100).dow() == "Friday"
assert Date(13, 6, 1997).dow() == "Friday"
Stap 12: een datum die niet bestaat¶
Tot nu toe kan iedereen de attributen van een Date rechtstreeks veranderen.
Voer deze code uit:
d = Date(28, 2, 2021)
d.day = 30
print(d, d.dow())
d.tomorrow()
print(d, d.dow())
Beantwoord in de cel hieronder, als commentaar:
Bestaat de datum die als eerste wordt afgedrukt?
Welke datums en weekdagen drukt de code af? Kan dat kloppen? Welke methode van
Dateheeft hier een fout gemaakt?Welke methoden van
Dateveranderen de datum van een object? Kan een van die methoden van een bestaande datum een datum maken die niet bestaat?Welke regel code heeft de datum dan wel kapotgemaakt, en waarom kon de klasse dat niet tegenhouden?
# jouw antwoorden
Stap 13: de datum afschermen¶
In stap 12 zag je dat de methoden van Date van een bestaande datum nooit een
onbestaande maken. Code van buiten de klasse kan dat wel, zolang iedereen
day, month en year kan veranderen. Dat sluit je nu af. Dat is
encapsulatie: de staat van het object is verborgen, en alleen zijn methoden
komen eraan.
Pas je klasse zo aan dat dag, maand en jaar van buiten wel te lezen zijn, maar niet te veranderen:
Sla ze in de constructor op als
self._day,self._monthenself._year.Geef de klasse drie properties,
day,monthenyear, zonder setter.Zoek in je methoden elke plek waar dag, maand of jaar een nieuwe waarde krijgt, en schrijf daar
self._day,self._monthofself._year.
Hint
Zoek op self.day =, self.day += en self.day -=, en hetzelfde voor month
en year. Waar een methode alleen leest, zoals __repr__, mag self.day blijven
staan: dat gaat nu via de property.
Voer na de testcel hieronder ook de testcellen van stap 2 tot en met 11 nog eens
uit; die horen allemaal te blijven slagen. De code van stap 12 geeft nu een
foutmelding: AttributeError: property 'day' of 'Date' object has no setter.
Precies de bedoeling.
Eén deur staat nog open: de constructor. Date(30, 2, 2021) maakt nog steeds
een datum die niet bestaat. Hoe een constructor zo’n aanroep weigert, met
raise, leer je in week 7.
d = Date(28, 2, 2021)
assert d.day == 28
d.tomorrow()
assert repr(d) == "01/03/2021"
assert d.day == 1
assert d.month == 3
assert d.year == 2021
assert Date(2, 12, 2020).diff(Date(1, 12, 2019)) == 367
Tot slot¶
Je klasse Date is nu een eeuwigdurende kalender. Alle regels over dagen,
maanden en schrikkeljaren staan op één plek, in de klasse, en elke verandering
van een bestaande datum loopt via een methode die die regels kent. Alleen de
constructor neemt nog elke datum aan; daar kom je in week 7 op terug. In het practicum ontwerp je zo’n klasse vanaf
het begin: een wezen dat zijn eigen levenspunten bewaakt.