CodeGym /Corsi /C++ SELF /Memorizzazione di date e orari — strategie semplici di fo...

Memorizzazione di date e orari — strategie semplici di formato

C++ SELF
Livello 66, Lezione 4
Disponibile

1. Perché «memorizzare il tempo» è un piccolo contratto

Quando aggiungete il tempo per la prima volta a un programma, c'è una forte tentazione di fare qualcosa del tipo long long t = ...; e vivere sereni. Fino al momento in cui aprite i dati salvati e capite all'improvviso: sono secondi? millisecondi? tempo in UTC? locale? relativo all'avvio del programma? relativo all'"epoca"?

In programmazione il tempo non è solo un «numero attuale», è anche un accordo sul significato di quel numero. La buona notizia: per iniziare correttamente non dovete immergervi nei fusi orari e nell'aritmetica del calendario. Basta scegliere una o due strategie semplici e rispettarle, come se fossero il mini-standard del progetto.

In questa lezione faremo due cose pratiche: sceglieremo formati semplici di memorizzazione (numero o stringa) e imposteremo una conversione coerente time_point ↔ numero ↔ stringa.

Due livelli: «come memorizzo» e «come mostro»

È importante non mescolare questi livelli. Quando il programma memorizza il tempo, di solito serve che sia comodo confrontare, ordinare, serializzare e non perdere precisione. Quando il programma mostra il tempo a una persona, serve che sia leggibile e familiare.

Quindi una buona abitudine è: nella logica interna viviamo in std::chrono (o in un formato numerico ben definito), e al confine (output su schermo, scrittura su file, JSON) convertiamo in una forma «umana».

Se provate a memorizzare subito una «bella stringa», vi accorgerete presto che l'ordinamento diventa strano, i confronti lenti, e il parsing doloroso. Se provate a mostrare all'utente «millisecondi dall'epoca», l'utente vi guarderà come un calcolatore che ha deciso di fare il filosofo.

2. Memorizzare un istante come numero dall'epoca

La via più pratica e popolare per progetti didattici e molte applicazioni: memorizzare un istante come un intero che indica «quante unità sono passate dall'epoca». L'epoca per system_clock corrisponde di solito all'Unix epoch (1970-01-01 00:00:00 UTC), ma qui ci interessa altro: scegliete l'unità e non la cambiate.

Di solito si sceglie una delle due:

Unità di memorizzazione Tipo tipico Vantaggi Svantaggi
secondi std::time_t o long long semplice, compatibile con le API C tradizionali precisione solo al secondo
millisecondi std::int64_t più preciso, comodo per log/eventi occorre convertire da/per time_point

In questa lezione raccomando i millisecondi come «start universale»: sono sufficientemente precisi per la maggior parte delle applicazioni, e al contempo rimangono un numero intero.

Conversione time_point ↔ millisecondi dall'epoca

Poi — due piccole funzioni di ponte: dentro il programma lavoriamo con std::chrono::system_clock::time_point, mentre per la memorizzazione sul modello/file usiamo std::int64_t ms_since_epoch.

time_point → ms_since_epoch

L'idea è semplice: un time_point può essere trasformato in una duration tramite time_since_epoch(), e la duration si converte in millisecondi con duration_cast. Poi rimane solo chiamare count().

#include <chrono>
#include <cstdint>

std::int64_t to_unix_ms(std::chrono::system_clock::time_point tp) {
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
        tp.time_since_epoch()
    );
    return ms.count();
}

Qui count() restituisce il tipo numerico "grezzo", ma lo usiamo solo al confine — perché ormai è il formato di memorizzazione.

ms_since_epoch → time_point

L'operazione inversa serve quando avete letto un numero da file/JSON e volete tornare a lavorare come una persona, non come un calcolatore.

#include <chrono>
#include <cstdint>

std::chrono::system_clock::time_point from_unix_ms(std::int64_t ms_since_epoch) {
    return std::chrono::system_clock::time_point{ std::chrono::milliseconds{ms_since_epoch} };
}

In sostanza è proprio "istante = epoca + durata".

Regola minima per i nomi del tempo numerico

Quando memorizzate un "numero nudo", non avete la protezione di tipo che fornisce std::chrono::milliseconds. Perciò compensate con il nome.

I nomi delle variabili non sono decorazione. Per il tempo sono una vera "assicurazione contro i bug". Se chiamate una variabile createdAt, tra un mese dimenticherete se è (secondi? millisecondi?).

Confrontate i due approcci:

std::int64_t createdAt = 1737040000000;     // non è chiaro cosa sia
std::int64_t created_ms_since_epoch = 1737040000000; // già meglio

Il secondo è più verboso, ma riduce le probabilità di ingannare voi stessi. E il programmatore che si inganna è il tipo più pericoloso (soprattutto se ha accesso alla produzione).

3. Alternative: std::time_t e memorizzazione come stringa

A volte i millisecondi non sono necessari. Per esempio, salvate la data di creazione di una nota e la precisione al secondo è sufficiente. Allora usare std::time_t è del tutto ragionevole. È comodo perché molte funzioni di formattazione lo aspettano.

Non è una religione né un motivo di disputa su "cosa è più giusto". È una scelta in base al caso d'uso: se la precisione al secondo vi basta, time_t è un formato semplice e chiaro.

Ecco come ottenere un time_t da system_clock::now():

#include <chrono>
#include <ctime>

std::time_t now_as_time_t() {
    auto now = std::chrono::system_clock::now();
    return std::chrono::system_clock::to_time_t(now);
}

E come riconvertire un time_t in time_point:

#include <chrono>
#include <ctime>

std::chrono::system_clock::time_point from_time_t(std::time_t t) {
    return std::chrono::system_clock::from_time_t(t);
}

Se memorizzate time_t, fissate nella testa (e nella documentazione del progetto): sono secondi.

Quando ha senso memorizzare il tempo come stringa

La stringa sembra comoda: apri il file e vedi subito la data. Ma la stringa ha un costo: va generata, parsata, validata, e facilmente si trasforma in uno zoo di formati ("16/01/26", "01-16-2026", "2026.01.16", "venerdì, ma non preciso").

La stringa è buona quando fate config, log per un umano, o un formato che una persona modificherà a mano. Per lo storage macchina (soprattutto dentro strutture dati) di solito è meglio un numero.

Comunque, il minimo che serve è saper stampare bene un time_point.

4. Stampare il tempo calendario con put_time

Ora costruiremo la "classica catena" di formattazione. Si basa su <ctime> e <iomanip>. Sì, sembra vecchio stile, ma è chiaro, stabile e spesso presente nel codice reale.

Qui ci sono diverse "rappresentazioni" del tempo: time_point è comodo per l'aritmetica, time_t per compatibilità, tm per scomporre in anno/mese/giorno, e put_time per stampare secondo un pattern.

Stampare direttamente su std::cout

#include <chrono>
#include <ctime>
#include <iomanip>
#include <iostream>

int main() {
    auto now = std::chrono::system_clock::now();
    std::time_t t = std::chrono::system_clock::to_time_t(now);

    std::tm tm = *std::localtime(&t);
    std::cout << std::put_time(&tm, "%Y-%m-%d %H:%M:%S") << '\n'; // per esempio: 2026-01-16 14:05:31
}

Nota importante: std::localtime restituisce un puntatore a un oggetto statico interno, quindi copiamo subito in std::tm tm = *...;. È più sicuro e prevedibile.

Formattare in una std::string

A volte non volete stampare subito, ma costruire una stringa da inserire in JSON, in un messaggio di errore o in un log.

#include <chrono>
#include <ctime>
#include <iomanip>
#include <sstream>
#include <string>

std::string format_local(std::chrono::system_clock::time_point tp) {
    std::time_t t = std::chrono::system_clock::to_time_t(tp);
    std::tm tm = *std::localtime(&t);

    std::ostringstream out;
    out << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
    return out.str();
}

Sì, qui compare ostringstream, ma probabilmente lo avete già incontrato (avete usato stringstream come idea di parsing/formatting a flusso).

5. system_clock e steady_clock: cosa si può salvare

Questo è un errore molto comune. steady_clock è ideale per misure di intervallo ("quanto è durato"), ma non è obbligatoriamente legato al tempo di calendario e all'epoca del 1970. I suoi valori possono essere consistenti solo durante l'esecuzione del programma.

Quindi la regola è semplice: se volete salvare un istante "su disco" o passarlo a un altro processo, usate system_clock o un formato numerico derivato da system_clock. Lasciate steady_clock per timer, deadline all'interno di un'esecuzione e misure di performance.

6. Mini‑esempio: tempo di creazione in un'applicazione con lista di task

Immaginate di avere una semplice applicazione "Lista di task" (l'abbiamo sviluppata gradualmente prima: input/output, struct, vector, funzioni di stampa ordinate). Ora vogliamo aggiungere al task il campo "quando è stato creato".

Non stiamo costruendo un sistema di scheduling perfetto. Aggiungiamo un solo livello pratico: memorizzare l'istante come ms_since_epoch e stampare per l'utente tramite format_local().

Modello Task

#include <cstdint>
#include <string>

struct Task {
    int id = 0;
    std::string title;
    std::int64_t created_ms_since_epoch = 0;
};

Notate: memorizziamo un numero. È comodo da serializzare (es. in JSON come numero) e comodo per l'ordinamento.

Creare un nuovo task con l'ora corrente

#include <chrono>
#include <cstdint>
#include <string>

Task make_task(int id, const std::string& title) {
    auto now = std::chrono::system_clock::now();
    return Task{ id, title, to_unix_ms(now) };
}

Qui si usa to_unix_ms() dalla sezione precedente.

Stampare il task "in modo umano"

#include <iostream>

void print_task(const Task& t) {
    auto tp = from_unix_ms(t.created_ms_since_epoch);
    std::cout << "#" << t.id << " " << t.title
              << " (created: " << format_local(tp) << ")\n";
    // per esempio: #1 Buy milk (created: 2026-01-16 14:07:10)
}

Qui si vede chiaramente la separazione dei livelli: dentro Task si conserva un numero, e prima dell'output ricostruiamo time_point e formattiamo.

7. Come scegliere il formato di memorizzazione

Lo scopo non è memorizzare a memoria una tabella. Lo scopo è sviluppare il "fiuto": scegliere il formato in base al caso, non "come viene". Questo è particolarmente importante quando salvate dati: cambiare formato dopo è doloroso perché richiede migrazione dei vecchi file.

Memorizzazione Adatto per Esempio di campo Commento
std::int64_t ms_since_epoch
eventi, log, ordinamento temporale, precisione
created_ms_since_epoch
pratico "default"
std::time_t
(secondi)
date/tempi semplici, compatibilità con C API
created_seconds
meno precisione, ma più semplice
"YYYY-MM-DD HH:MM:SS" l'umano leggerà/modificherà, config
created_text
comodo per gli occhi, peggiore per l'elaborazione automatica

Se usate già JSON nel progetto, ms_since_epoch è particolarmente gradevole: si salva come numero, non richiede escaping e non dipende dalla locale.

Su ISO 8601, senza fanatismi

Spesso vedrete una forma tipo 2026-01-16T14:07:10. È simile a ISO 8601. La versione completa di solito include il fuso orario (Z per UTC o +03:00), ma qui non entriamo nei fusi orari per non trasformare la lezione in un "thriller scientifico".

Pensiero pratico: se scrivete una stringa, fatela univoca nell'ordine dei campi. Il formato YYYY-MM-DD è buono perché si ordina lessicograficamente nello stesso modo del tempo (purché zona e formato siano uguali).

Sapete già stampare YYYY-MM-DD HH:MM:SS con put_time. Se volete, potete sostituire lo spazio con T:

// dentro format_local:
out << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S"); // 2026-01-16T14:07:10

8. Errori tipici

Errore №1: memorizzare il tempo come int senza indicare l'unità.
int created = 1737040000; potrebbe essere in secondi, in millisecondi, o "secondi dall'avvio del programma". Dopo qualche settimana diventa un enigma del tipo "cosa intendeva l'autore". Si risolve in due passi: fissare l'unità nel nome (*_ms_since_epoch) e, se possibile, usare std::int64_t, perché i millisecondi diventano rapidamente numeri grandi.

Errore №2: confondere system_clock e steady_clock quando si salva su disco.
steady_clock serve per misure e deadline dentro il processo, ma non per il tempo "calendario". Se avete salvato il valore di steady_clock::now() e poi lo interpretate "come una data", otterrete fantascienza ben formata ma software difettoso. Per salvare istanti usate system_clock o numeri derivati da esso.

Errore №3: stampare time_since_epoch().count() senza convertire all'unità desiderata.
count() restituisce il numero nelle unità del tipo corrente di duration. Se non avete fatto duration_cast<std::chrono::milliseconds>(...), non potete affermare onestamente che sia "millisecondi". È meglio convertire prima all'unità desiderata, poi stampare il numero e annotare " ms".

Errore №4: prendere il puntatore da std::localtime() e conservarlo "per dopo".
std::localtime() restituisce un puntatore a un oggetto interno che può essere sovrascritto dalla chiamata successiva. Quindi il pattern corretto è copiare subito con std::tm tm = *std::localtime(&t);. Questo riduce la probabilità di effetti strani durante formattazioni ripetute.

Errore №5: salvare la data come stringa "come capita" e poi cercare di parsarla.
Se una volta avete scritto "16/01/2026 14:07" e poi decidete di voler i secondi, o cambiate l'ordine in "01-16-2026", vi siete creati una mini-migrazione. In un progetto didattico è fastidioso, in uno reale rompe la compatibilità inversa. Se salvate come stringa, fissate un formato e non lo cambiate senza motivo.

1
Compito
C++ SELF, livello 66, lezione 4
Bloccato
Suddivisione del tempo
Suddivisione del tempo
1
Compito
C++ SELF, livello 66, lezione 4
Bloccato
Convertitore circolare
Convertitore circolare
1
Compito
C++ SELF, livello 66, lezione 4
Bloccato
Coda di eventi
Coda di eventi
1
Compito
C++ SELF, livello 66, lezione 4
Bloccato
Log in UTC
Log in UTC
1
Sondaggio/quiz
std::chrono, livello 66, lezione 4
Non disponibile
std::chrono
std::chrono
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION