Basis

Deze pagina bevat uitvoerbare code.

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.