Tennis - Soluzione

In questo tutorial non verrà presentata la possibile soluzione nella sua interezza, ma soltano le parti interessanti, che dovranno essere opportunamente integrate per fornire la soluzione completa e compilabile. Per ottenere un programma completo e funzionante, si tratta pertanto di scrivere la funzione main che dichiara le variabili necessarie e che chiama opportunamente le funzioni descritte.

Per cominciare, si può dichiarare la seguente struttura:

struct partita {
    char g1[51], g2[51];   // nomi dei giocatori
    int maxset;            // numero massimo di set da giocare
    int n_set;             // numero di set giocati
    int score[5][2];       // game giocati nei vari set
};

La lettura del file

La funzione di lettura può essere come la seguente:

struct partita *leggi_file(FILE *f, int *count)
{
    struct partita *v, *partita;
    int conv, dim;
    char buf[1000];

    *count = 0;
    dim = 4;
    if (!(v = malloc(dim * sizeof(*v)))) {
        return NULL;
    }

    while (fgets(buf, sizeof(buf), f)) {
        /* evito di riscriverlo nella sscanf */
        partita = v + (*count);
        conv = sscanf(buf, "%50s %50s %d %d-%d %d-%d %d-%d %d-%d %d-%d",
                partita->g1, partita->g2, &partita->maxset,
                &partita->score[0][0], &partita->score[0][1],
                &partita->score[1][0], &partita->score[1][1],
                &partita->score[2][0], &partita->score[2][1],
                &partita->score[3][0], &partita->score[3][1],
                &partita->score[4][0], &partita->score[4][1]);

        /* Devono essere convertiti almeno 7 valori. */
        if (conv < 7) {
            free(v);
            return NULL;
        }
        partita->n_set = (conv - 3) / 2;

        /*
         * Nel caso in cui il vettore attualmente allocato
         * sia stato completamente riempito, lo rialloca
         * raddoppiando la dimensione.
         */
        if (*count + 1 >= dim) {
            dim *= 2;
            if (!(v = realloc(v, dim * sizeof(*v)))) {
                free(v);
                return NULL;
            }
        }
        (*count)++;
    }
    v = realloc(v, (*count) * sizeof(*v));
    return v;
}

La funzione è sufficientemente commentata da risultare di immediata comprensione.

Da notare due dettagli:

  • l'istruzinoe partita = v + (*count) serve per assegnare ad un puntatore l'indirizzo dell'elemento corrente da riempire, in modo da evitare di ripetere v + (*count) in tutti i punti necessari nella funzione;
  • la sscanf tenta sempre di convertire 5 set; solo successivamente, con l'istruzione partita->n_set = (conv - 3) / 2, si va a impostare il numero effettivo di set giocati in base al numero di elementi convertiti

ATTENZIONE: il numero di set effettivamente giocati può essere inferiore al numero massimo di set da giocare. Infatti, per esempio, una partita che prevede al massimo 5 set può terminare dopo 3 soli set, se questi vengono vinti tutti dallo stesso giocatore. Pertanto è importante calcolare il numero di set effettivamente letti da file.

1) Partita con più giochi

La seguente funzione serve per calcolare il numero di games giocati in una partita, eseguendo un ciclo sul numero di set della partita e sommando i valori del vettore score per ciascun set:

int giochi_in_partita(struct partita *partita)
{
    int i, games = 0;

    for (i = 0; i < partita->n_set; i++) {
        games += partita->score[i][0] + partita->score[i][1];
    }
    return games;
}

La funzione giochi_in_partita viene usata dalla seguente:

int indice_partita_max_giochi(struct partita *elenco, int n, int *max)
{
    int games;
    int i, id = 0;

    *max = giochi_in_partita(elenco);
    for (i = 1; i < n; i++) {
        games = giochi_in_partita(elenco + i);
        if (games > *max) {
            id = i;
            *max = games;
        }
    }
    return id;
}

Questa funzione esamina tutte le partite nel vettore elenco. Calcola per ciascuna il numero di games giocati usando la giochi_in_partita, e conserva l'indice della partita con il massimo numero di games giocati. Questo indice può essere usato nel main per accedere all'elemento del vettore elenco e stampare il nome dei giocatori.

Si noti che la condizione nell'if è games > *max, con il minore stretto. In questo modo, in caso ci siano più elementi del vettore aventi il numero massimo di games, si conserva l'indice del primo elemento che compare nel vettore. Se si fosse messo il maggiore-uguale (games >= *max) si sarebbe mantenuto l'indice dell'ultimo elemento del vettore avente il massimo numero di games, che però non è quanto richiesto nel quesito.

2) Giochi totali

Avendo realizzato la funzione giochi_in_partita per il punto precedente, la si può usare anche per calcolare il numero totale di games giocati in tutte le partite, come segue:

int tot_giochi(struct partita *elenco, int n)
{
    int i, s = 0;

    for (i = 0; i < n; i++) {
        s += giochi_in_partita(elenco + i);
    }
    return s;
}

Ovviamente non è indispensabile aver risposto al punto precedente per poter realizzare una funzione come tot_giochi. Per esempio, la variante che segue non fa uso della funzione giochi_in_partita:

int tot_giochi(struct partita *elenco, int n)
{
    struct partita *partita;
    int i, j, s = 0;

    for (i = 0; i < n; i++) {
        partita = elenco + i;
        for (j = 0; j < partita->n_set; j++)
            s += partita->score[j][0] + partita->score[j][1];
    }
    return s;
}

Sicuramente, però, il fatto di usare la funzione giochi_in_partita anche per risolvere questo punto rappresenta un ottimo esempio di riutilizzo di funzioni. Inoltre, come si nota, rende il codice della tot_giochi più compatto ed elegante.

3) Media set

La seguente funzione calcola la media delle partite giocate al meglio di n_set set, in quanto la media dei set giocati è richiesto che sia distinta tra partite al meglio dei 3 o dei 5 set.

double media(struct partita *elenco, int n, int n_set)
{
    struct partita *partita;
    int i, count = 0;
    double s = 0.0;

    for (i = 0; i < n; i++) {
        partita = elenco + i;
        if (partita->maxset == n_set) {
            s += partita->n_set;
            count++;
        }
    }
    if (count > 0)
        return s / count;
    else
        return -1.0;
}

Il ciclo for all'interno della funzione esamina tutte le partite, e somma il numero di set giocati soltanto se maxset è uguale a n_set, ovvero solo se stiamo considerando il tipo di partita desiderato in base al massimo numero di set da giocare.

Questo metodo è il più immediato e auto-contenuto (nel senso che tutto il codice è contenuto nella funzione), ma non è il solo possibile. In alternativa, si potrebbe implementare una funzione che si limita a calcolare il numero di set totali su tutti gli elementi del vettore passato come parametro, evitando quindi il parametro n_set. In questo caso, il codice che chiama la funzione ha la responsabilità di creare un vettore con le sole partite di cui fare la somma (eseguendo un cosiddetto "filtro"), prima di passare questo vettore alla funzione media.

4) Numero di tie break

La seguente funzione calcola il numero di tie-break giocati in tutte le partite:

int tie(struct partita *elenco, int n)
{
    struct partita *partita;
    int i, j, s = 0;

    for (i = 0; i < n; i++) {
        partita = elenco + i;
        for (j = 0; j < partita->n_set; j++)
            if (partita->score[j][0] + partita->score[j][1] == 13)
                s++;
    }
    return s;
}

Viene fatto un ciclo per tutte le partite, che contiene un ciclo per tutti i set di ciascuna partita. Se la somma dei games vinti dai due giocatori nel set è pari a 13 significa che il set è terminato al tie-break, cioè con punteggio pari a 7-6 oppure 6-7. In tal caso viene incrementato il valore di s per conteggiare il tie-break.

5) Numero utenti unici

Per determinare il numero di utenti unici serve una struttura che conservi l'elenco degli utenti che man mano vengono incontrati mentre si esaminano tutte le partite caricate da file. Serve quindi una struttura dati che contenga i dati degli utenti.

Per identificare l'utente basta il nome, ma come vedremo è utile aggiungere anche il punteggio di classifica per usare questa stessa struttura anche per la soluzione del punto (6):

struct utente {
    char nome[51];
    int punteggio;
};

La seguente funzione crea un vettore di struct utente contenente l'elenco dei nomi dei giocatori:

struct utente *elenco_utenti_unici(struct partita *elenco, int n, int *count)
{
    int i, presente;
    struct utente *utenti; // vettore di stringhe da allocare successivamente

    /* per semplicita` viene allocato spazio come se ogni
     * partita fosse giocata da tutti giocatori diversi */
    if (!(utenti = malloc(2 * n * sizeof(*utenti))))
        return NULL;

    *count = 0;
    /* un ciclo per ogni partita giocata */
    for (i = 0; i < n; i++) {
        presente = indice_utente(utenti, *count, (elenco + i)->g1);
        if (presente < 0) strcpy(utenti[(*count)++].nome, (elenco + i)->g1);
        presente = indice_utente(utenti, *count, (elenco + i)->g2);
        if (presente < 0) strcpy(utenti[(*count)++].nome, (elenco + i)->g2);
    }
    utenti = realloc(utenti, (*count) * sizeof(*utenti));
    return utenti;
}

Per fare questo, prima alloca il vettore utenti per contenere un numero di utenti pari al doppio del numero di partite giocate, in caso tutte le partite siano giocate da giocatori tutti diversi tra loro. Il numero di utenti attualmente contenuti nel vettore è conservato in *count.

Successivamente vengono esaminate tutte le partite con il ciclo for. Per ciascuna partita, viene chiamata la funzione indice_utente, riportata sotto, per determinare se un utente è già presente nel vettore. Se è già presente, la funzione indice_utente restituisce l'indice dell'elemento, altrimenti restituisce -1. Pertanto verrà inserito un nuovo elemento nel vettore utenti solo se l'indice restituito dalla indice_utente è negativo. Ovviamente il controllo della presenza dei giocatori nel vettore utenti deve essere fatta per entrambi i giocatori, per cui nel ciclo for si ripetono le istruzioni per i due giocatori.

Alla fine della funzione, la realloc ridimensiona il vettore in modo da avere esattamente la dimensione necessaria a contenere *count strutture.

La funzione indice_utente non fa altro che svolgere una ricerca sequenziale nel vettore passatole come paramentro, che contiene i nomi trovati. In altri termini, esamina sequenzialmente tutte le strutture per cercarne una che contenga il nome specificato. Viene usata la strcmp per confrontare il nome cercato con quello all'interno del vettore elenco. Se trova tale struttura, ne restituisce l'indice. Se viene terminato il ciclo senza aver trovato il nome cercato, viene restituito -1, che corrisponde ad un indice non valido per un vettore, in modo da indicare la mancata individuazione del giocatore.

int indice_utente(struct utente *elenco, int n, char *nome)
{
    int i;
    for (i = 0; i < n; i++) {
        /* verifica la presenza del giocatore nel vettore */
        if (!strcmp(elenco[i].nome, nome)) return i;
    }
    return -1;
}

6) Classifica

Per calcolare la classifica è fondamentale avere un elenco di giocatori unici che hanno giocato le partite.

Dal momento che la classifica è basata sul numero di set vinti, si può partire da una funzione come la seguente la quale, data una partita (un puntatore ad essa, per la precisione), restituisce il numero di set vinti dai due giocatori.

I due valori vengono restituiti per mezzo del passaggio per indirizzo delle variabili puntate da v1 ev2`:

void n_set_vinti(struct partita *partita, int *v1, int *v2)
{
    int i;

    *v1 = *v2 = 0;
    for (i = 0; i < partita->n_set; i++) {
        if (partita->score[i][0] > partita->score[i][1]) (*v1)++;
        else (*v2)++;
    }
}

La funzione calcola_punteggi calcola la classifica considerando tutte le partite memorizzate nel vettore elenco (di n elementi) e il vettore utenti (di n_utenti elementi) contente la lista dei giocatori univoci trovata nel punto (5).

Per ciascuna partita si determinano i set vinti con n_set_vinti. Con il ciclo for più interno si vanno a individuare i due elementi nel vettore utenti che corridpondono ai due giocatori della partita considerata, confrontando con strcmp il nome del giocatore corrente con quello nel vettore. Quando un giocatore viene trovato, ne si incrementa il punteggio di classifica.

void calcola_punteggi(struct partita *elenco, int n, struct utente *utenti, int n_utenti)
{
    int i, j;
    int p1, p2;

    /* azzera il punteggio di tutti i giocatori */
    for (j = 0; j < n_utenti; j++)
        utenti[j].punteggio = 0;

    for (i = 0; i < n; i++) {
        /* assegna il numero di set vinti da ciascun giocatore */
        n_set_vinti(elenco + i, &p1, &p2);
        for (j = 0; j < n_utenti; j++) {
            if (!strcmp(utenti[j].nome, elenco[i].g1)) utenti[j].punteggio += p1;
            if (!strcmp(utenti[j].nome, elenco[i].g2)) utenti[j].punteggio += p2;
        }
    }
}

Le due funzioni precedenti servono per creare un vettore di giocatori con i rispettivi punteggi di classifica. Per effettuare l'ordinamento si può usare la qsort nel modo seguente:

qsort(utenti, n_utenti, sizeof(*utenti), cmp_utenti);

dove utenti è il vettore di n_utenti elementi, con i rispettivi punteggi di classifica.

La parte fondamentale di questa istruzione è la funzione di confronto cmp_utenti, che può essere formulata come segue:

int cmp_utenti(const void *p1, const void *p2)
{
    const struct utente *u1 = p1;
    const struct utente *u2 = p2;
    if (u1->punteggio < u2->punteggio)
        return 1;
    else if (u1->punteggio > u2->punteggio)
        return -1;
    else {   // stessi punti: ordino per nome
        return strcmp(u1->nome, u2->nome);
    }
}

Viene restituito 1 se il punteggio del primo giocatore è minore del secondo (perché si vuole l'ordinamento in senso decrescente di punteggio), oppure -1 se il primo è maggiore del secondo. In caso i punteggi siano uguali, si restituisce il valore a sua volta restituito dalla strcmp, in quanto si desidera l'ordinamento in senso crescente per nome a parità di punteggio.