"""
Corso Machine Learning Engineer - Modulo 01, Lezione 1
"Python per i dati 1: ambiente e fondamenti"

DOMINIO DI QUESTO FILE
----------------------
Una **stazione meteo simulata**. I dati NON sono reali: sono inventati qui
dentro e dichiarati tali. La stazione registra una lettura ogni 90 minuti,
a partire dalla mezzanotte, e tre operatori (Anna, Berto, Carla) si danno il
cambio a rotazione, una lettura ciascuno.

Da questo piccolo mondo escono tre esercizi sulle tre idee della lezione:

  1. la divisione intera `//` e il resto `%` sono definiti col PAVIMENTO,
     quindi funzionano anche all'indietro nel tempo (minuti negativi);
  2. lo stesso `%` fa "girare" un indice su una lista corta: è il modo
     idiomatico di gestire una rotazione, anche con indici negativi;
  3. `zip` tronca IN SILENZIO alla sequenza più corta: su due colonne di
     dati disallineate perdi righe senza nessun errore.

COME SI USA
-----------
    python3 esercizi.py

Il file gira anche così com'è: stampa quali esercizi mancano ancora.
Completa i TODO uno alla volta, in qualunque ordine, e rilancia. Le soluzioni
di riferimento sono commentate in fondo: guardale solo dopo aver provato.

AMBIENTE DICHIARATO
-------------------
Eseguito con Python 3.14.4. Serve solo la libreria standard: nessun pacchetto
da installare, nessun numero che dipenda dalla versione.

    python3 -m venv .venv && source .venv/bin/activate
"""

# ----------------------------------------------------------------------
# Dati simulati della stazione (dichiarati come simulati)
# ----------------------------------------------------------------------

MINUTI_AL_GIORNO = 24 * 60          # 1440
PASSO_LETTURE = 90                  # una lettura ogni 90 minuti
OPERATORI = ("Anna", "Berto", "Carla")

# Otto letture di temperatura in decimi di grado, numeri INTERI per scelta:
# così ogni media e ogni differenza resta esatta e verificabile a mano.
TEMPERATURE_DECIMI = [118, 124, 131, 145, 152, 149, 137, 126]

# Gli orari corrispondenti, in minuti dalla mezzanotte: 0, 90, 180, ...
ORARI_MINUTI = [k * PASSO_LETTURE for k in range(len(TEMPERATURE_DECIMI))]


# ----------------------------------------------------------------------
# ESERCIZIO 1 - L'orologio che sa andare all'indietro
#
# La centrale chiede: "che ora era 150 minuti PRIMA della mezzanotte?".
# In minuti dalla mezzanotte quel momento vale -150. La conversione in
# "HH:MM" deve funzionare per qualunque intero: negativo, dentro la
# giornata, oltre le 24 ore.
#
# L'identità da sfruttare è quella della lezione:
#     a == (a // b) * b + (a % b)
# con `//` definito come PAVIMENTO e il resto che prende il segno del
# divisore. Quindi `m % 1440` è SEMPRE fra 0 e 1439, anche se m è negativo:
# il minuto "rientra" nella giornata da solo, senza nessun `if`.
#
# Domanda a cui rispondere a parole, dopo aver visto i numeri:
#   perché `int(m / 1440)` NON va bene al posto di `m // 1440`?
# ----------------------------------------------------------------------

def minuti_a_orario(m):
    """Converte minuti dalla mezzanotte in una stringa "HH:MM".

    Parametri
    ---------
    m : int, anche negativo o maggiore di 1440.

    Ritorna
    -------
    str nella forma "HH:MM" con due cifre per campo (es. "21:30"),
    oppure None se l'esercizio non è ancora svolto.

    Suggerimento: prima riporta m dentro la giornata con `m % 1440`,
    poi separa ore e minuti con `divmod(..., 60)`.
    Per la stampa a due cifre: f"{ore:02d}:{minuti:02d}".
    """
    # TODO (esercizio 1): sostituisci `return None` con la conversione.
    return None


# ----------------------------------------------------------------------
# ESERCIZIO 2 - Il turno che gira
#
# Gli operatori si alternano: la lettura 0 è di Anna, la 1 di Berto, la 2
# di Carla, la 3 di nuovo di Anna. La domanda interessante è all'indietro:
# chi aveva fatto la lettura -1, cioè l'ultima di ieri?
#
# Con tre operatori, `-1 % 3` vale 2 (NON -1): il resto ha il segno del
# divisore, quindi l'indice "gira" correttamente anche verso sinistra.
#
# Domanda: che cosa restituirebbe questa funzione se, invece di `%`,
# usassi una catena di `if` scritta in fretta?
# ----------------------------------------------------------------------

def operatore_di_turno(indice, operatori=OPERATORI):
    """Nome dell'operatore che esegue la lettura numero `indice`.

    Parametri
    ---------
    indice : int, anche negativo (letture del giorno prima).
    operatori : sequenza di nomi, in ordine di turno.

    Ritorna
    -------
    str con il nome, oppure None se l'esercizio non è ancora svolto.

    Suggerimento: una sola espressione, con l'indice riportato
    nell'intervallo valido dal resto della divisione.
    """
    # TODO (esercizio 2): sostituisci `return None` con la selezione.
    return None


# ----------------------------------------------------------------------
# ESERCIZIO 3 - Le righe che `zip` fa sparire
#
# La stazione scrive due file separati: uno con gli orari, uno con le
# temperature. Se un file è più corto (un'interruzione di corrente ha
# troncato la scrittura), `zip(orari, valori)` NON protesta: si ferma alla
# sequenza più corta e le righe in eccesso svaniscono.
#
# Scrivi la versione prudente: restituisce le coppie E quante righe sono
# andate perse, così il disallineamento diventa un numero da riportare
# invece di un silenzio.
#
# Domanda: `zip(a, b, strict=True)` solleva un errore. Quando preferisci
# l'errore e quando preferisci il conteggio?
# ----------------------------------------------------------------------

def abbina_prudente(orari, valori):
    """Abbina orari e valori segnalando il disallineamento.

    Parametri
    ---------
    orari, valori : due sequenze, non necessariamente della stessa lunghezza.

    Ritorna
    -------
    tupla (coppie, n_perse) dove `coppie` è la lista delle coppie
    (orario, valore) effettivamente abbinate e `n_perse` è il numero di
    elementi rimasti fuori dalla sequenza più lunga.
    Oppure None se l'esercizio non è ancora svolto.

    Suggerimento: le coppie sono `list(zip(orari, valori))`; le perse sono
    la differenza fra la lunghezza massima e quella minima.
    """
    # TODO (esercizio 3): sostituisci `return None` con il calcolo.
    return None


# ----------------------------------------------------------------------
# Runner: stampa i risultati degli esercizi svolti e segnala quelli mancanti
# ----------------------------------------------------------------------

def _sezione(titolo):
    print()
    print(titolo)
    print("-" * len(titolo))


def _da_fare(numero):
    print(f"  [ ] Esercizio {numero} non ancora svolto: completa il TODO qui sopra.")


def esegui_esercizio_1():
    _sezione("Esercizio 1 - l'orologio che sa andare all'indietro")
    print("  minuti |  // 1440 |  % 1440 | orario")
    for m in (-150, 0, 90, 630, 1439, 1530):
        orario = minuti_a_orario(m)
        if orario is None:
            _da_fare(1)
            return False
        print(f"  {m:6d} | {m // MINUTI_AL_GIORNO:8d} | {m % MINUTI_AL_GIORNO:7d} | {orario}")
    print("  Riga -150: quoziente -1, resto 1290, cioè le 21:30 del giorno prima.")
    print("  Dentro l'ora: -150 // 60 = ", -150 // 60, " e -150 % 60 = ", -150 % 60,
          " -> 21:30.", sep="")
    return True


def esegui_esercizio_2():
    _sezione("Esercizio 2 - il turno che gira")
    print("  lettura | indice % 3 | operatore")
    for indice in (-3, -2, -1, 0, 1, 2, 3, 7):
        nome = operatore_di_turno(indice)
        if nome is None:
            _da_fare(2)
            return False
        print(f"  {indice:7d} | {indice % len(OPERATORI):10d} | {nome}")
    print("  La lettura -1 è di Carla: -1 % 3 vale 2, non -1.")
    return True


def esegui_esercizio_3():
    _sezione("Esercizio 3 - le righe che zip fa sparire")
    orari_troncati = ORARI_MINUTI[:6]            # il file degli orari si è interrotto
    print("  letture di temperatura :", len(TEMPERATURE_DECIMI))
    print("  orari effettivamente scritti:", len(orari_troncati))
    print("  righe prodotte da zip() nudo:", len(list(zip(orari_troncati, TEMPERATURE_DECIMI))))

    esito = abbina_prudente(orari_troncati, TEMPERATURE_DECIMI)
    if esito is None:
        _da_fare(3)
        return False
    coppie, perse = esito
    print(f"  abbina_prudente -> {len(coppie)} coppie, {perse} righe perse")

    # la media delle sole letture abbinate, in decimi di grado: numeri interi,
    # quindi il risultato è una frazione esatta e si può verificare a mano
    somma = sum(v for _, v in coppie)
    print(f"  somma dei decimi abbinati: {somma} su {len(coppie)} letture"
          f" -> media {somma}/{len(coppie)} = {somma / len(coppie):.4f} decimi di grado")
    somma_tutte = sum(TEMPERATURE_DECIMI)
    print(f"  se nessuna riga fosse andata persa: {somma_tutte}/{len(TEMPERATURE_DECIMI)}"
          f" = {somma_tutte / len(TEMPERATURE_DECIMI):.4f}")
    print("  Due medie diverse, nessun messaggio di errore: è questo il pericolo.")
    return True


def main():
    print("Corso MLE - Modulo 01, Lezione 1 - esercizi di codice")
    print("Dominio: una stazione meteo SIMULATA (dati inventati in questo file).")
    fatti = 0
    for esegui in (esegui_esercizio_1, esegui_esercizio_2, esegui_esercizio_3):
        if esegui():
            fatti += 1
    print()
    print(f"Esercizi completati: {fatti} su 3.")
    if fatti < 3:
        print("Completa i TODO e rilancia: python3 esercizi.py")


if __name__ == "__main__":
    main()


# ======================================================================
# SOLUZIONI DI RIFERIMENTO
# Decommenta una funzione alla volta, incollala al posto del TODO
# corrispondente e rilancia. Guardarle prima di aver provato non insegna
# niente: la difficoltà di questi esercizi è tutta nel ragionamento.
# ======================================================================
#
# --- Esercizio 1 -------------------------------------------------------
# def minuti_a_orario(m):
#     ore, minuti = divmod(m % MINUTI_AL_GIORNO, 60)
#     return f"{ore:02d}:{minuti:02d}"
#
# Atteso: -150 -> "21:30", 0 -> "00:00", 90 -> "01:30", 630 -> "10:30",
# 1439 -> "23:59", 1530 -> "01:30" (il giorno dopo).
# Il punto della riga -150: -150 % 1440 = 1290, e divmod(1290, 60) = (21, 30).
# Nessun `if` sul segno: ci pensa la definizione di `%`.
# Perché non `int(m / 1440)`: `int()` tronca VERSO LO ZERO, mentre `//`
# arrotonda verso il basso. Per m = -150 si ha int(-150/1440) = 0 ma
# -150 // 1440 = -1: con `int` perderesti il "giorno prima" e la formula
# a == (a // b) * b + (a % b) smetterebbe di valere.
#
# --- Esercizio 2 -------------------------------------------------------
# def operatore_di_turno(indice, operatori=OPERATORI):
#     return operatori[indice % len(operatori)]
#
# Atteso: -3 -> Anna, -2 -> Berto, -1 -> Carla, 0 -> Anna, 1 -> Berto,
# 2 -> Carla, 3 -> Anna, 7 -> Berto.
# La riga che sorprende è -1 -> Carla. In Python `-1 % 3` vale 2 perché il
# resto prende il segno del DIVISORE; in altri linguaggi (C, Java) lo stesso
# calcolo dà -1 e `operatori[-1]` funzionerebbe lo stesso per puro caso,
# mentre `operatori[-4]` solleverebbe IndexError. La versione con `%` è
# corretta per costruzione, non per fortuna.
# Una catena di `if` scritta in fretta (`if indice == 0 ... elif indice == 1`)
# coprirebbe solo i casi previsti e cadrebbe sul primo indice non elencato.
#
# --- Esercizio 3 -------------------------------------------------------
# def abbina_prudente(orari, valori):
#     coppie = list(zip(orari, valori))
#     perse = max(len(orari), len(valori)) - min(len(orari), len(valori))
#     return coppie, perse
#
# Atteso: 6 coppie e 2 righe perse. Le due medie: 819/6 = 136.5000 decimi
# contro 1082/8 = 135.2500 decimi. La differenza non è enorme, ed è
# proprio questo il problema: un numero plausibile e sbagliato non attira
# l'attenzione. `zip(orari, valori, strict=True)` solleverebbe ValueError.
# Quando preferire l'uno o l'altro: `strict=True` in una pipeline di
# produzione, dove un disallineamento è un difetto da fermare subito; il
# conteggio in un lavoro di pulizia, dove vuoi finire il giro e riportare
# quante righe hai dovuto scartare.
