Vai al contenuto
Allin

L'aritmetica delle date è più difficile di quanto sembri

Pubblicato il 06/06/2025 · 15 min di lettura · Calcolatrici quotidiane

Lena Hoffmann

Lena HoffmannRedattrice Scienza e Istruzione presso Allin

Matematica · Fisica

Verificato su 5 fonti

Vedi il profilo
In breve

Chiedi quanto fa il 31 gennaio più un mese e non c'è una risposta che la matematica ti imponga. La maggior parte dei sistemi tronca alla fine del mese più corto e restituisce il 28 febbraio 2026 — o il 29 febbraio in un anno bisestile come il 2024. L'aritmetica ingenua in giorni restituisce tutt'altro: più 31 giorni dà il 3 marzo, più 30 giorni il 2 marzo. Tutte e tre sono difendibili, ed è esattamente questo il problema. Quel troncamento ha una conseguenza che quasi nessuno nota: l'addizione di mesi non è né invertibile né associativa. Il 31 marzo meno un mese è il 28 febbraio, e riaggiungere un mese dà il 28 marzo, non il 31 marzo. Il 31 gennaio più un mese più un mese è il 28 marzo, ma il 31 gennaio più due mesi è il 31 marzo. L'aritmetica oraria si rompe in un'altra direzione. In Europe/Paris il 29 marzo 2026 dura 23 ore e il 25 ottobre ne dura 25, quindi aggiungere 86 400 secondi a un appuntamento delle 9:00 del 28 marzo atterra alle 10:00 del giorno dopo. E l'età è un confronto di calendario, non una divisione: su tutti i compleanni dal 1930 al 2020, giorni ÷ 365 sbaglia età nel 3,49 % dei casi. La regola che risolve tutto: fai l'aritmetica di calendario nei campi di calendario, quella degli istanti in UTC, e non mescolarle mai.

«Un mese dopo» non ha una risposta unica, e ogni libreria di date ha dovuto sceglierne una. L'addizione di mesi non è né associativa né invertibile, un giorno non dura sempre 24 ore, e l'età non è i giorni diviso 365,25.

Non esiste un'aritmetica che imponga una risposta

Aggiungere uno a un numero è privo di ambiguità. Aggiungere un mese a una data no, perché i mesi non sono un'unità — sono etichette di lunghezza disuguale, da 28 a 31 giorni, e la lunghezza di quello in cui atterri dipende da quello da cui sei partito. Il 31 gennaio più un mese deve atterrare da qualche parte in febbraio, e febbraio non ha un 31. Qualcosa deve cedere. La scelta quasi universale è troncare: tenere il mese, tenere l'anno e riportare il giorno all'ultimo valido. Questo dà il 28 febbraio 2026 e il 29 febbraio 2024. java.time in Java, l'API Temporal di ECMAScript, l'addizione di intervalli di PostgreSQL e dateutil in Python si comportano tutti così, e lo fanno perché l'alternativa è peggiore.

L'alternativa è trattare un mese come un numero fisso di giorni. Scegli 30 e il 31 gennaio più un mese diventa il 2 marzo 2026; scegli 31 e diventa il 3 marzo. Entrambe saltano febbraio per intero, che è esattamente ciò che l'utente che chiede «un mese dopo» non vuole. La tabella qui sopra applica tutte e tre le definizioni a cinque date di partenza, e coincidono solo quando il giorno del mese è abbastanza piccolo da esistere ovunque. Ecco la lezione pratica: dal 1º al 28 tutti i metodi sono identici, e il 29, il 30 e il 31 tutti divergono da qualche parte. Circa una data su dieci è nella zona di pericolo, ed è per questo che il bug sopravvive così facilmente ai test.

Il troncamento ti costa associatività e invertibilità

Esegui questo e guarda sparire una proprietà che davi per scontata. Il 31 marzo 2026 meno un mese tronca al 28 febbraio. Riaggiungi un mese e ottieni il 28 marzo — tre giorni prima di dove sei partito. L'addizione di mesi non è invertibile: sottrarre e poi aggiungere non è l'identità. Lo stesso difetto si manifesta come perdita dell'associatività. Il 31 gennaio 2026 più un mese più un mese è il 28 marzo, perché il valore intermedio è stato troncato al 28 febbraio e il troncamento è definitivo. Il 31 gennaio più due mesi, calcolato in un colpo solo, è il 31 marzo. Due espressioni che dovrebbero valere lo stesso differiscono di tre giorni.

Non è un bug di nessuna libreria in particolare — è una conseguenza del calendario, e ogni libreria che tronca la eredita. La regola pratica che ne segue merita un post-it: non costruire mai una serie mensile aggiungendo un mese al risultato precedente. Aggiungi sempre n mesi alla data di ancoraggio originale. Un abbonamento che parte il 31 gennaio e si rinnova per iterazione slitterà al 28 e ci resterà per sempre; lo stesso abbonamento ancorato al 31 gennaio e calcolato come inizio + n mesi cade il 28 febbraio, il 31 marzo, il 30 aprile, il 31 maggio — che è ciò che il cliente si aspetta e ciò che il processore di pagamento addebiterà. Il bug è invisibile undici mesi l'anno e poi arriva tutto insieme.

Un giorno non è 24 ore — Europe/Paris, marzo e ottobre 2026

Prendi le regole reali del database tz invece di un'ipotesi. Nel 2026 Europe/Paris passa da UTC+1 a UTC+2 il 29 marzo e torna indietro il 25 ottobre. Misura la distanza fra la mezzanotte locale del 29 marzo e la mezzanotte locale del 30 marzo: va da 2026-03-28T23:00Z a 2026-03-29T22:00Z, cioè 23 ore. Fai lo stesso attorno al 25 ottobre e ottieni da 2026-10-24T22:00Z a 2026-10-25T23:00Z, cioè 25 ore. Il giorno di calendario e il giorno da 86 400 secondi sono oggetti diversi, e due volte l'anno si separano in modo visibile. America/New_York fa lo stesso in altre date — 23 ore l'8 marzo 2026 e 25 ore il 1º novembre.

La conseguenza ricade su appuntamenti veri. Uno slot delle 9:00 del 28 marzo 2026 a Parigi è l'istante 2026-03-28T08:00Z. Aggiungi esattamente 24 ore trascorse e ottieni 2026-03-29T08:00Z, che a Parigi si legge 10:00 — la riunione è slittata di un'ora in avanti. Fallo a ottobre e slitta di un'ora indietro: le 9:00 del 24 ottobre più 24 ore sono le 8:00 del 25 ottobre. Nessuna delle due è ciò che significa «domani alla stessa ora». «Domani alla stessa ora» è un'operazione di calendario: incrementare il campo data, tenere il campo dell'orologio da parete, poi risolvere di nuovo l'istante contro il fuso. Aggiungere una durata è un'operazione di fisica. Coincidono 363 giorni l'anno, che è esattamente quanto basta perché il guasto sembri casuale.

Il trabocco silenzioso: il 30 febbraio non solleva alcun errore

In JavaScript, new Date(2026, 1, 30) non solleva eccezioni. Restituisce il 2 marzo 2026. Il costruttore accetta qualunque intero e normalizza riportando l'eccedenza nel mese successivo, quindi un campo giorno pari a 30 in un febbraio di 28 giorni diventa silenziosamente il 2 del mese dopo. La stessa normalizzazione trasforma l'indice di mese 12 nel gennaio dell'anno seguente e un giorno 0 nell'ultimo giorno del mese precedente — che è il trucco dietro l'idioma consueto per «giorni di questo mese», new Date(y, m, 0).getDate(). È un comportamento utile quando lo vuoi e una corruzione silenziosa dei dati quando non lo vuoi, e nulla nel valore restituito ti dice in quale caso ti trovi.

È per questo che validare una data è più che controllare che il parser non si sia lamentato. Un modulo che accetta 30/02/2026 e memorizza 2026-03-02 ha perso l'errore dell'utente invece di segnalarlo, e il record ora dice qualcosa che l'utente non ha mai digitato. Il controllo difensivo sta in una riga: costruisci la data, poi verifica che anno, mese e giorno che ricevi siano i tre che hai messo. Se non lo sono, l'input non era una data reale. L'altra metà della difesa è sapere quanto è lungo ogni mese prima ancora di costruire l'input — 31, 28 o 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 — ed è a questo che risponde il calcolatore dei giorni del mese, per qualunque anno indichi.

L'età è un confronto, non una divisione

La scorciatoia allettante è contare i giorni trascorsi e dividere per 365,25, con la motivazione che l'anno medio dura 365,25 giorni. Mettila alla prova. Una persona nata il 23 agosto 2000, interrogata il 22 agosto 2026, ha vissuto 9 495 giorni. Dividi per 365 e ottieni 26,0137, la cui parte intera è 26 — ma la persona ha 25 anni e il compleanno è domani. Dividi per 365,25 e ottieni 25,9959, la cui parte intera è 25, e lì è giusto. Allora percorri tutto lo spazio invece di un esempio: ogni data di nascita dal 1º gennaio 1930 al 1º gennaio 2020, valutata il 22 agosto 2026, fa 32 873 date. Giorni ÷ 365 dà l'età sbagliata su 1 148 di esse, cioè il 3,49 %. Giorni ÷ 365,25 va molto meglio ma sbaglia ancora su 44, cioè lo 0,13 %.

I fallimenti residui sono i più crudeli: cadono proprio il giorno del compleanno. Chi è nato il 22 agosto 1932 ha vissuto 34 333 giorni al 22 agosto 2026, e 34 333 ÷ 365,25 = 93,9986, quindi la divisione dice 93 la mattina in cui compie 94 anni. Nessuna raffinatura del divisore risolve questo, perché nessun divisore unico può farlo, ed è tutto qui il punto — il calendario non è una scala uniforme. L'algoritmo corretto non contiene alcuna divisione. Sottrai l'anno di nascita dall'anno corrente, poi sottrai ancora uno se il mese e il giorno correnti non hanno ancora raggiunto il mese e il giorno di nascita. Tre confronti fra interi, esatti ovunque, e non ha mai bisogno di sapere quanto dura un anno.

La regola: campi di calendario per il calendario, UTC per gli istanti

Quasi ogni bug sulle date è uno di due errori. O a una domanda di calendario si è risposto con tempo trascorso — «un mese» diventato 30 giorni, «domani» 86 400 secondi, «l'età» una divisione — oppure a una domanda di istante si è risposto in campi locali, così un timestamp memorizzato si è spostato quando è cambiato lo scostamento. Il rimedio è decidere, prima di scrivere una riga, che tipo di grandezza stai maneggiando. Date di rinnovo, compleanni, scadenze, periodi di fatturazione e orari di apertura sono grandezze di calendario: tienile come anno, mese, giorno e ora da parete con un fuso nominato, e fai l'aritmetica su quei campi. Timeout, ordinamento dei log, scadenza della cache, limiti di frequenza e durate sono grandezze di istante: tienile come timestamp UTC e somma secondi.

Dove i due devono incontrarsi — un promemoria alle 9:00 ora locale, inviato da un server che capisce solo istanti — converti al confine, e solo al confine. Fai prima il passo di calendario nel fuso nominato dell'utente, risolvi il risultato in un istante UTC una volta sola e consegna quell'istante allo scheduler. Memorizzare lo scostamento invece del nome del fuso si rompe nel momento in cui cambiano le regole politiche, cosa che accade più volte l'anno; il database tz pubblica release proprio perché i governi continuano a spostare le loro transizioni. E non memorizzare mai un appuntamento locale futuro come solo istante UTC, perché se le regole di quel fuso vengono modificate prima della data, l'istante salvato non corrisponderà più alle 9:00 del mattino di nessuno.

La stessa data di partenza, tre definizioni difendibili di «un mese dopo» — e le risposte divergono
Data di partenza+ 1 mese, troncato+ 30 giorni+ 31 giorni
31 gennaio 202628 febbraio 20262 marzo 20263 marzo 2026
31 gennaio 2024 (anno bisestile)29 febbraio 20241º marzo 20242 marzo 2024
31 marzo 202630 aprile 202630 aprile 20261º maggio 2026
31 agosto 202630 settembre 202630 settembre 20261º ottobre 2026
30 novembre 202630 dicembre 202630 dicembre 202631 dicembre 2026
Giorni di un meseIl numero di giorni di qualsiasi mese di qualsiasi anno da 1 a 9999, la regola bisestile svolta riga per riga per febbraio, il giorno della settimana del 1º e dell'ultimo, quanti ce ne sono per ogni giorno della settimana e il calendario del mese.Prova lo strumento

Domande frequenti

Quanto fa il 31 gennaio più un mese?
Dipende dalla definizione che usa il tuo sistema, e non c'è una risposta imposta dalla matematica. Il troncamento — il comportamento di java.time, dell'API Temporal, degli intervalli PostgreSQL e di dateutil in Python — tiene mese e anno e riporta il giorno all'ultimo valido, dando il 28 febbraio 2026 o il 29 febbraio 2024. L'aritmetica a giorni fissi dà altro: più 30 giorni è il 2 marzo 2026 e più 31 giorni il 3 marzo. Il troncamento è quasi sempre la scelta giusta per date rivolte alle persone, perché chi chiede «un mese dopo» intende la data corrispondente del mese successivo, non un numero fisso di giorni. Se fatturi, contratti o pianifichi, dichiara nelle condizioni quale regola usi, perché i clienti notano eccome se un abbonamento del 31 gennaio si rinnova il 28 febbraio.
Perché sottrarre un mese e riaggiungerlo non restituisce la data di partenza?
Perché il troncamento distrugge informazione e nulla può ripristinarla. Il 31 marzo 2026 meno un mese deve atterrare in febbraio, febbraio non ha un 31, quindi il risultato è troncato al 28 febbraio. Quel risultato non ricorda più di essere venuto da un 31. Aggiungere un mese al 28 febbraio dà perciò il 28 marzo, e sei tre giorni prima di dove sei partito. Lo stesso meccanismo ti costa l'associatività: il 31 gennaio più un mese più un mese è il 28 marzo, mentre il 31 gennaio più due mesi in un solo passo è il 31 marzo. La conseguenza pratica è una regola da applicare ovunque: costruisci i calendari ricorrenti aggiungendo n mesi alla data di ancoraggio originale, mai iterando un mese alla volta dal risultato precedente. L'iterazione lascia che un singolo troncamento si propaghi per sempre.
Un giorno dura sempre 24 ore?
No, non come giorno di calendario locale. In Europe/Paris nel 2026 il 29 marzo dura 23 ore e il 25 ottobre ne dura 25, perché il fuso passa da UTC+1 a UTC+2 e torna indietro. Misurate come istanti, la mezzanotte locale del 29 marzo è 2026-03-28T23:00Z e quella del 30 marzo è 2026-03-29T22:00Z — 23 ore di distanza. America/New_York fa lo stesso l'8 marzo e il 1º novembre 2026. Alcune transizioni non sono nemmeno di ore intere; l'isola di Lord Howe si sposta di 30 minuti. I fusi senza ora legale hanno giorni di 24 ore tutto l'anno, ma non puoi presumere che i tuoi utenti siano lì. L'abitudine sicura è trattare «un giorno» in senso di calendario come un incremento del campo data, risolto contro un fuso nominato, e riservare gli 86 400 secondi al vero lavoro sul tempo trascorso, fatto in UTC.
Come devo calcolare l'età di qualcuno?
Con confronti, mai con una divisione. Sottrai l'anno di nascita dall'anno corrente, poi sottrai ancora uno se il mese corrente precede il mese di nascita, oppure se i mesi coincidono e il giorno corrente precede il giorno di nascita. È esatto per qualunque data. Le divisioni falliscono in modo misurabile: su tutti i compleanni dal 1930 al 2020 valutati il 22 agosto 2026 — 32 873 date — giorni trascorsi ÷ 365 sbaglia età nel 3,49 % dei casi e ÷ 365,25 nello 0,13 %. Peggio, i fallimenti residui si concentrano proprio sul compleanno, l'unico giorno in cui la gente controlla. Chi è nato il 22 agosto 1932 ha vissuto 34 333 giorni al 22 agosto 2026, e 34 333 ÷ 365,25 = 93,9986, quindi la divisione riporta 93 la mattina in cui compie 94 anni. I nati il 29 febbraio richiedono una decisione di policy a parte, perché le giurisdizioni divergono su se il compleanno legale in un anno comune sia il 28 febbraio o il 1º marzo.
Perché il mio modulo accetta il 30 febbraio senza protestare?
Perché la maggior parte dei costruttori di date normalizza invece di validare. In JavaScript, new Date(2026, 1, 30) restituisce il 2 marzo 2026 senza errore: il campo giorno trabocca e l'eccedenza viene riportata nel mese successivo. La stessa regola trasforma un indice di mese pari a 12 nel gennaio dell'anno seguente e un giorno 0 nell'ultimo giorno del mese precedente, ed è per questo che new Date(y, m, 0).getDate() è l'idioma consueto per la durata di un mese. Nulla nel valore restituito distingue un trabocco voluto da un refuso. La difesa è un controllo di andata e ritorno: costruisci la data, poi verifica che anno, mese e giorno riletti siano i tre che hai scritto. Se differiscono, rifiuta l'input. Farlo al confine costa molto meno che scoprire più tardi che una colonna del database contiene una data che nessuno ha mai inserito.
Devo memorizzare le date in UTC o in ora locale?
Dipende da cosa significa il valore, e rispondere «sempre UTC» causa tanti bug quanti ne previene. Tutto ciò che registra quando qualcosa è accaduto — una riga di log, un pagamento, una scadenza di cache, una finestra di rate limit — è un istante, e gli istanti vanno in UTC. Tutto ciò che registra quando qualcosa deve accadere nella giornata di qualcuno — un promemoria alle 9:00, una finestra di consegna, gli orari di un negozio, una riunione ricorrente — è un valore di calendario, e memorizzarlo come nudo istante UTC è un bug in attesa di un cambio di regole. I governi modificano le regole dell'ora legale più volte l'anno, e il database tz pubblica release per seguirle; un appuntamento futuro congelato come istante si allontanerà dall'ora da parete voluta se il suo fuso viene modificato. Memorizza quei valori come data locale, ora locale e nome del fuso IANA, e risolvi in istante solo nel momento in cui devi agire.

Articoli che potrebbero interessarti

Tutte le guide
SpiegazioneAnni bisestili: la regola, l'eccezione e l'eccezione all'eccezioneDivisibile per quattro, tranne i secoli, tranne i secoli divisibili per quattrocento. Questa regola in tre righe esiste perché l'anno tropico non dura 365,25 giorni, e la sua aritmetica spiega dieci giorni cancellati nel 1582 e un calendario che si ripete esattamente ogni 400 anni.SpiegazioneCalcolare un anniversarioContare all'indietro e contare in avanti sono due conti diversi, e il giorno stesso divergono di un anno. Che cosa fa questa calcolatrice con il 29 febbraio, che cosa fanno le altre librerie di date, e da dove vengono davvero le liste dei regali tradizionali e moderni.TutorialQuante settimane mancano a una data? (con esempi)Conta le settimane tra oggi e una data futura. Scopri il metodo del dividere per sette, quando arrotondare e la differenza tra conteggio inclusivo ed esclusivo.TutorialCome aggiungere o sottrarre giorni a una dataConta in avanti per aggiungere, indietro per sottrarre, passando le fini dei mesi. Ecco come, perché la lunghezza dei mesi e gli anni bisestili ingannano, e quando i weekend contano.TutorialQuanti giorni mancano a una data? (con esempi)Conta i giorni fino a qualsiasi data futura, capisci il conteggio inclusivo o esclusivo e crea un conto alla rovescia affidabile.SpiegazioneLeggere e scrivere l'ora militare americana0800 non è «le otto» né «08:00»: è «zero eight hundred», con una lettera di fuso in fondo. Le quattro cifre, la pronuncia, la mezzanotte a 0000, la disputa sul 2400, e perché la notazione a 12 ore non riesce a risolvere il mezzogiorno.

Strumenti correlati

Fonti

Hai notato un errore in questo articolo?