Basis¶
Een eeuwigdurende kalender¶
Opgave: Een eeuwigdurende kalender
De klasse hieronder bevat alle methoden van stap 2 tot en met 11. Stap 13 staat verderop als een nieuwe versie van dezelfde klasse, zodat je beide kunt vergelijken.
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
def copy(self):
"""Geeft een nieuw object met dezelfde dag, maand en jaar."""
return Date(self.day, self.month, self.year)
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
def is_before(self, d2):
"""Geeft True als self eerder valt dan d2."""
if self.year != d2.year:
return self.year < d2.year
if self.month != d2.month:
return self.month < d2.month
return self.day < d2.day
def is_after(self, d2):
"""Geeft True als self later valt dan d2."""
return not self.is_before(d2) and not self.equals(d2)
def tomorrow(self):
"""Verandert de datum in de dag erna."""
if self.is_leap_year():
fdays = 29
else:
fdays = 28
dim = [0, 31, fdays, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
self.day += 1
if self.day > dim[self.month]:
self.day = 1
self.month += 1
if self.month > 12:
self.month = 1
self.year += 1
def yesterday(self):
"""Verandert de datum in de dag ervoor."""
if self.is_leap_year():
fdays = 29
else:
fdays = 28
dim = [0, 31, fdays, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
self.day -= 1
if self.day < 1:
self.month -= 1
if self.month < 1:
self.month = 12
self.year -= 1
self.day = dim[self.month]
def add_n_days(self, n):
"""Verandert de datum in n dagen later en drukt elke datum onderweg af."""
print(self)
for i in range(n):
self.tomorrow()
print(self)
def sub_n_days(self, n):
"""Verandert de datum in n dagen eerder en drukt elke datum onderweg af."""
print(self)
for i in range(n):
self.yesterday()
print(self)
def diff(self, d2):
"""Geeft het aantal dagen van d2 tot self; negatief als self eerder valt."""
self_copy = self.copy()
d2_copy = d2.copy()
count = 0
while self_copy.is_before(d2_copy):
self_copy.tomorrow()
count -= 1
while self_copy.is_after(d2_copy):
self_copy.yesterday()
count += 1
return count
def dow(self):
"""Geeft de dag van de week als Engelse string, zoals "Monday"."""
days = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
]
return days[self.diff(Date(10, 10, 2010)) % 7]
Stap 1: klasse en object¶
Eén klasse,
Date, en twee objecten.Date(...)staat er twee keer, en alleen een aanroep van de klasse maakt een object.d = nygeeft het object vannyeen tweede naam.Bij
nyisyear2021, bijnc2100. Elk object heeft zijn eigen waarden voor dezelfde attributen.De methode staat één keer in de klasse, maar
selfis bij elke aanroep een ander object. Bijny.is_leap_year()rekent de methode met het jaar vanny,2021; bijDate(1, 1, 2024).is_leap_year()met2024.
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())
True
False
2021 2100
False True
Stap 2: copy(self)¶
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)¶
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)¶
Is het jaar verschillend, dan beslist het jaar. Anders beslist de maand, en pas als die ook gelijk is de dag.
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)¶
Een datum die niet eerder valt en niet gelijk is, valt 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)¶
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)¶
Het spiegelbeeld van tomorrow. Het verschil zit in de volgorde: eerst de
maand terugzetten, en pas dan opzoeken hoeveel dagen die maand heeft.
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)¶
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"
02/12/2020
03/12/2020
04/12/2020
05/12/2020
06/12/2020
06/12/2020
Stap 9: sub_n_days(self, n)¶
d = Date(6, 12, 2020)
d.sub_n_days(4)
assert repr(d) == "02/12/2020"
06/12/2020
05/12/2020
04/12/2020
03/12/2020
02/12/2020
Stap 10: diff(self, d2)¶
Hooguit één van de twee lussen draait. Is self eerder, dan loopt de kopie
vooruit en telt count naar beneden; is self later, dan loopt de kopie
terug en telt count op.
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)¶
10 oktober 2010 was een zondag. Het verschil in dagen, modulo 7, is dan de
index in een lijst die met "Sunday" begint. Ook voor datums vóór 2010 werkt
dat: % geeft in Python bij een negatief getal een uitkomst van 0 tot en met 6.
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¶
d = Date(28, 2, 2021)
d.day = 30
print(d, d.dow())
d.tomorrow()
print(d, d.dow())
30/02/2021 Tuesday
01/03/2021 Monday
Nee. Februari 2021 heeft 28 dagen;
30/02/2021bestaat niet.Wat er precies verschijnt, hangt af van hoe je
tomorrowendiffhebt geschreven. Deze uitwerking drukt30/02/2021 Tuesdayen01/03/2021 Mondayaf: een dinsdag met een maandag als volgende dag. Andere oplossingen die alle tests halen, geven bijvoorbeeld twee keerMonday, of31/02/2021als volgende dag. Geen van die uitkomsten kan kloppen. Toch heeft geen enkele methode een fout gemaakt.dowentomorrowrekenen correct met wat ze krijgen, maar ze krijgen een datum die niet bestaat, en dan komt er onzin uit.tomorrow,yesterday,add_n_daysensub_n_days. Ze gaan dag voor dag en kennen de lengte van elke maand, dus van een bestaande datum maken ze altijd weer een bestaande datum.copymaakt een nieuw object met dezelfde, bestaande datum.d.day = 30. Die regel komt van buiten de klasse en gaat langs alle methoden heen. De klasse kan het niet tegenhouden, omdatdayeen gewoon attribuut is waar iedereen bij kan.
Stap 13: de datum afschermen¶
Alleen de regels die dag, maand of jaar een nieuwe waarde geven, zijn
veranderd: in de constructor, in tomorrow en in yesterday. Alle andere
methoden lezen self.day, en dat gaat nu via de property.
class Date:
"""Een datum: dag, maand en jaar, die van buiten alleen te lezen zijn."""
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
def copy(self):
"""Geeft een nieuw object met dezelfde dag, maand en jaar."""
return Date(self.day, self.month, self.year)
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
def is_before(self, d2):
"""Geeft True als self eerder valt dan d2."""
if self.year != d2.year:
return self.year < d2.year
if self.month != d2.month:
return self.month < d2.month
return self.day < d2.day
def is_after(self, d2):
"""Geeft True als self later valt dan d2."""
return not self.is_before(d2) and not self.equals(d2)
def tomorrow(self):
"""Verandert de datum in de dag erna."""
if self.is_leap_year():
fdays = 29
else:
fdays = 28
dim = [0, 31, fdays, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
self._day += 1
if self.day > dim[self.month]:
self._day = 1
self._month += 1
if self.month > 12:
self._month = 1
self._year += 1
def yesterday(self):
"""Verandert de datum in de dag ervoor."""
if self.is_leap_year():
fdays = 29
else:
fdays = 28
dim = [0, 31, fdays, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
self._day -= 1
if self.day < 1:
self._month -= 1
if self.month < 1:
self._month = 12
self._year -= 1
self._day = dim[self.month]
def add_n_days(self, n):
"""Verandert de datum in n dagen later en drukt elke datum onderweg af."""
print(self)
for i in range(n):
self.tomorrow()
print(self)
def sub_n_days(self, n):
"""Verandert de datum in n dagen eerder en drukt elke datum onderweg af."""
print(self)
for i in range(n):
self.yesterday()
print(self)
def diff(self, d2):
"""Geeft het aantal dagen van d2 tot self; negatief als self eerder valt."""
self_copy = self.copy()
d2_copy = d2.copy()
count = 0
while self_copy.is_before(d2_copy):
self_copy.tomorrow()
count -= 1
while self_copy.is_after(d2_copy):
self_copy.yesterday()
count += 1
return count
def dow(self):
"""Geeft de dag van de week als Engelse string, zoals "Monday"."""
days = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
]
return days[self.diff(Date(10, 10, 2010)) % 7]
@property
def day(self):
"""De dag, alleen om te lezen."""
return self._day
@property
def month(self):
"""De maand, alleen om te lezen."""
return self._month
@property
def year(self):
"""Het jaar, alleen om te lezen."""
return self._year
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
De tests van stap 2 tot en met 11 slagen ook met deze versie:
d = Date(1, 1, 2100)
d2 = d
assert id(d) == id(d2)
d3 = d.copy()
assert id(d) != id(d3)
assert repr(d3) == "01/01/2100"
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
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))
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))
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"
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"
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"
d = Date(6, 12, 2020)
d.sub_n_days(4)
assert repr(d) == "02/12/2020"
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
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"
02/12/2020
03/12/2020
04/12/2020
05/12/2020
06/12/2020
06/12/2020
06/12/2020
05/12/2020
04/12/2020
03/12/2020
02/12/2020
En de regel uit stap 12 wordt nu geweigerd:
d = Date(28, 2, 2021)
d.day = 30
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[17], line 2
1 d = Date(28, 2, 2021)
----> 2 d.day = 30
AttributeError: property 'day' of 'Date' object has no setter