quinta-feira, 4 de fevereiro de 2016

Fusos horários brasileiros no Ruby on Rails

Estou fazendo um sistema onde o usuário pode adicionar uma data limite para a execução de algumas operações, após trabalhar um pouco em algumas telas e ter que fazer algumas conversões na data e hora para ser exibida corretamento no Bootstrap 3 Datepicker, me veio a preocupação em relação aos fusos horários brasileiros.

Inicialmente pensei em adotar o horário de Brasília e colocar observações nos locais onde as datas e hora seriam exibidas, pois inicialmente o sistema teria um uso maior nas regiões que utilizam este fuso horário, além disso tornaria a implementação um pouco mais fácil.

Depois pensei em como eu gostaria que o sistema se comportasse se fosse eu o usuário, aí não restou dúvida, mesmo sabendo que o público alvo se concentraria mais em uma região, o serviço ainda continuará na nuvem, podendo ser acessado de todo o território nacional.

O Brasil possui 27 Unidades Federativas, sendo que apenas 9 utilizam o horário de Brasília, desta forma optar por utilizar apenas este fuso horário, seria ignorar 18 UFs!

Existem diversos posts a respeito de como utilizar o fuso horário do usuário que fez a requisição, um é o It's About Time (Zones) do pessoal da thoughtbot, que descreve até a inclusão do campo do fuso horário na edição do perfil do usuário.

A classe do Rails que contem os fusos horários é a ActiveSupport::TimeZone, porém o único fuso horário brasileiro é o de Brasília e eu gostaria de colocar todos os fusos horários, são apenas 4, nas primeiras opções do select.

A biblioteca TZInfo, utilizada pelo ActiveSupport::TimeZone, apresenta os seguintes fusos para o Brasil:

br = TZInfo::Country.get('BR')
br.zone_names
=> ["America/Noronha",  "America/Belem",  "America/Fortaleza", "America/Recife",
"America/Araguaina",  "America/Maceio", "America/Bahia",  "America/Sao_Paulo",
"America/Campo_Grande", "America/Cuiaba",  "America/Santarem",  "America/Porto_Velho",
"America/Boa_Vista",  "America/Manaus",  "America/Eirunepe", "America/Rio_Branco"]

Para adicionar os novos fusos, criei o arquivo config/initializers/active_support/time_zone.rb com o seguinte conteúdo:

ActiveSupport::TimeZone::MAPPING['Fernando de Noronha'] = 'America/Noronha'
ActiveSupport::TimeZone::MAPPING['Amazônia'] = 'America/Manaus'
ActiveSupport::TimeZone::MAPPING['Acre'] = 'America/Rio_Branco'
ActiveSupport::TimeZone.instance_variable_set('@zones', nil)
ActiveSupport::TimeZone.instance_variable_set('@zones_map', nil)
module ActiveSupport
  class TimeZone
    def self.br_zones
      @br_zones ||= all.find_all { |z| z.name =~ /Brasilia|Noronha|Amazônia|Acre/ }
    end
  end
end

Fiz um mapeamento usando os nomes dos fusos horários que estão na Wikipédia, forcei as variávies @zones e @zones_map para nil, pois elas são cacheadas durante a criação do objeto que representa a classe ActiveSupport::TimeZone, desta forma quando  clientes da classe fazem uso dos métodos all e/ou zone_maps (as únicas formas de acessá-las sem metaprogramação), estas variáveis são recomputadas.

Por último adicionei o método br_zones, inspirado no método us_zones, presente na classe, o que facilita a criação do select:

<%= f.input :time_zone, priority: ActiveSupport::TimeZone.br_zones %>