PHP e intervalli di tempo: verificare sovrapposizioni e disponibilità

by theArchitect
SHARE
PHP e intervalli di tempo: verificare sovrapposizioni e disponibilità
© Guida-HTML5.it

Introduzione

Quando si lavora con date e orari in PHP, un’esigenza molto comune è capire se due intervalli temporali si sovrappongono. Questo problema compare in molti contesti reali: prenotazioni di sale, appuntamenti, turni di lavoro, noleggi di automobili o periodi di manutenzione.

Un intervallo può essere rappresentato da un momento iniziale e da un momento finale. Per esempio, una prenotazione potrebbe iniziare alle 14:00 e terminare alle 16:00. Se un’altra prenotazione comincia alle 15:30, esiste una sovrapposizione. Se invece inizia alle 16:00, le due prenotazioni possono essere considerate consecutive e non sovrapposte.

In questo tutorial useremo DateTimeImmutable e definiremo gli intervalli come semiaperti: l’inizio è incluso, mentre la fine è esclusa. In forma matematica, un intervallo sarà quindi rappresentato come [inizio, fine). Questa scelta evita ambiguità quando un evento termina esattamente nello stesso momento in cui ne inizia un altro.

Codice completo

<?php

declare(strict_types=1);

/**
 * Verifica se due intervalli temporali si sovrappongono.
 *
 * Gli intervalli sono considerati semiaperti:
 * [inizio, fine)
 */
function intervalliSovrapposti(
    DateTimeImmutable $inizioA,
    DateTimeImmutable $fineA,
    DateTimeImmutable $inizioB,
    DateTimeImmutable $fineB
): bool {
    // Un intervallo non valido non può essere elaborato correttamente.
    if ($inizioA >= $fineA || $inizioB >= $fineB) {
        throw new InvalidArgumentException(
            ´La data iniziale deve essere precedente alla data finale.´
        );
    }

    /*
     * Gli intervalli si sovrappongono quando:
     * - A inizia prima della fine di B
     * - B inizia prima della fine di A
     *
     * Il confronto con < esclude il caso in cui un intervallo
     * inizi esattamente quando termina l´altro.
     */
    return $inizioA < $fineB && $inizioB < $fineA;
}

/**
 * Converte una stringa nel formato ISO 8601 in DateTimeImmutable.
 */
function creaData(string $valore): DateTimeImmutable
{
    $data = DateTimeImmutable::createFromFormat(
        DateTimeInterface::ATOM,
        $valore
    );

    $errori = DateTimeImmutable::getLastErrors();

    if (
        $data === false ||
        ($errori !== false && ($errori[´warning_count´] > 0 || $errori[´error_count´] > 0))
    ) {
        throw new InvalidArgumentException(
            "Data non valida: {$valore}"
        );
    }

    return $data;
}

// Intervallo già presente nel calendario.
$inizioPrenotazione = creaData(´2026-09-24T10:00:00+02:00´);
$finePrenotazione   = creaData(´2026-09-24T12:00:00+02:00´);

// Intervallo richiesto da un nuovo utente.
$inizioRichiesta = creaData(´2026-09-24T11:30:00+02:00´);
$fineRichiesta   = creaData(´2026-09-24T13:00:00+02:00´);

try {
    $sovrapposta = intervalliSovrapposti(
        $inizioPrenotazione,
        $finePrenotazione,
        $inizioRichiesta,
        $fineRichiesta
    );

    if ($sovrapposta) {
        echo ´La fascia oraria richiesta non è disponibile.´;
    } else {
        echo ´La fascia oraria richiesta è disponibile.´;
    }
} catch (InvalidArgumentException $e) {
    echo ´Errore: ´ . $e->getMessage();
}

Spiegazione

Rappresentare correttamente un intervallo

Ogni intervallo richiede due oggetti: uno per l’inizio e uno per la fine. Nell’esempio utilizziamo stringhe ISO 8601, come 2026-09-24T10:00:00+02:00. Questo formato include data, ora e offset rispetto all’UTC, rendendo il valore facilmente interpretabile.

DateTimeImmutable è preferibile a DateTime quando vogliamo evitare modifiche accidentali. Le operazioni su un oggetto immutabile restituiscono un nuovo oggetto, lasciando invariato quello originale. Questo comportamento è particolarmente utile quando una stessa data viene utilizzata in più controlli.

La formula per individuare la sovrapposizione

La condizione utilizzata è:

$inizioA < $fineB && $inizioB < $fineA

La prima parte verifica che l’intervallo A inizi prima della fine dell’intervallo B. La seconda controlla che B inizi prima della fine di A. Se entrambe le condizioni sono vere, i due intervalli hanno almeno una porzione di tempo in comune.

Consideriamo questi casi:

  • 10:00–12:00 e 11:00–13:00: gli intervalli si sovrappongono.
  • 10:00–12:00 e 12:00–14:00: non si sovrappongono, perché il secondo inizia quando il primo termina.
  • 10:00–12:00 e 08:00–10:00: non si sovrappongono.
  • 10:00–12:00 e 09:00–15:00: il secondo intervallo contiene completamente il primo.

Controllare intervalli non validi

Un intervallo con fine precedente all’inizio è logicamente errato. Anche un intervallo con inizio uguale alla fine ha durata zero e, nella maggior parte dei sistemi di prenotazione, dovrebbe essere rifiutato.

Per questo motivo la funzione controlla la condizione $inizio >= $fine e lancia un’eccezione. Separare questo controllo dalla logica principale rende il codice più leggibile e impedisce risultati inattesi.

Best practice

  • Usa sempre oggetti data: evita di confrontare date come semplici stringhe, a meno che il formato sia rigorosamente ordinabile e sempre identico.
  • Preferisci DateTimeImmutable: riduce gli effetti collaterali e rende più sicure le funzioni che ricevono date come parametri.
  • Definisci la semantica degli estremi: stabilisci se la fine dell’intervallo è inclusa o esclusa. L’approccio [inizio, fine) è spesso il più pratico.
  • Valida prima di confrontare: controlla che entrambe le date esistano e che l’inizio preceda la fine.
  • Conserva un formato coerente: nei database è consigliabile memorizzare i valori in un formato standard, come UTC, applicando la presentazione locale solo nell’interfaccia.
  • Gestisci gli errori esplicitamente: non ignorare le eccezioni provenienti da date malformate o intervalli inconsistenti.
  • Scrivi test sui casi limite: verifica soprattutto intervalli adiacenti, intervalli identici, intervalli contenuti e periodi che attraversano la mezzanotte.

In un’applicazione reale, il controllo dovrà essere eseguito contro tutte le prenotazioni già presenti. È importante filtrare i record in modo efficiente, ad esempio usando query indicizzate su inizio e fine, e poi applicare in PHP le regole aggiuntive eventualmente necessarie.

Riepilogo

Per verificare la disponibilità di una fascia oraria non è necessario calcolare manualmente la durata degli eventi. È sufficiente rappresentare ogni intervallo con un inizio e una fine, validare i dati e applicare la condizione:

$inizioA < $fineB && $inizioB < $fineA

L’uso di DateTimeImmutable, del formato ISO 8601 e di intervalli semiaperti permette di costruire controlli chiari, prevedibili e riutilizzabili in sistemi di prenotazione, agende e pianificazione delle risorse.

Approfondisci con risorse ufficiali

SHARE