Newsgroup: it.comp.programmare
Titolo: Ora legale e ora solare: un consiglio

Gente, mi serve un consiglio o un suggerimento.
Ho trovato una imprecisione in una procedura che in determinate 
circostanze sbaglia a calcolare l'ora.
Ho una tabella di log: ogni record contiene un campo col valore 
dell'orario in cui e' stato scritto.
Solo che questo campo contiene la data e l'ora in formato universale 
(numero di secondi trascorsi dal 1 gennaio 1970) riferito al meridiano 
di greenwich, quindi per trasformare il dato nel fuso orario italiano 
devo aggiungere 1 ora o 2 ore a seconda se siamo in regime di ora legale 
o ora solare.
Questa procedura, che opera tale trasformazione, commette un errore 
formale piuttosto grave: per sapere quanto aggiungere per trasformare 
l'orario, interroga il sistema operativo, il quale correttamente 
restituisce +1 o +2 a seconda del regime di orario *attuale*.
E questo funziona se si trasformano i valori del giorno corrente.
Il problema e' che se io vado a recuperare un record vecchio, e voglio 
convertire il campo di orario, la procedura mi aggiunge +1 o +2 in base 
al regime *attuale* e non in base a quello del momento in cui si 
riferisce il record.
Si accettano suggerimenti su come procedere.
Sono consentite tutte le funzioni standard di un linguaggio procedurale 
di alto livello.
E' nota la regola che il cambio di ora avviene l'ultima domenica di 
marzo e l'ultima di ottobre.
Non si possono effettuare chiamate al sistema operativo.
Non si puo' usare una tabella di appoggio.
Le date risalenti a quando il cambio dell'ora legale era in settembre 
invece che in ottobre non contano perche' non ci sono dati di 
quell'epoca, quindi possiamo ignorarle.
Non e' necessario prendere in considerazione l'eventualita' che la 
regola dell'ora legale possa cambiare in futuro.
La funzione deve ricevere come argomento un longint col numero di 
secondi dalla mezzanotte del 1/1/70 e restituire un datetime (o una 
stringa con la data e l'ora)

 
[Really big cut]
Allora, il problema è che vuoi salvare dei record.
Utilizzando UTC nel salvataggio non hai problemi di ore legali, solari e
fusi orari di qualsiasi genere.
Però, se tu volessi risalire all'ora locale in quel periodo non hai la
possibilità di sapere le regole in vigore in quel periodo, giusto?
La cosa più semplice (costo contenuto e funziona sicuro da qui
all'eternità, anche se l'Italia decidesse di adottare il fuso orario
della Romania) è di salvare 2 timestamp per record, uno in UTC e l'altro
in localtime.
Per ordinamenti et simili lavori su quello in UTC.
Per verifiche (visualizzazioni, ricerche ecc.) lavori con quello in
localtime.
Salvando anche il flag di DST, se qualcuno ti chiede le 2:30 del giorno
di transizione da solare a legale (o l'altra? qual'è quello che torna
indietro di un'ora?) mostrerai 2 record, uno con le 2:30 ora solare e
l'altro con le 2:30 ora legale, ma in UTC avvenuto un'ora dopo.
Niente funzioni che non avrebbero senso né potrebbero funzionare senza
tabelle a lato, per il semplice fatto che le variazioni alla gestione
dell'ora solare non vengono prese in base a equazioni matematiche, ma in
base a scelte "politiche", quindi fuori da schemi,
Luca

 base a scelte "politiche", quindi fuori da schemi,
La tua e' una buona soluzione, che l'analista avrebbe dovuto prendere
in considerazione a suo tempo.
Invece l'applicativo scrive in UTC, che cosi' non ha problemi di
valori duplicati in caso di cambi di fuso orario e ore legali, e io
poi devo convertire in data perche' tanto "e' facile".
Ieri rimuginandoci sopra mi sono reso conto che facile non e'...

 
Passo indietro, allora.
Che linguaggio usi?
Io vedo 2 soluzioni: usare la localtime (o equivalente o altre funzioni
di sistema) per convertire il timestamp da UTC a CET, oppure costruirti
una tabella (una array in memoria) con gli UTC di partenza e di fine
dell'ora legale, oppure scrivere una funzione legata a doppia mandata
con il nostro fuso orario (ma non mi sembra sia questo il problema).
In quest'ultimo modo puoi lavorare con quasi qualsiasi linguaggio (deve
per lo meno avere la conversinone da unix epoch time a human readable time)
Mi spiego meglio.
Noi siamo a CET, quindi UTC+1 più eventualmente anche l'ora legale.
Quindi dal timestamp in UTC aggiungi 3600 per avere CET senza DST e poi
verifichi se ti trovi in un intervallo di date in cui dovrebbe essere
attiva la DST, nel qual caso aggiungi ancora 3600.
Poi utilizzi le funzioni di GMTTime (quindi senza ulteriori traduzioni
con il fuso orario).
Per gli intervalli è sufficiente una associazione anno-giorno
inizio-giorno fine, ad esempio
2007,45,254
2008,46,255
ecc..
Te la compili a mano per una 20na di anni (si tratta di una array di
interi con dimensioni 20x3) e le uniche conversioni da fare sono quelle
di tradurre i giorni come ordinale dall'inizio dell'anno a timestamp
alle 2 del mattino per fare le verifiche. Se le regole, dal 2010 per
dire un anno a caso, dovessero cambiare, ricalcoli solo i giorni di
inizio e fine per gli anni dal 2010 in poi e vivi comunque felice.
Occhio che gli anni multipli di 4 ad eccezione di quelli multipli di
100, ma reintegrando quelli multipli di 400 sono bistestili, quindi con
29 giorni a febbraio.
Di più nin so.
Luca

 
Anche questa soluzione ho considerato.
Volevo evitare tabelle di appoggio.
E non mi piace lasciare in giro procedure a scadenza: il Y2K e' passato 
e non mi pare il caso di riproporlo con una tabella che nessuno si 
prendera' la briga di aggiornare in futuro.

 
[...]
Non avevi detto che si trattava di un server sql.
Almeno sai che tipo di dbms si tratta e se potrebbe cambiare in futuro?
Se supporta i trigger e un minimo di procedure (non dico perl, ma
almento plsql), potresti (ma non so se te lo fanno fare) aggiungere una
colonna nullable alla tabella e mettere un trigger sull'inserimento che
dalla data in UTC genera al volo la data in localtime.
Poi con un programma esterno completi la tabella con le date passate.
In questo modo non modifichi il programma che aggiunge i dati, ma ti
calcoli al volo i valori (sempre che sia possibile con il dbms che hai a
disposizione).
E non aggiungi tabelle allo schema.
Questa è veramente la mia ultima idea.
Però una domanda me la devi concedere.
Ok che i dati sono su un db.
Ok che le query devono fare ricerche sulle date e tutto.
Ma le maschere utente non sono scritte in SQL, giusto?
Non è che potresti lavorare sulle maschere di consultazione in modo che
fungano da interfaccia comunicando con l'utente in CET e con il db in
UTC? (se hai scartato questa soluzione, un motivo c'è, ma non l'hai detto)
Luca

 UTC? (se hai scartato questa soluzione, un motivo c'è, ma non l'hai detto)
Il motivo e' che non e' roba mia, non so chi usa quei dati.
Ho solo trovato in produzione questa procedura, che si comporta in modo 
oggettivamente sbagliato, e converte dei dati, che non sono io che 
scrivo, ma che andranno ad incrociarsi con i dati che scrivo io in altre 
tabelle, per produrre dei risultati di cui ignoro la destinazione.
Vorrei semplicemente prevenire un possibile problema che se emerge, mi 
coinvolgera' al 100%.

Secondo voi e' sufficiente come condizione
dato dd=giorno mm=mese w=giorno_settimana
dire che
se mm=marzo and dd>=25 and w=domenica
allora siamo nell'ultima domenica di marzo
e analogamente per ottobre.
A me pare sufficiente, ma siccome non ho mai trovato niente del genere 
non vorrei che mi sfuggisse qualche controindicazione.
Se funziona ho trovato il modo per risolvere il mio problema.


Sě. Facendo un rapido controllo tra il 2000 e il 2020 risulta una sola
domenica sia nell'intervallo di marzo che in quello di ottobre.
Quindi, a meno di editti papali, dovrebbe andare bene.
LP

non vorrei che mi sfuggisse qualche controindicazione.
A quel che ne so, non è mai successo nulla che abbia interrotto la sequenza
settimanale (neanche il passaggio al calendario gregoriano), o comunque
modificato l'ultima settimana di marzo o ottobre, sicché non mi riesce di
vedere nulla che modifichi il calcolo. Neanche a me però è mai capitato di
vedere calcolare l'ultima domenica del mese in questo modo, forse proprio
perché di solito la domanda è "qual è l'ultima domenica del mese?" e non
"questa è l'ultima domenica del mese?".
Ciao.

 
Effettivamente hai centrato il punto.
Coinsiderando l'intervallo di giorni dal 25 al 31, se becco la domenica, 
quella e' senz'altro l'ultima domenica del mese.
Ma se becco un mercoledi, non sono mica sicuro se l'ultima domenica sia 
gia' passata o se debba ancora arrivare, in funzione di sapere se 
applicare l'ora estiva oppure no.
Tra l'altro, tanto per complicare le cose, mi sono accorto che la 
funzione Oracle che restituisce il giorno della settimana, e' 
maledettamente server-depended: se il server e' impostato in inglese la 
settimana parte dalla domenica, mentre se in italiano, la settimana 
parte da lunedi, o qualcosa del genere.
Non mi ritorna lo stesso valore per lo stesso giorno.
Me ne sono accorto perche' il server di produzione e' settato in un modo 
e quello di sviluppo in un altro (evviva le cose fatte bene!)
Io ODIO le localizzazioni dei server! E ho ragione ad odiarle.

Ho risolto!
Lascio il codice SQL, nel caso possa interessare a qualcuno.
Prima ho definito una funzione WEEKDAY(DATE) che mi ritorna il numero del
giorno della settimana (0=domenica, 1=lunedi, ... 6=sabato), che funziona
indipendentemente dai settaggi del server, che altrimenti il numero del
giorno  della settimana puo' variare.
FUNCTION "WEEKDAY" ( DT IN DATE) RETURN INTEGER
--
-- Ritorna il numero del giorno della settimana
-- 0=Domenica, 1=Lunedi, ... 6=Sabato
-- Senza preoccuparsi del settaggio del server
--
IS
BEGIN
    -- Prendo la data nel calendario Giuliano,
    -- aggiungo 1 per riportarmi nel range desiderato
    -- dividendo in modulo 7 ottengo il risultato voluto                 
    RETURN (MOD(TO_NUMBER(TO_CHAR(DT, 'J')), 7));
END; -- Function WEEKDAY
/
Questa funzione viene usata dalla seguente:
FUNCTION "UTC2DATE" (UTC IN NUMBER) RETURN DATE
--
-- Trasforma una data in formato UTC (numero di secondi trascorsi a
partire dal 1/1/1970)
-- nel formato DATE di Oracle
--
IS
    v_DT   Date;
    v_LS03 Integer;
    v_LS10 Integer;
    v_YYYY   Integer;	 
BEGIN
    -- Data di riferimento iniziale 
    v_DT := TO_DATE('01-01-1970', 'DD-MM-YYYY');
 
    -- Aggiungo il tempo UTC 
    v_DT := v_DT + (UTC / 86400);
 
    -- Aggiungo 1 ora per portarmi nel fuso orario italiano
    v_DT := v_DT + (1/24);
 
    -- Ricavo l'anno della data
    v_YYYY := TO_NUMBER(TO_CHAR(v_DT, 'YYYY'));
 
    -- Ricavo l'ultima domenica di marzo dell'anno v_YYYY 
    -- sottraendo a 31 il suo giorno della settimana
    v_LS03 := 31 - WEEKDAY(TO_DATE('31/03/' || v_YYYY, 'DD/MM/YYYY'));
 
    -- Ricavo l'ultima domenica di ottobre dell'anno YYYY 
    -- sottraendo a 31 il suo giorno della settimana
    v_LS10 := 31 - WEEKDAY(TO_DATE('31/10/' || v_YYYY, 'DD/MM/YYYY'));
 
    -- Se la data e' compresa nell'intervallo di ora estiva, aggiungo
un'altra ora
    IF v_DT BETWEEN TO_DATE(v_LS03 || '/03/' || v_YYYY || ' 02:00',
'DD/MM/YYYY HH24:MI')
                AND TO_DATE(v_LS10 || '/10/' || v_YYYY || ' 02:00',
'DD/MM/YYYY HH24:MI') THEN
        v_DT := v_DT + (1/24);
    END IF;
 
    RETURN(v_DT);
 
END; -- Function UTC2DATE
/
Il succo della funzione sta nel calcolo dell'ultima domenica del mese, che
si ottiene abbastanza banalmente sottraendo al giorno dell'ultimo del
mese, il suo giorno della settimana, con la convenzione 0=dom, 1=lun,
2=mar.
Per esempio se il 31 Marzo fosse stato mercoledi (3) allora l'ultima
domenica di marzo sara' stata il 31-3=28.
In questo modo, anno per anno, la funzione calcola l'ultima domenica del
mese e valuta l'intervallo di tempo in cui si deve applicare l'ora estiva.
Senza tabelle, senza altri casini.
Ed e' indipendente da qualunque settaggio del sistema operativo.
-- 
questo articolo e` stato inviato via web dal servizio gratuito 
http://www.newsland.it/news
 segnala gli abusi ad 
ab...@newsland.it