Il codice originale del nostro piccolo programma, con le correzione della pillola precedente, dovrebbe avere quest’aspetto:

from datetime import datetime

def is_leap(year):
    return year % 4 == 0 and year % 100 != 0

def __increase(start, stop, items):
    c = 0
    for a in range(start, stop):
        c += items[a]
    return c


def int_input(msg, min, max):
    try:
        r = int(input(f"{msg} [{min}-{max}]? "))
        if r < min or r > max:
            raise Exception()
        return r
    except Exception:
        print(f"Error: Please insert an integer from {min} to {max}")
        return int_input(msg, min, max)


def calc_gap(source_year, source_month, source_day, target_month, target_day):
    month_days = [ 31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]

    if source_month == target_month:
        # Sto lavorando su un intervallo breve dello stesso mese di partenza e di arrivo
        if source_day < target_day:
            return target_day - source_day

    month_days[2] = 29 if is_leap(year) else 28
    gap = target_day
    if source_month < target_month:
        gap += __increase(source_month, target_month, month_days)
    else:
        gap += __increase(source_month, len(month_days), month_days)
        month_days[2] = 29 if is_leap(year + 1) else 28
        gap += __increase(1, target_month, month_days)
    gap -= source_day
    return gap


if __name__ == '__main__':
    year = int(datetime.today().strftime('%Y'))
    month = int(datetime.today().strftime('%m'))
    day = int(datetime.today().strftime('%d'))

    target_month = int_input("Target month", 1, 12)
    target_day = int_input("Target day", 1, 31)

    print(f"{calc_gap(year, month, day, target_month, target_day)} days to go")

calc_gap fa piuttosto paura. cinque parametri! Facile scordarsi l’ordine con cui vanno inseriti. Possiamo chiamarli per nome, ma tutto diventa ancora più prolisso!

calc_gap(source_year = year, source_month = month, target_month=tgt_month, source_day = day, target_day = tgt_day)

Faticoso! Oltretutto in questo codice ci sono funzioni e variabili che hanno senso soltanto qui. Sarebbe notevole dare una struttura più solida e circoscritta! Possiamo farlo definendo una classe.

class DaysGap:
    month_days = [ 31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]

    def __init__(self):
        self.source_year = datetime.today().year
        self.source_month = datetime.today().month
        self.source_day = datetime.today().day

Questa non è la classe completa ma definisce molti concetti chiave.

class DaysGap: definisce una classe con nome DaysGap per convenzione in Python le classi usano al notazione CamelCase. In cui tutte le parole hanno l’iniziale maiuscola e GliSpaziSpariscono. Come vedete nomi composti, come il nostro, restano abbastanza leggibili.

In Python tutte le classi hanno un def __init__(self, ...) se non c’è è implicito. Si tratta del costruttore della classe, grazie a lui possiamo istanziare oggetti. Si noti la presenza di self, all’interno di una classe consente di accedere espressamente a metodi (le funzioni) e proprietà (le variabili) definite nella classe. Complicato? Prendiamo per esempio self.source_year = datetime.today().year associa un valore alla property e lo rende accessibile da tutti i metodi della classe.

Notare anche che abbiamo modificato il modo di caricare il valore dell’anno. Così è più immediato e corretto (non ho bisogno di passare da una stringa per ottenere il valore intero) l’approccio usato prima però ci ha fatto imparare diverse cosette.

from datetime import datetime

class DaysGap:
    month_days = [ 31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]
    
    def __init__(self):
        self.source_year = datetime.today().year
        self.source_month = datetime.today().month
        self.source_day = datetime.today().day
        
    def is_leap(self):
        return self.source_year % 4 == 0 and self.source_year % 100 != 0

    def __increase(self, start, stop, items):
        c = 0
        for a in range(start, stop):
            c += items[a]
        return c

    def calc_gap(self, target_month, target_day):
        if self.source_month == target_month:
            # Sto lavorando su un intervallo breve dello stesso mese di partenza e di arrivo
            if self.source_day < target_day:
                return target_day - self.source_day
        
        self.month_days[2] = 29 if self.is_leap() else 28
        gap = target_day
        if self.source_month < target_month:
            gap += self.__increase(self.source_month, target_month, self.month_days)
        else:
            gap += self.__increase(self.source_month, len(self.month_days), self.month_days)
            self.source_year += 1
            self.month_days[2] = 29 if self.is_leap() else 28
            gap += self.__increase(1, target_month, self.month_days)
        gap -= self.source_day
        return gap

Questa è la nostra classe completa. Siete invitati a confrontare con la versione precedente alla ricerca di tutte le differenze. Intanto abbiamo dovuto indentare tutto il codice, che deve stare all’interno della definizione della classe class DaysGap:e poi ci sono self un po’ dappertutto.

Guardate soprattutto la definizione di is_leap. In origine prendeva il parametro year, ora prende unicamente self e all’interno usa self.source_year. Il risultato però è che altrove nel codice della classe abbiamo dovuto scrivere:

self.source_year += 1
self.month_days[2] = 29 if self.is_leap() else 28

Potrebbe migliorare? Direi di sì

    def is_leap(self, year=None):
        if year is None:
            year = self.source_year
        return year % 4 == 0 and year % 100 != 0

Ora funziona con o senza l’anno assegnato esplicitamente!

Notare il (non) valore None, per quasi tutti gli altri linguaggi è null. Significa che la variabile (o il parametro) non è assegnato. E’ diverso da False o da 0 (zero), e notate che viene confrontato con l’operatore is.

Manca un pezzo di codice, eccolo.

def int_input(msg, min, max):
    try:
        r = int(input(f"{msg} [{min}-{max}]? "))
        if r < min or r > max:
            raise Exception()
        return r
    except Exception:
        print(f"Error: Please insert an integer from {min} to {max}")
        return int_input(msg, min, max)

if __name__ == '__main__':
    target_month = int_input("Target month", 1, 12)
    target_day = int_input("Target day", 1, 31)

    days_gap = DaysGap()
    print(f"{days_gap.calc_gap(target_month, target_day)} days to go")

Notate che int_input(...) non è stato trasformato in un metodo della nostra classe. Il motivo è che non fa parte del contesto di cui si occupa la classe, non ha nulla a che fare con le date, esegue unicamente un controllo sul dato in input.

La parte più importante è però alla fine: days_gap = DaysGap() rende days_gap l’istanza della nostra classe DaysGap sotto il cofano DaysGap() altro non fa che chiamare il costruttore __init__(self). Si noti tuttavia che DaysGap() non ha alcun parametro! e che self, di fatto, viene restituito dal costruttore e per noi diventa days_gap. Anche days_gap.calc_gap(target_month, target_day) non considera affatto self. Tutto nasce però sempre da days_gap.calc_gap(...) è come se avessimo chiamato DaysGap.calc_gap(days_gap, ...) ed è vero. Provate! Ma poi usate la sintassi originale molto più chiara e stringata.

Parecchi miglioramenti ma anche un problema insidioso che vedremo prossimamente.