PHP Criando DateTime com Construtor sem Máscaras de Formatação

$string_date = '2010-08-07 08:39:00';
[$y, $m, $d] = [null, null, null];

switch(true) {
  case preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $string_date, $fragments):
    list($y, $m, $d) = [$fragments[1], $fragments[2], $fragments[3]];
    break;
  case preg_match('/^(\d{4})\/(\d{2})\/(\d{2})/', $string_date, $fragments):
    list($y, $m, $d) = [$fragments[1], $fragments[2], $fragments[3]];
    break;
  case preg_match('/^(\d{2})-(\d{2})-(\d{4})/', $string_date, $fragments):
    list($d, $m, $y) = [$fragments[1], $fragments[2], $fragments[3]];
    break;
  case preg_match('/^(\d{2})\/(\d{2})\/(\d{4})/', $string_date, $fragments):
    list($m, $d, $y) = [$fragments[1], $fragments[2], $fragments[3]];
    break;
  default:
    throw new Exception('String Date supplied with unknown format');
}

if (!checkdate($m, $d, $y)) {
  throw new Exception ('String Date supplied with invalid date content');
}

try {
  $object_datetime = new DateTime($string_date);
} catch (\Exception $exception) {
  echo 'String Date supplied impossible to convert to Date';
  $object_datetime = null;
}

null !== $object_date
  ? $object_date->setTime(0,0)
  ; null;

var_dump($object_datetime);

Pessoal, este código surgiu da necessidade de se converter um campo de data fornecido como string numa integração entre sistemas.

O fato é que quando se depende de informações externas o processo pode facilmente quebrar quando as regras estabelecidas não forem estritamente respeitadas.

No meu caso um campo que deveria ser enviado no formato date Y-m-d acabou sendo enviado num formato DateTime Y-m-d H:i:s. O problema se agravou pelo fato de que apenas um dos fornecedores acabou enviando desta forma e os demais fornecedores enviavam a informação no formato acordado.

Dúvida cruel… solicitar ao fornecedor para corrigir o envio da informação? Ou flexibilizar a entrada do dados de forma inteligente?

Opção dois na veia! O fato é que todos os fornecedores continuavam enviando uma data válida pra nós, só que com formatos ou máscaras diferentes, mas ainda assim eram datas válidas.

Baseado neste preceito a solução foi substituir a criação do objeto DateTime calcada no createFromFormat a partir de máscaras previamente conhecidas por simplesmente passar a string date recebida para o construtor do DateTime.

// stop creating date based on known format, this will fail when datetime content is sent
$object_datetime = DateTime::createFromFormat('Y-m-d', $string_date);

// start creating datetime based on constructor, this will work with any valid date/time content
$object_datetime = new DateTime($string_date);

A grande sacada consiste em validar se uma data válida foi enviada, no caso fazemos esta validação para a porção Y-m-d somente. Se a validação passar neste nível a probabilidade da informação ser uma data válida inclusive na sua porção hora será muito grande.

Para exemplificar a flexibilidade de entrada desta informação, segue abaixo alguns exemplos de formatos válidos que irão funcionar com o construtor.

$object_datetime = new DateTime('2010-08');
var_dump($object_datetime); //2010-08-01 00:00:00.000000

$object_datetime = new DateTime('2010-08 08:39');
var_dump($object_datetime); //2010-08-01 08:39:00.000000

$object_datetime = new DateTime('2010-08 08:39:53');
var_dump($object_datetime); //2010-08-01 08:39:53.000000

$object_datetime = new DateTime('2010-08T08:39:53Z');
var_dump($object_datetime); //2010-08-01 08:39:53.000000

$object_datetime = new DateTime('2010-08-07');
var_dump($object_datetime); //2010-08-07 00:00:00.000000

$object_datetime = new DateTime('2010-08-07 08:39');
var_dump($object_datetime); //2010-08-07 08:39:00.000000

$object_datetime = new DateTime('2010-08-07 08:39:53');
var_dump($object_datetime); //2010-08-07 08:39:53.000000

$object_datetime = new DateTime('2010-08-07T08:39:53Z');
var_dump($object_datetime); //2010-08-07 08:39:53.000000

Considere ainda que podemos receber barras ao invés de hífen como separador que irá funcionar corretamente com todas as variações acima apresentadas.

$object_datetime = new DateTime('2010/08/07 08:39:53');
var_dump($object_datetime); //2010-08-07 08:39:53.000000

$object_datetime = new DateTime('2010/08/07T08:39:53Z');
var_dump($object_datetime); //2010-08-07 08:39:53.000000

Outra grande vantagem de se utilizar o construtor do DateTime é que ele vai funcionar com o formato nativo americano m/d/Y quando separado por barras e com o formato nativo britânico m-d-Y quando separado por hífens.

$object_datetime = new DateTime('08/07/2010 08:39:53');
var_dump($object_datetime); //2010-08-07 08:39:53.000000

$object_datetime = new DateTime('07-08-2010 08:39:53');
var_dump($object_datetime); //2010-08-07 08:39:53.000000

Lembrando que em nenhum momento tivemos que informar máscaras de formatação para efetivamente criar o objeto DateTime. Simplesmente mandamos a string date para o construtor e pronto.

E caso a informação final deva ser uma data sem a porção hora, simplesmente zeramos esta porção depois do objeto criado, assim:

$object_datetime = new DateTime('08/07/2010 08:39:53');
var_dump($object_datetime); //2010-08-07 08:39:53.000000

$object_datetime->setTime(0,0);
var_dump($object_datetime); //2010-08-07 00:00:00.000000

Depois desta demonstração de flexibilidade de formatos acredito que as vantagens de criação do DateTime utilizando o construtor tenham ficado claras.

E é claro que o código do início do artigo merece um refactoring para ser mais CleanCode e também para suportar os demais formatos aceitos pelo construtor do DateTime, mas para fins didáticos preferi mantê-lo mais simples mesmo.

E agora? Troca ou não troca o DateTime::createFromFormat pelo new DateTime constructor? Você decide…

Happy code!

PHP DateTime Diff Calculando diferença de dias entre datas

function calculateDiffInDays(string $date1, string $date2) {
  $datetime1 = new \DateTime($date1);
  $datetime2 = new \DateTime($date2);
  
  return $datetime1->diff($datetime2)->days;
}

echo calculateDiffInDays('1970-08-25', '2020-12-30');
// 18390
echo calculateDiffInDays('2020-12-30', '1970-08-25');
// 18390

Recentemente precisei ajustar um processo que já fazia um cálculo de diferença de dias entre datas e me deparei com um código bem similar a este, só que não funcionava.

Daí fui pesquisar a boa e velha documentação do PHP e encontrei a falha.

Primeiro vamos esclarecer que o DateTime::diff retornar um DateInterval o qual então pode ser formatado de várias formas… https://www.php.net/manual/en/class.dateinterval.php

Observe que days representa o total de número de dias entre a primeira e segunda datas e isso significa dizer que o resultado será um número inteiro.

Enquanto isso d representa simplesmente um possível dia, ou seja, será um número entre 0 e 30 pois o cálculo é feito levando em conta apenas a porção dia. Assim a hipótese para o maior resultado seria o dia 31 menos o dia 01 sendo o resultado 30.

Então, o código original com o qual me deparei formatava o resultado usando a tag ->d quando na verdade deveria estar utilizando ->days para obter a diferença total de dias.

return $datetime1->diff($datetime2)->d;

Confesso que apenas lendo a documentação fica difícil de ter certeza do resultado assim de imediato. Como sempre com PHP um teste rápido é o seu melhor aliado.

Mas todos nós sabemos que o PHP faz o que é preciso fazer ou faz o que a gente quer que ele faça, não importa.

O que algumas pessoas costumam usar para criticar o PHP, na verdade é o que eu mais gosto nele!

PHP Criando DateTime sem a porção horas

$datetime_without_hours = \DateTime::createFromFormat('!Y-m-d', '1970-08-25');
// 1970-08-25 00:00:00.000000

$datetime_with_hours = \DateTime::createFromFormat('Y-m-d', '1970-08-25');
// 1970-08-25 05:07:32.000000

$datetime_with_hours->setTime(0,0);
// 1970-08-25 00:00:00.000000

Esta semana mesmo eu estive trabalhando num projeto onde tive que incorporar diversos campos de data num integração de sistemas com PHP.

Decidi que enviar os campos com o tipo DateTime para garantir que o aplicativo que recebia a informação pudesse utilizar as datas de forma segura, mas acabei me deparando com a questão de introspecção de horas para uma informação genuinamente Date.

$datetime_with_hours = \DateTime::createFromFormat('Y-m-d', '1970-08-25');
// 1970-08-26 05:07:32.000000

Na primeira tentativa utilizei a máscara Y-m-d simples e verifiquei que a porção Horas:Minutos:Segundos foi inserida na composição da data. O PHP utilizou o Time corrente do Server e incorporou na data.

Depois de uma rápida pesquisa na internet identifiquei que este é o comportado esperado da função. Para evitar a porção horas na composição da data podemos fazer o uso da exclamação na máscara durante a criação do DateTime.

Mas atenção, a inclusão da exclamação na máscara !Y-m-d fará com que apenas um reset da data e hora corrente seja executado antes do datetime ser criado. No entanto, todos os elementos explicitamente definidos na máscara serão tomados em consideração no momento da criação do objeto datetime.

$datetime_with_hours = \DateTime::createFromFormat('!Y-m-d H:i:s', '1970-08-25 13:14:15');
// 1970-08-25 13:14:15.000000

E caso você já possua um objeto que tenha sido criado com a porção horas na composição, você pode resetar este conteúdo para algo qualquer inclusive a nossa desejada hora ZERO, conforme demonstrado abaixo.

$datetime_with_hours = \DateTime::createFromFormat('Y-m-d', '1970-08-25');
// 1970-08-25 05:07:32.000000

$datetime_with_hours->setTime(0,0);
// 1970-08-25 00:00:00.000000

Também, se a data a ser criada seja algo próxima da data corrente você pode utilizar parâmetros no construtor do objeto DateTime para criar o objeto já sem a porção horas.

$datetime_without_hours = new \DateTime("midnight");
// 2021-11-13 00:00:00.000000

$datetime_without_hours = new \DateTime("today");
// 2021-11-13 00:00:00.000000

$datetime_without_hours = new \DateTime("yesterday");
// 2021-11-12 00:00:00.000000

$datetime_without_hours = new \DateTime("tomorrow");
// 2021-11-14 00:00:00.000000

E aí? Curtiu estes recursos para criação de datas?