Basis¶
De eeuwigdurende kalender, met operatoren¶
Opgave: De eeuwigdurende kalender, met operatoren
De klasse hieronder bevat alle methoden van stap 1 tot en met 7. De nieuwe
methoden staan tussen dow en de properties; de constructor is vervangen.
class Date:
"""Een datum die bestaat, met operatoren; dag, maand en jaar zijn alleen te lezen."""
def __init__(self, day, month, year):
"""Maak een datum; gooit een ValueError als die datum niet bestaat."""
self._day = day
self._month = month
self._year = year
if self.is_leap_year():
fdays = 29
else:
fdays = 28
dim = [0, 31, fdays, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
if month < 1 or month > 12 or day < 1 or day > dim[month]:
raise ValueError(f"{self} bestaat niet")
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]
def __eq__(self, other):
"""Geeft True als other een Date is met dezelfde kalenderdatum."""
if not isinstance(other, Date):
return False
return self.equals(other)
def __lt__(self, other):
"""Geeft True als self eerder valt dan other."""
if not isinstance(other, Date):
raise TypeError("een Date is alleen met een Date te vergelijken")
return self.is_before(other)
def __gt__(self, other):
"""Geeft True als self later valt dan other."""
if not isinstance(other, Date):
raise TypeError("een Date is alleen met een Date te vergelijken")
return self.is_after(other)
def __iadd__(self, n):
"""Verandert de datum in n dagen later, zonder iets af te drukken."""
if not isinstance(n, int):
raise TypeError("bij een Date tel je alleen een integer op")
for i in range(n):
self.tomorrow()
return self
def __isub__(self, n):
"""Verandert de datum in n dagen eerder, zonder iets af te drukken."""
if not isinstance(n, int):
raise TypeError("van een Date trek je alleen een integer af")
for i in range(n):
self.yesterday()
return self
def __sub__(self, other):
"""Geeft het aantal dagen van other tot self, zoals diff."""
if not isinstance(other, Date):
raise TypeError("van een Date trek je met - alleen een Date af")
return self.diff(other)
@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
Stap 1: __eq__(self, other)¶
__eq__ gebruikt equals. Het verschil zit in de eerste regels: equals gaat
ervan uit dat d2 een Date is, __eq__ controleert het en geeft anders
False.
d = Date(1, 1, 2100)
d2 = d.copy()
assert d == d2
assert d is not d2
assert not d == Date(2, 1, 2100)
assert d != Date(2, 1, 2100)
assert not d == "01/01/2100"
Stap 2: __lt__(self, other) en __gt__(self, other)¶
Beide gebruiken een methode die er al was: is_before en is_after. Zonder
__gt__ zou ny > d ook werken: Python draait het dan om naar d < ny. De
opgave vraagt beide, naast is_before en is_after.
ny = Date(1, 1, 2021)
d = Date(2, 12, 2020)
assert d < ny
assert not ny < d
assert not d < d
assert ny > d
assert not d > ny
assert not d > d
Stap 3: __iadd__(self, n)¶
__iadd__ verschuift de datum met tomorrow in een lus, net als add_n_days,
maar zonder af te drukken. d2 wijst na d += 1318 nog steeds hetzelfde
object aan als d: += heeft het object veranderd, en er is geen nieuw object
gemaakt.
d = Date(28, 2, 2021)
d += 3
assert repr(d) == "03/03/2021"
d += 0
assert repr(d) == "03/03/2021"
d2 = d
d += 1318
assert repr(d) == "11/10/2024"
assert d2 is d
got_error = False
try:
d += 1.5
except TypeError:
got_error = True
assert got_error
Stap 4: waarom return self¶
Zonder return self geeft __iadd__ niets terug, dus None. Python schrijft
d += 3 als d = d.__iadd__(3), en dus wijst d daarna None aan. Het object
zelf is wel veranderd, maar er wijst geen naam meer naar. Een subklasse zonder
return self laat het zien:
class ForgetfulDate(Date):
"""Een datum waarvan __iadd__ vergeet self terug te geven."""
def __iadd__(self, n):
"""Verandert de datum in n dagen later, maar geeft niets terug."""
for i in range(n):
self.tomorrow()
d = ForgetfulDate(28, 2, 2021)
d += 3
print(d)
None
Stap 5: __isub__(self, n)¶
Het spiegelbeeld van __iadd__, met yesterday.
d = Date(3, 3, 2021)
d -= 3
assert repr(d) == "28/02/2021"
d = Date(1, 1, 2021)
d -= 366
assert repr(d) == "01/01/2020"
got_error = False
try:
d -= "drie"
except TypeError:
got_error = True
assert got_error
Stap 6: __sub__(self, other)¶
__sub__ roept diff aan. d - 5 geeft de TypeError uit __sub__: je kunt
van een datum met - alleen een andere datum aftrekken. Vijf dagen terug is
d -= 5, en dat verandert d zelf.
d = Date(2, 12, 2020)
d2 = Date(19, 7, 2021)
assert d2 - d == 229
assert d - d2 == -229
assert d - d == 0
assert repr(d) == "02/12/2020"
assert repr(d2) == "19/07/2021"
d = Date(2, 12, 2020)
print(d - 5)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[8], line 2
1 d = Date(2, 12, 2020)
----> 2 print(d - 5)
Cell In[1], line 160, in Date.__sub__(self, other)
157 def __sub__(self, other):
158 """Geeft het aantal dagen van other tot self, zoals diff."""
159 if not isinstance(other, Date):
--> 160 raise TypeError("van een Date trek je met - alleen een Date af")
161 return self.diff(other)
TypeError: van een Date trek je met - alleen een Date af
Stap 7: een datum die niet bestaat, weigeren¶
De constructor zet eerst de attributen, zodat is_leap_year het jaar kan lezen,
en controleert daarna de maand en de dag. De maand komt eerst: bij maand 13
zou dim[month] zelf een fout geven. De melding gebruikt __repr__, dus
Date(30, 2, 2021) geeft ValueError: 30/02/2021 bestaat niet.
tomorrow en yesterday gaan niet via de constructor, maar die maken van een
bestaande datum altijd een bestaande datum. copy en dow roepen de constructor
wel aan, met een datum die bestaat.
assert repr(Date(29, 2, 2024)) == "29/02/2024"
assert repr(Date(31, 12, 2021)) == "31/12/2021"
assert repr(Date(1, 1, 2000)) == "01/01/2000"
d = Date(28, 2, 2021)
d += 1
assert repr(d) == "01/03/2021"
got_error = False
try:
Date(0, 5, 2024)
except ValueError:
got_error = True
assert got_error
d = Date(30, 2, 2021)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[10], line 1
----> 1 d = Date(30, 2, 2021)
Cell In[1], line 15, in Date.__init__(self, day, month, year)
11 else:
12 fdays = 28
13 dim = [0, 31, fdays, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
14 if month < 1 or month > 12 or day < 1 or day > dim[month]:
---> 15 raise ValueError(f"{self} bestaat niet")
ValueError: 30/02/2021 bestaat niet
Stap 8: datums uit tekst inlezen¶
De try staat binnen de lus, om één datum. Eén except ValueError vangt drie
soorten fouten af: een datum die niet bestaat (de constructor), een tekst
zonder twee schuine strepen (het uitpakken van text.split("/") in drie
namen), en een deel dat geen getal is (int). De cel drukt af:
overgeslagen: 31/04/2024
overgeslagen: 29/02/2023
overgeslagen: 15/13/2024
overgeslagen: 00/05/2024
overgeslagen: 01-05-2024
def parse_dates(texts):
"""Zet strings als 30/01/2024 om naar Date-objecten, en slaat ongeldige over."""
dates = []
for text in texts:
try:
day, month, year = text.split("/")
dates.append(Date(int(day), int(month), int(year)))
except ValueError:
print("overgeslagen:", text)
return dates
texts = [
"30/01/2024",
"31/04/2024",
"29/02/2024",
"29/02/2023",
"15/13/2024",
"00/05/2024",
"01-05-2024",
"1/5/2024",
]
dates = parse_dates(texts)
assert [repr(d) for d in dates] == ["30/01/2024", "29/02/2024", "01/05/2024"]
assert parse_dates([]) == []
overgeslagen: 31/04/2024
overgeslagen: 29/02/2023
overgeslagen: 15/13/2024
overgeslagen: 00/05/2024
overgeslagen: 01-05-2024
Tot slot¶
Alle regels over datums staan nog steeds in de klasse. De operatoren voegen geen nieuwe regels toe, maar roepen methoden aan die er al waren.