Assine a Lista Dicas-L
Receba diariamente por email as dicas
de informática publicadas neste site
Para se descadastrar, clique aqui.
Tratando as exceções da nova regra para o Horário de Verão
Colaboração: Miguel Angelo Rozsas
Data de Publicação: 20 de September de 2008
A partir de 2008 os dias em que começam e terminam o horário de verão no Brasil serão fixos, válido pelo menos até quando o "Grande Colisor de Hádrons" produza um buraco negro que saia do controle e "engula" o planeta ou talvez até o inicio do mandato do próximo presidente, o que ocorrer primeiro.
Segundo publicado no diário oficial da União, o horário de verão começará sempre no terceiro domingo de Outubro e terminará no terceiro domingo de fevereiro, exceto se tal domingo, for o domingo de carnaval. Nesse caso, termina no quarto domingo.
As datas fixas facilitam um pouco, pois é possivel uma configuração também fixa. Não levando em conta a exceção, o arquivo de timezone ficaria assim:
#Rule NAME FROM TO TYPE IN ON AT SAVE LETTER/S Rule BR 2008 only - Feb 17 0:00 0:00 S Rule BR 2008 MAX - Oct Sun>=15 0:00 1:00 D Rule BR 2009 MAX - Feb Sun>=15 0:00 0:00 S #Zone NAME GMTOFF RULES/SAVE FORMAT [UNTIL] Zone Brazil/East -3:00 BR BR%s
e a compilação do arquivo e uma rápida verificação retornaria:
[root@babylon5 ~]# zic -v /tmp/Brazil_East.zic [root@babylon5 ~]# zdump -v -c 2016 Brazil/East Brazil/East Fri Dec 13 20:45:52 1901 UTC = Fri Dec 13 17:45:52 1901 BRS isdst=0 gmtoff=-10800 Brazil/East Sat Dec 14 20:45:52 1901 UTC = Sat Dec 14 17:45:52 1901 BRS isdst=0 gmtoff=-10800 Brazil/East Sun Oct 19 02:59:59 2008 UTC = Sat Oct 18 23:59:59 2008 BRS isdst=0 gmtoff=-10800 Brazil/East Sun Oct 19 03:00:00 2008 UTC = Sun Oct 19 01:00:00 2008 BRD isdst=1 gmtoff=-7200 Brazil/East Sun Feb 15 01:59:59 2009 UTC = Sat Feb 14 23:59:59 2009 BRD isdst=1 gmtoff=-7200 Brazil/East Sun Feb 15 02:00:00 2009 UTC = Sat Feb 14 23:00:00 2009 BRS isdst=0 gmtoff=-10800 Brazil/East Sun Oct 18 02:59:59 2009 UTC = Sat Oct 17 23:59:59 2009 BRS isdst=0 gmtoff=-10800 Brazil/East Sun Oct 18 03:00:00 2009 UTC = Sun Oct 18 01:00:00 2009 BRD isdst=1 gmtoff=-7200 ...
Como podem ver, em 2008, o horário de verão começaria em 19 de Outubro e terminaria em 15 de fevereiro.
Mas a tal exceção me deixou intrigado. A exceção tirou a beleza de uma configuração fixa, o que me fez coçar os neurônios.
Já conhecia a man page do zic(1) há tempos. Lá fala de uma função
yearistype que é chamada para cada ano, e se retornar verdadeiro (true)
a respectiva linha no time zone é aplicada.
Humm...então essa função poderia então ser usada para tratar a exceção da regra do "Horário de Verão Brasileiro a partir de 2008". Para tanto, "basta" saber se o domingo que antecede o carnaval cai no terceiro domingo do mês. Se cair, aplica-se a regra do quarto domingo do mês. Simples não ?
Humm...o problema é que o carnaval, uma festa pagã, é data móvel, determinada pela data em que cai outro feriado, a páscoa, um feriado sagrado da igreja Católica Romana, outra data móvel, que é determinada, segundo regras estabelecidas pelo concilio de Nicea no longiguo ano de 325, como sendo o primeiro domingo depois da primeira lua cheia depois do equinócio vernal (equinócio da primavera no hemisfério norte).
Humm...complicado heim ?
Bem, como Google é pai e wikipedia é mãe, acabei encontrando o algoritmo elaborado por "Meeus/Jones/Butcher" válido para quem usa o calendário gregoriano (somos nós ! - Os católicos ortodoxos orientais usam o calendário Juliano) (http://en.wikipedia.org/wiki/Computus#Algorithms)
Humm...então basta eu implementar esse algoritmo para descobrir a data da páscoa de um determinado ano, obter dai a data do carnaval e testar se domingo que o antecede é o terceiro domingo e se for, aplica-se a exceção! Está ficando simples....
Implementei o algoritmo de "Meeus/Jones/Butcher" em dc(1). (Salve os
comandos dc abaixo em /usr/local/lib/easter.dc)
# y=year from cmd line sy 0 k # a=y mod 19 ly 19 % sa # b=y/100 ly 100 / sb # c=y mod 100 ly 100 % sc # d=b/4 lb 4 / sd # e=b mod 4 lb 4 % se # f=(b+8)/25 lb 8 + 25 / sf # g = (b - f + 1) / 3 lb lf - 1 + 3 / sg # h = (19 × a + b - d - g + 15) mod 30 19 la * lb + ld - lg - 15 + 30 % sh # i = c / 4 lc 4 / si # k = c mod 4 lc 4 % sk # L = (32 + 2 × e + 2 × i - h - k) mod 7 32 le 2 * + li 2 * + lh - lk - 7 % sl # m = (a + 11 × h + 22 × L) / 451 la 11 lh * + 22 ll * + 451 / sm # n=(h + L - 7 × m + 114) lh ll + 7 lm * - 114 + sn # month= n/31 ln 31 / 4 k 100 / # day=(n mod 31)+1 0 k ln 31 % 1 + 4 k 10000 / + ly + p
...e use-o como:
[miguel@babylon5 ~]$ dc -e 2008 -f /usr/local/lib/easter.dc 2008.0323 [miguel@babylon5 ~]$ dc -e 2009 -f /usr/local/lib/easter.dc 2009.0412 [miguel@babylon5 ~]$ dc -e 2010 -f /usr/local/lib/easter.dc 2010.0404 [miguel@babylon5 ~]$
(as datas da páscoa dos anos passados na CLI são retornadas no formato yyyy.mmdd)
humm...agora é necessário descobrir a data do carnaval. O tal conselho de Nicea decidiu que deve se haver pelo menos 40 dias entre o final do carnaval e a sexta-feira santa que antecede a páscoa, a fim de que os fieis se preparem e jejuem adequadamente depois daquela esbórnia da festa pagã em homenagem ao deus "Sol" Trocando em miúdos, entre a terça-feira do carnaval e o domingo de páscoa devem haver exatos 47 dias. Mas eu quero saber não quando é a terça feira de carnaval, mas sim, quando é o domingo que antecede tal terça-feira - ora, trivial, são 49 dias.
humm...como subtrair de uma data (a data da páscoa), um determinado número de dias (47) ? :scratch:
Os leitores talvez possam sugerir outras alternativas. Eu usei os comandos jday e j2d do pacote jday do fedora 9 (jday-2.4-3.fc9.i386) (http://sourceforge.net/projects/jday/)
"basta" então converter uma data (a data da páscoa) para um número Juliano (usando o comando jday), subtrair 49 (usando expr) e converter esse outro número juliano para uma data do calendário gregoriano (usando j2d) !
Usando o código abaixo vcs podem verificar que o domingo que antecede o carnaval no ano de 2009 sera o dia 22 de fevereiro, "simples" não ?
year="2009"
easter=$(dc -e $year -f /usr/local/lib/easter.dc | sed -e 's/\./-/; s/-\([0-9]\{2\}\)/-\1-/')
jday=$(jday -d $easter 0:0:0 | cut -d. -f 1)
# The sunday before carnival is 49 dias before easter (48, in jday account)
jcar=$(expr $jday - 48)
carnival=$(j2d $jcar | cut -d' ' -f 1)
echo "Sunday of carnival is on $carnival"
Voilá...então se o tal dia for maior ou igual a 15 e menor que 22, então é o terceiro domingo ! Fim da coceira nos neuronios.
Então já temos tudo para o compilador de zonas criar um arquivo binário com todas a regra do "Horario Brasileiro de Verão", incluindo as exceções.
O arquivo de zonas fica assim:
#Rule NAME FROM TO TYPE IN ON AT SAVE LETTER/S Rule BR 2008 MAX - Oct Sun>=15 0:00 1:00 D Rule BR 2008 MAX regular Feb Sun>=15 0:00 0:00 S Rule BR 2008 MAX carnOn3rdSun Feb Sun>=22 0:00 0:00 S #Zone NAME GMTOFF RULES/SAVE FORMAT [UNTIL] Zone Brazil/East -3:00 BR BR%s
O arquivo acima faz menção a dois tipos de anos: "regular" é o ano onde o horário de verão começa no terceiro domingo. "carnOn3rdSun" é o ano onde o horário de verão começa no quarto domingo porque o domingo que antecede o carnaval cai no terceiro domingo.
O programa zic(1) irá chamar o programa yearistype passando como
primeiro argumento um ano, e como segundo argumento o conteúdo do campo
TYPE ("regular" ou carnOn3rdSun)
humm...(é o último, eu prometo), mas cadê o tal programa
yearistype? Basicamente é o código imediatamente acima, em "bash",
com o valor de retorno correto. Não fiz a critica sobre os argumentos de
entrada, uma vez que esse código é para ser chamado pelo zic, sempre da
mesma maneira. Mas quem quiser, fique a vontade para fazer o tratamento dos
argumentos de entrada.
Salve o código abaixo em /usr/local/bin ou outro lugar que o zic
(como root) possa chamar o programa. Verifique se o seu programa zic
aceita a especificação do path do comando yearistype. No Fedora 9 é
possivel usar a chave -y para indicar o path do comando yearistype
(zic -y ~miguel/bin/yearistype zone_file.zic)
#!/bin/bash
#
# Usage: yearistype year type
#
year=$1
type=$2
function check3rdSun() {
easter=$(dc -e $year -f /usr/local/lib/easter.dc | sed -e 's/\./-/; s/-\([0-9]\{2\}\)/-\1-/')
jday=$(jday -d $easter 0:0:0 | cut -d. -f 1)
# The sunday before carnival is 48 dias before easter
jcar=$(expr $jday - 48)
carnival=$(j2d $jcar | cut -d' ' -f 1)
#echo "Sunday of carnival is on $carnival"
# I want only the day of month
carday=$(echo $carnival | cut -d- -f3)
# return 0 if it is the 3rd Sunday, 1 otherwise
[ "$carday" -ge "15" ] && [ "$carday" -lt "22" ] && return 0 || return 1
}
check3rdSun $year
rc=$?
case $type in
carnOn3rdSun)
#echo "carnOn3rdSun: $carnival $rc"
exit $rc
;;
*)
#echo "regular: $carnival $rc"
[ "$rc" = "0" ] && exit 1 || exit 0
esac
O próximo passo é a compilação do arquivo de zonas e teste:
[root@babylon5 ~]# zic -v -y ~miguel/bin/yearistype ~miguel/src/Brazil_East.zic [root@babylon5 ~]# zdump -v -c 2016 Brazil/East ... Brazil/East Sun Feb 17 01:59:59 2008 UTC = Sat Feb 16 23:59:59 2008 BRD isdst=1 gmtoff=-7200 Brazil/East Sun Feb 17 02:00:00 2008 UTC = Sat Feb 16 23:00:00 2008 BRS isdst=0 gmtoff=-10800 Brazil/East Sun Oct 19 02:59:59 2008 UTC = Sat Oct 18 23:59:59 2008 BRS isdst=0 gmtoff=-10800 Brazil/East Sun Oct 19 03:00:00 2008 UTC = Sun Oct 19 01:00:00 2008 BRD isdst=1 gmtoff=-7200 Brazil/East Sun Feb 15 01:59:59 2009 UTC = Sat Feb 14 23:59:59 2009 BRD isdst=1 gmtoff=-7200 Brazil/East Sun Feb 15 02:00:00 2009 UTC = Sat Feb 14 23:00:00 2009 BRS isdst=0 gmtoff=-10800 Brazil/East Sun Oct 18 02:59:59 2009 UTC = Sat Oct 17 23:59:59 2009 BRS isdst=0 gmtoff=-10800 Brazil/East Sun Oct 18 03:00:00 2009 UTC = Sun Oct 18 01:00:00 2009 BRD isdst=1 gmtoff=-7200 ... Brazil/East Sun Feb 26 01:59:59 2012 UTC = Sat Feb 25 23:59:59 2012 BRD isdst=1 gmtoff=-7200 Brazil/East Sun Feb 26 02:00:00 2012 UTC = Sat Feb 25 23:00:00 2012 BRS isdst=0 gmtoff=-10800 ... Brazil/East Sun Feb 22 01:59:59 2015 UTC = Sat Feb 21 23:59:59 2015 BRD isdst=1 gmtoff=-7200 Brazil/East Sun Feb 22 02:00:00 2015 UTC = Sat Feb 21 23:00:00 2015 BRS isdst=0 gmtoff=-10800 ...
Em 2009 o horário de verão termina às 0:00hs do dia 15 (voltando a ser 23:00hs do dia 14), seguindo a regra "regular". Já em 2012 ocorre uma das exceções: O horário de verão termina em 26 de fevereiro, que é o quarto domingo, porque o carnaval em 2012 ocorre em 21, sendo o domingo que o antecede, o terceiro domingo do mês. Outra excessão ocorre em 2015, com o horário de verão terminando no quarto domingo, dia 22.
Para ver todas as excessões, edite o arquivo yearistype des-comentando os
dois comandos echo que existem dentro do case (echo ``carnOn3rdSun:
$carnival $rc) e (echo ``regular: $carnival $rc).
Depois disso, execute o comando dentro de um laço for do bash:
[miguel@babylon5 ~]$ for y in $(seq 2008 2100 ) ; do ~/bin/yearistype $y carnOn3rdSun; done carnOn3rdSun: 2008-02-03 1 carnOn3rdSun: 2009-02-22 1 carnOn3rdSun: 2010-02-14 1 carnOn3rdSun: 2011-03-06 1 carnOn3rdSun: 2012-02-19 0 carnOn3rdSun: 2013-02-10 1 carnOn3rdSun: 2014-03-02 1 carnOn3rdSun: 2015-02-15 0 ...
Todas as linhas que terminam com 0 indicam os anos cujo domingo de carnaval
cai no terceiro domingo do mes. São as excessões da regra geral.
Vocè pode até compilar o arquivo de zonas com o zic deixando o arquivo
yearistype com os echos des-comentados. Você irá ver a invocação,
pelo zic, do comando yearistype e a respectiva saida. Experimente !
Há espaço para otimizações.
Eu suspeito que o uso do comando jday/j2c pode ser eliminado, calculando-se diretamente a data do carnaval, não a data da pascóa.
Para isso, é necessário adaptar o algoritmo de "Meeus/Jones/Butcher" para obter uma data (47+2) dias antes, mas é preciso investigar o algoritmo para verificar se isso é realmente possivel e mais fácil do que usar j2c/jday. Fica a sugestão para os mais aventurosos.
Foi um dia produtivo. Me diverti resolvendo o problema e depois mais ainda em escrever esse blog que espero ser útil para demonstrar a flexibilidade do unix e o poder dos scripts e utilitários.
# y=year from cmd line
sy
0 k
# a=y mod 19
ly 19 % sa
# b=y/100
ly 100 / sb
# c=y mod 100
ly 100 % sc
# d=b/4
lb 4 / sd
# e=b mod 4
lb 4 % se
# f=(b+8)/25
lb 8 + 25 / sf
# g = (b - f + 1) / 3
lb lf - 1 + 3 / sg
# h = (19 × a + b - d - g + 15) mod 30
19 la * lb + ld - lg - 15 + 30 % sh
# i = c / 4
lc 4 / si
# k = c mod 4
lc 4 % sk
# L = (32 + 2 × e + 2 × i - h - k) mod 7
32 le 2 * + li 2 * + lh - lk - 7 % sl
# m = (a + 11 × h + 22 × L) / 451
la 11 lh * + 22 ll * + 451 / sm
# n=(h + L - 7 × m + 114)
lh ll + 7 lm * - 114 + sn
# month= n/31
ln 31 /
4 k
100 /
# day=(n mod 31)+1
0 k
ln 31 % 1 +
4 k
10000 / + ly +
p
#!/bin/bash
#
# Usage: yearistype year type
#
year=$1
type=$2
function check3rdSun() {
easter=$(dc -e $year -f /usr/local/lib/easter.dc | sed -e 's/\./-/; s/-\([0-9]\{2\}\)/-\1-/')
jday=$(jday -d $easter 0:0:0 | cut -d. -f 1)
# The sunday before carnival is 48 dias before easter
jcar=$(expr $jday - 48)
carnival=$(j2d $jcar | cut -d' ' -f 1)
#echo "Sunday of carnival is on $carnival"
# I want only the day of month
carday=$(echo $carnival | cut -d- -f3)
# return 0 if it is the 3rd Sunday, 1 otherwise
[ "$carday" -ge "15" ] && [ "$carday" -lt "22" ] && return 0 || return 1
}
check3rdSun $year
rc=$?
case $type in
carnOn3rdSun)
#echo "carnOn3rdSun: $carnival $rc"
exit $rc
;;
*)
#echo "regular: $carnival $rc"
[ "$rc" = "0" ] && exit 1 || exit 0
esac
Veja a relação completa dos artigos de Miguel Angelo Rozsas
Referências Adicionais
Referências adicionais sobre os assuntos abordados neste site podem ser encontradas em nossa Bibliografia.
Avalie esta dica
Opinião dos Leitores
20 Set 2008, 23:21
20 Set 2008, 23:14
Parabens pelo artigo.
Acho que voce poderia incluir mais um parágrafo para ensinar como apontar o Linux para trabalhar com a zona alterada, copiando o arquivo gerado para /etc/localtime ou fazendo um link simbolico para /usr/share/zoneinfo/Brazil/East.
Abraços
JGeraldo
20 Set 2008, 13:08
20 Set 2008, 12:21
Sinceramente, se há uma exceção à regra (for carnaval no domingo), não adiantou nada! Continuarão sendo necessárias atualizações ou intervenções manuais para corrigir o horário de verão... Por que não fizeram um negócio decente e colocaram a data de término do horário de verão em um domingo que certamente nunca será carnaval?
20 Set 2008, 11:10
#!/usr/bin/env python
# yearistype
import mx.DateTime
import sys
year = int(sys.argv[1])
yearType = sys.argv[2]
carnivalSunday = mx.DateTime.Feasts.EasterSunday(year) - 49 * mx.DateTime.oneDay
returnCode = 15 <= carnivalSunday.day < 22
if yearType != 'regular': returnCode = not returnCode
returnCode = int(returnCode)
print carnivalSunday.Format('%Y-%m-%d'), returnCode
sys.exit(returnCode)
20 Set 2008, 11:02
Gostaria de aproveitar a oportunidade e deixar uma contribuição para quem aprecia a linguagem de programação Python.
Se instalar o pacote abaixo da sua distribuição Linux (estou usando o Ubuntu 7.10),
python-egenix-mxdatetime
o programa yearistype pode ficar assim:
#!/usr/bin/env python
# yearistype
import mx.DateTime
import sys
year = int(sys.argv[1])
yearType = sys.argv[2]
carnivalSunday = mx.DateTime.Feasts.EasterSunday(year) - 49 * mx.DateTime.oneDay
returnCode = 15 <= carnivalSunday.day < 22
if yearType != 'regular':
returnCode = not returnCode
returnCode = int(returnCode)
print carnivalSunday.Format('%Y-%m-%d'), returnCode
sys.exit(returnCode)
20 Set 2008, 09:10
19 Set 2008, 02:46
18 Set 2008, 19:26
18 Set 2008, 18:13












