Aritmética de Datas com PHP

Artigo que apresenta funções do PHP para manipular datas a fim de se realizar contas como somar dias, meses, anos, etc.

Introdução
Calendário

Aritmética de datas é o processo de realizar contas utilizando datas e intervalos de tempo.

O fator mais complicante nestes tipos de operações é que data não é algo tão exato quanto se imagina. Tudo por conta que a Terra leva 365,2422 dias para completar uma volta inteira no Sol. Para entender com detalhes, leia esta explicação.

Em termos computacionais, uma data pode ser considerada um conjunto de componentes (dia, mês, ano, hora, minuto, segundo, fuso horário, etc), ou, se observarmos a essência da data, podemos considerá-la como um "instante único" (na Terra). Na prática, trabalhar com as componentes de maneira individual para realizar contas com datas é complicado. Para facilitar, foi criada a famosa "Era Unix", que é uma forma de representar um timestamp através do número de segundos decorrentes desde 01/01/1970. Para realizar operações, portanto, todas as componentes são convertidas em segundo e depois é obtido o valor das componentes desejadas (dia, mês, etc) através de um banco de informações.

Entretanto, o tipo de dado utilizado para guardar o timestamp é o inteiro, que na arquitetura de computadores de 32 bits, consegue armazenar valores até o ano de 2038. Para a arquitetura de 64 bits isso se resolveria por um prazo bastante razoável.


Funções para Manipulação de Data

Antes de PHP 5.2, a forma disponível para se realizar aritmética de datas era através de algumas funções que trabalhavam com timestamp. As principais delas são:

  • Geração de Timestamp
    • mktime - Para criar um timestamp a partir de componentes de data.
    • time - Para obter o timestamp atual.
  • Leitura (parse) de uma data
    • strptime - Para obter as componentes de uma data formatada com strftime (string parse time).
    • strtotime - Para obter um timestamp de uma data em formato americano e, opcionalmente, fazer contas com a data base.
    • Funções de string como preg_match, explode, strpos e substr.
  • Escrita (formatação) de uma data
    • strftime - Para formatar um timestamp (string from time).
    • date - Para formatar um timestamp (com lista de formatos diferentes de strftime).
  • Validação de Datas
    • checkdate - Para checar se uma data existe.

A aritmética de datas está escondida em algumas funções como mktime e strtotime.

Primeiramente, mktime devolve sempre um timestamp. Se pedirmos o timestamp do dia 32 de janeiro, a função irá devolver, na verdade, o timestamp do dia primeiro de fevereiro (afinal, janeiro tem 31 dias). Por outro lado, se pedirmos o dia zero de janeiro, será devolvido o último dia de dezembro do ano anterior.

// Criando o timestamp (hora, minuto, segundo, mês, dia, ano)
$time = mktime(0, 0, 0, 1, 32, 2010);

// Formatando o timestamp calculado (01/02/2010 00:00:00)
echo strftime('%d/%m/%Y %H:%M:%S', $time);

Se temos a uma data no formato dd/mm/aaaa, como somar 40 dias?

$data = '01/01/2010';

// Obter as componentes de data com strptime
$componentes = strptime($data, '%d/%m/%Y');
$dia = $componentes['tm_mday'];
$mes = $componentes['tm_mon'] + 1;                                  
$ano = $componentes['tm_year'] + 1900;

// Ou obter as componentes com preg_match
preg_match('/^(\d+)\/(\d+)\/(\d+)$/', $data, $matches);
list($data, $dia, $mes, $ano) = $matches;

// Realizar a soma de 40 dias
$time = mktime(0, 0, 0, $mes, $dia + 40, $ano);

// Formatar a data obtida
echo strftime('%d/%m/%Y', $time); // 10/02/2010

Atenção: somar 40 dias é diferente de somar 1 mês e 10 dias. Afinal, "1 mês" não é algo exato, já que cada mês possui uma quantidade diferente de dias.

A função strtotime possui algumas utilidades extras, como, por exemplo, somar semanas ou obter o último sabado a partir de um timestamp:

// Um timestamp qualquer     
$time1 = mktime(0, 0, 0, 1, 1, 2010);

// Somar uma semana e dois dias
$time2 = strtotime('+1 week 2 day', $time1);

// Formatar a data obtida
echo strftime('%d/%m/%Y', $time2);

// Obter ultimo sabado a partir de $time1
$time3 = strtotime('last saturday', $time1);

// Formatar a data obtida
echo strftime('%d/%m/%Y', $time3);

A comparação de duas datas é feita por um processo semelhante: primeiro duas datas são convertidas para timestamp, depois os dois timestamps são comparados normalmente (comparação de inteiros). Para comparar componentes (checar se duas datas estão no mesmo mês, por exemplo), precisa primeiro converter para timestamp, obter a componente desejada e comparar a componente.


As classes DateTime, DataInterval, DatePeriod e DateTimeZone

As classes DateTime, DataInterval, DateTimeZone e DatePeriod são formas mais atuais de se trabalhar com datas utilizando o paradigma Orientado a Objetos.

Os principais métodos são:

  • DateTime
    • __construct - Criar um objeto a partir de uma data no formato americano ou data vazia.
    • createFromFormat - Criar um objeto a partir de uma data formata (mesmo formato aceito na função date).
    • diff - Obtém o intervalo de diferença entre duas datas (representado por um DateInterval).
    • format - Formata uma data com os mesmos elementos da função date.
    • getTimestamp - Obtém o timestamp (para compatibilidade com as funções antigas).
    • modify - Realiza operações, assim como strtotime.
    • setDate, setTime, setTimestamp, setTimezone - Modifica a data, hora, timestamp ou TimeZone do objeto.
    • add e sub - Adiciona ou subtrai um intervalo de tempo através de um DateInterval.
  • DateInterval
    • __construct - Construtor do intervalo.
    • format - Formata o intervalo.

Vamos tentar fazer os mesmos exemplos feitos com funções:

$data = '01/01/2010';

// Criar o objeto representando a data
$obj_data = DateTime::createFromFormat('d/m/Y', $data);

// Definir a hora, minuto e segundo, que não foram informados
// (caso contrário, é obtido os valores da hora atual)
$obj_data->setTime(0, 0, 0);

// Realizar a soma de 40 dias
$obj_data->modify('+40 days');

// Formatar a data obtida
echo $obj_data->format('d/m/Y');

Bem mais simples, não?

Agora vamos fazer o mesmo exemplo, mas utilizando a classe DateInterval ao invés do método modify:

$data = '01/01/2010';

// Criar o objeto representando a data
$obj_data = DateTime::createFromFormat('d/m/Y', $data);
$obj_data->setTime(0, 0, 0);

// Realizar a soma de 40 dias
$intervalo = new DateInterval('P40D');
$obj_data->add($intervalo);

// Formatar a data obtida
echo $obj_data->format('d/m/Y');

Para criar um intervalo, é utilizada uma string que começa com a letra P, seguida por números inteiros acompanhados de sua respectiva unidade (Y para anos, M para meses, D para dias e W para semanas). Caso o intervalo especifique horas, deve ser seguido pela letra T, que é seguido por números inteiros acompanhados de sua respectiva unidade (H para horas, M para minutos e S para segundos).

Exemplos de strings para intervalos (note em vermelho a porção de data e em azul a porção de tempo):

  • P1Y - Um ano
  • P2Y3D - Dois anos e três dias
  • P3MT4M - Três meses e quatro minutos
  • PT4S - Quatro segundos
  • P1Y2M3DT4H5M6S - Um ano, dois meses, três dias, quatro horas, cinco minutos e seis segundos

Observação: de maneira análoga a mktime, as funções setDate e setTime também trabalham com aritmética de datas. Veja o exemplo:

$data = '01/01/2010';

// Criar o objeto representando a data
$obj_data = DateTime::createFromFormat('d/m/Y', $data);
$obj_data->setTime(0, 0, 0);

// Realizar a soma de 40 dias
$dia = $obj_data->format('d');
$mes = $obj_data->format('m'); 
$ano = $obj_data->format('Y');
$obj_data->setDate($ano, $mes, $dia + 40);

// Formatar a data obtida
echo $obj_data->format('d/m/Y H:i:s');

Porém, esta última forma não é muito indicada, já que existem formas mais adequadas.

Desde o PHP 5.2.2, é possível utilizar os operadores de comparação (==, !=, <, <=, >, >=) para comparar duas datas. Antes dessa versão, a forma mais útil de comparar duas datas era usando o método getTimestamp e comparar dois inteiros. Outra forma de se obter uma comparação é fazendo a diferença (método diff) entre duas datas, que retorna um intervalo.

A classe DateTimeZone é utilizada para representar o fuso horário de um DateTime. Já a classe DatePeriod é apenas uma forma conveniente de representar um período (data de início e data de término, ou data de início mais um intervalo). DataPeriod é útil para ser usado em estruturas foreach, já que é "atravessável".

5 comentários

Anônimo disse...

como utilizar uma variável ao invés de numero 40 em $intervalo = new DateInterval('P40D');

Rubens Takiguti Ribeiro (autor do blog) disse...

Anônimo, pela sua pergunta, você precisa entender como funcionam as strings no PHP.
Você pode usar aspas duplas ou usar a função sprintf:
sprintf('P%dD', $dias)

Veja esses links:
http://php.net/manual/en/language.types.string.php
http://php.net/manual/en/function.sprintf.php

Anônimo disse...

Grato pelo retorno, mas não entendi. Eu queria colocar uma variavel nessa linha $intervalo = new DateInterval('P$VARIAVELD'); Não está dando certo nem com aspas duplas

Rubens Takiguti Ribeiro (autor do blog) disse...

Anônimo, como existe uma letra depois do nome da variável, você precisa delimitá-la com chaves assim:
"P{$variavel}D", caso contrário, o PHP iria procurar pela variável chamada $variavelD, que não existe.