Basis

Deze pagina bevat uitvoerbare code.

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

  1. Eén klasse, Date, en twee objecten. Date(...) staat er twee keer, en alleen een aanroep van de klasse maakt een object. d = ny geeft het object van ny een tweede naam.

  2. Bij ny is year 2021, bij nc 2100. Elk object heeft zijn eigen waarden voor dezelfde attributen.

  3. De methode staat één keer in de klasse, maar self is bij elke aanroep een ander object. Bij ny.is_leap_year() rekent de methode met het jaar van ny, 2021; bij Date(1, 1, 2024).is_leap_year() met 2024.

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
  1. Nee. Februari 2021 heeft 28 dagen; 30/02/2021 bestaat niet.

  2. Wat er precies verschijnt, hangt af van hoe je tomorrow en diff hebt geschreven. Deze uitwerking drukt 30/02/2021 Tuesday en 01/03/2021 Monday af: een dinsdag met een maandag als volgende dag. Andere oplossingen die alle tests halen, geven bijvoorbeeld twee keer Monday, of 31/02/2021 als volgende dag. Geen van die uitkomsten kan kloppen. Toch heeft geen enkele methode een fout gemaakt. dow en tomorrow rekenen correct met wat ze krijgen, maar ze krijgen een datum die niet bestaat, en dan komt er onzin uit.

  3. tomorrow, yesterday, add_n_days en sub_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. copy maakt een nieuw object met dezelfde, bestaande datum.

  4. d.day = 30. Die regel komt van buiten de klasse en gaat langs alle methoden heen. De klasse kan het niet tegenhouden, omdat day een 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