Executando operações aritméticas com datas e horários

Embora as estruturas DateTime e DateTimeOffset forneçam membros que realizam operações aritméticas em seus valores, os resultados das operações aritméticas são muito diferentes. Este artigo examina tais diferenças, as relaciona a graus de reconhecimento de fuso horário em dados de data e hora e discute como executar totalmente operações de reconhecimento de fuso horário usando dados de data e hora.

Comparações e operações aritméticas com valores DateTime

A propriedade DateTime.Kind permite que um valor DateTimeKind seja atribuído à data e à hora para indicar se elas representam a hora local, a UTC (Hora Universal Coordenada) ou a hora em um fuso horário não especificado. No entanto, essas informações limitadas de fuso horário são ignoradas ao comparar ou executar a aritmética de data e hora em valores DateTimeKind. O exemplo a seguir, que compara a hora local atual com a hora UTC atual, ilustra como as informações de fuso horário são ignoradas.

using System;

public enum TimeComparison
{
   EarlierThan = -1,
   TheSameAs = 0,
   LaterThan = 1
}

public class DateManipulation
{
   public static void Main()
   {
      DateTime localTime = DateTime.Now;
      DateTime utcTime = DateTime.UtcNow;

      Console.WriteLine("Difference between {0} and {1} time: {2}:{3} hours",
                        localTime.Kind,
                        utcTime.Kind,
                        (localTime - utcTime).Hours,
                        (localTime - utcTime).Minutes);
      Console.WriteLine("The {0} time is {1} the {2} time.",
                        localTime.Kind,
                        Enum.GetName(typeof(TimeComparison), localTime.CompareTo(utcTime)),
                        utcTime.Kind);
   }
}
// If run in the U.S. Pacific Standard Time zone, the example displays
// the following output to the console:
//    Difference between Local and Utc time: -7:0 hours
//    The Local time is EarlierThan the Utc time.
Public Enum TimeComparison As Integer
    EarlierThan = -1
    TheSameAs = 0
    LaterThan = 1
End Enum

Module DateManipulation
    Public Sub Main()
        Dim localTime As Date = Date.Now
        Dim utcTime As Date = Date.UtcNow

        Console.WriteLine("Difference between {0} and {1} time: {2}:{3} hours", _
                          localTime.Kind.ToString(), _
                          utcTime.Kind.ToString(), _
                          (localTime - utcTime).Hours, _
                          (localTime - utcTime).Minutes)
        Console.WriteLine("The {0} time is {1} the {2} time.", _
                          localTime.Kind.ToString(), _
                          [Enum].GetName(GetType(TimeComparison), localTime.CompareTo(utcTime)), _
                          utcTime.Kind.ToString())
        ' If run in the U.S. Pacific Standard Time zone, the example displays 
        ' the following output to the console:
        '    Difference between Local and Utc time: -7:0 hours
        '    The Local time is EarlierThan the Utc time.                                                    
    End Sub
End Module

O método CompareTo(DateTime) relata que a hora local é anterior (ou menor) que a hora UTC e a operação de subtração indica que a diferença entre a hora UTC e a hora local para um sistema no fuso horário padrão do Pacífico dos EUA é de sete horas. Porém, como esses dois valores oferecem representações diferentes de um único momento, fica claro que, neste caso, o intervalo de tempo é completamente atribuível ao deslocamento do fuso horário local em relação à hora UTC.

Normalmente, a propriedade DateTime.Kind não afeta os resultados retornado pelos métodos aritméticos e de comparação Kind (como indica a comparação de dois momentos idênticos), embora possa afetar a interpretação dos resultados. Por exemplo:

  • O resultado de qualquer operação aritmética executada em dois valores de data e hora com as propriedades DateTime.Kind equivalentes a DateTimeKind reflete o intervalo de tempo real entre os dois valores. Da mesma forma, a comparação dos dois valores de data e hora reflete com precisão a relação entre horários.

  • O resultado de qualquer operação aritmética ou de comparação realizada em dois valores de data e hora com as propriedades DateTime.Kind iguais a DateTimeKind ou em dois valores de data e hora com valores de propriedade DateTime.Kind diferentes reflete a diferença na hora do relógio entre os dois valores.

  • As operações aritméticas ou de comparação em valores de data e hora local não consideram se um valor específico é ambíguo ou inválido nem levam em conta o efeito de regras de ajuste que resultam da transição do fuso horário local de ou para o horário de verão.

  • Qualquer operação que compara ou calcula a diferença entre o UTC e o horário local inclui um intervalo de tempo igual ao deslocamento do fuso horário local em relação ao UTC no resultado.

  • Qualquer operação que compara ou calcula a diferença entre um horário não especificado e o UTC ou o horário local reflete a hora do relógio simples. As diferenças de fuso horário não são consideradas; o resultado não reflete a aplicação das regras de ajuste de fuso horário.

  • Qualquer operação que compara ou calcula a diferença entre dois horários não especificados poderá incluir um intervalo desconhecido que reflete a diferença entre o horário em dois fusos horários diferentes.

Há muitos cenários em que as diferenças de fuso horário não afetam os cálculos de data e hora (para uma discussão de alguns desses cenários, veja Escolher entre DateTime, DateTimeOffset, TimeSpan e TimeZoneInfo) ou em que o contexto dos dados de data e hora define o significado de comparação ou operações aritméticas.

Comparações e operações aritméticas com valores DateTimeOffset

Um valor DateTimeOffset inclui não somente uma data e hora, mas também um deslocamento que define inequivocamente essa data e hora em relação à hora UTC. Este deslocamento torna possível definir a igualdade de maneira diferente dos valores DateTime. Enquanto os valores DateTime são iguais quando têm o mesmo valor de data e hora, os valores DateTimeOffset são iguais quando ambos se referem ao mesmo momento. Quando usado em comparações e na maioria das operações aritméticas que determinam o intervalo entre duas datas e horas, um valor DateTimeOffset é mais preciso e objetivo. O exemplo a seguir, que é o DateTimeOffset equivalente ao exemplo anterior que comparou valores DateTimeOffset locais e UTC, ilustra essa diferença de comportamento.

using System;

public enum TimeComparison
{
   EarlierThan = -1,
   TheSameAs = 0,
   LaterThan = 1
}

public class DateTimeOffsetManipulation
{
   public static void Main()
   {
      DateTimeOffset localTime = DateTimeOffset.Now;
      DateTimeOffset utcTime = DateTimeOffset.UtcNow;

      Console.WriteLine("Difference between local time and UTC: {0}:{1:D2} hours",
                        (localTime - utcTime).Hours,
                        (localTime - utcTime).Minutes);
      Console.WriteLine("The local time is {0} UTC.",
                        Enum.GetName(typeof(TimeComparison), localTime.CompareTo(utcTime)));
   }
}
// Regardless of the local time zone, the example displays
// the following output to the console:
//    Difference between local time and UTC: 0:00 hours.
//    The local time is TheSameAs UTC.
Public Enum TimeComparison As Integer
    EarlierThan = -1
    TheSameAs = 0
    LaterThan = 1
End Enum

Module DateTimeOffsetManipulation
    Public Sub Main()
        Dim localTime As DateTimeOffset = DateTimeOffset.Now
        Dim utcTime As DateTimeOffset = DateTimeOffset.UtcNow

        Console.WriteLine("Difference between local time and UTC: {0}:{1:D2} hours.", _
                          (localTime - utcTime).Hours, _
                          (localTime - utcTime).Minutes)
        Console.WriteLine("The local time is {0} UTC.", _
                          [Enum].GetName(GetType(TimeComparison), localTime.CompareTo(utcTime)))
    End Sub
End Module
' Regardless of the local time zone, the example displays 
' the following output to the console:
'    Difference between local time and UTC: 0:00 hours.
'    The local time is TheSameAs UTC.
'          Console.WriteLine(e.GetType().Name)

Neste exemplo, o método CompareTo indica que a hora local atual e a hora UTC atual são iguais, e a subtração de valores CompareTo(DateTimeOffset) indica que a diferença entre as duas horas é TimeSpan.Zero.

A principal limitação do uso de valores DateTimeOffset na aritmética de data e hora é que, embora os valores DateTimeOffset tenham algum reconhecimento de fuso horário, eles não são totalmente conscientes do fuso horário. Embora o deslocamento do valor DateTimeOffset reflita o deslocamento de um fuso horário de UTC quando uma variável DateTimeOffset recebe um valor pela primeira vez, depois disso, ele fica desassociado do fuso horário. Como não estão mais diretamente associadas a um horário identificável, a adição e a subtração dos intervalos de data e hora não consideram as regras de ajuste de um fuso horário.

Para ilustrar, a transição para o horário de verão no fuso horário padrão central dos EUA ocorre às 2h em 9 de março de 2008. Com isso em mente, adicionar um intervalo de duas horas e meia a um horário padrão central de 1h30 em 9 de março de 2008 deve resultar em uma data/hora às 5h em 9 de março de 2008. No entanto, como mostra o exemplo a seguir, o resultado da adição é 4h em 9 de março de 2008. O resultado dessa operação representa o momento correto, embora não seja o horário no fuso horário em que estamos interessados (ou seja, não tem o deslocamento de fuso horário esperado).

using System;

public class IntervalArithmetic
{
   public static void Main()
   {
      DateTime generalTime = new DateTime(2008, 3, 9, 1, 30, 0);
      const string tzName = "Central Standard Time";
      TimeSpan twoAndAHalfHours = new TimeSpan(2, 30, 0);

      // Instantiate DateTimeOffset value to have correct CST offset
      try
      {
         DateTimeOffset centralTime1 = new DateTimeOffset(generalTime,
                    TimeZoneInfo.FindSystemTimeZoneById(tzName).GetUtcOffset(generalTime));

         // Add two and a half hours
         DateTimeOffset centralTime2 = centralTime1.Add(twoAndAHalfHours);
         // Display result
         Console.WriteLine("{0} + {1} hours = {2}", centralTime1,
                                                    twoAndAHalfHours.ToString(),
                                                    centralTime2);
      }
      catch (TimeZoneNotFoundException)
      {
         Console.WriteLine("Unable to retrieve Central Standard Time zone information.");
      }
   }
}
// The example displays the following output to the console:
//    3/9/2008 1:30:00 AM -06:00 + 02:30:00 hours = 3/9/2008 4:00:00 AM -06:00
Module IntervalArithmetic
    Public Sub Main()
        Dim generalTime As Date = #03/09/2008 1:30AM#
        Const tzName As String = "Central Standard Time"
        Dim twoAndAHalfHours As New TimeSpan(2, 30, 0)

        ' Instantiate DateTimeOffset value to have correct CST offset
        Try
            Dim centralTime1 As New DateTimeOffset(generalTime, _
                       TimeZoneInfo.FindSystemTimeZoneById(tzName).GetUtcOffset(generalTime))

            ' Add two and a half hours      
            Dim centralTime2 As DateTimeOffset = centralTime1.Add(twoAndAHalfHours)
            ' Display result
            Console.WriteLine("{0} + {1} hours = {2}", centralTime1, _
                                                       twoAndAHalfHours.ToString(), _
                                                       centralTime2)
        Catch e As TimeZoneNotFoundException
            Console.WriteLine("Unable to retrieve Central Standard Time zone information.")
        End Try
    End Sub
End Module
' The example displays the following output to the console:
'    3/9/2008 1:30:00 AM -06:00 + 02:30:00 hours = 3/9/2008 4:00:00 AM -06:00

Operações aritméticas com horas em fusos horários

A classe TimeZoneInfo inclui métodos de conversão que aplicam ajustes automaticamente ao converter horários de um fuso horário para outro. Esses métodos de conversão incluem:

Para saber mais, veja Convertendo horários entre fusos horários.

A classe TimeZoneInfo não fornece nenhum método que aplique regras de ajuste automaticamente ao executar a aritmética de data e hora. No entanto, é possível aplicar regras de ajuste convertendo a hora em um fuso horário para UTC, executando a operação aritmética e, em seguida, convertendo de UTC novamente para a hora no fuso horário. Para saber mais, veja Como usar fusos horários na aritmética de data e hora.

Por exemplo, o código a seguir é semelhante ao código anterior que adicionou duas horas e meia às 2h em 9 de março de 2008. No entanto, como converte um horário padrão da região central em UTC antes de realizar a aritmética de data e hora e, depois, converte o resultado do UTC novamente no horário padrão da região central, o horário resultante reflete a transição do fuso horário padrão da região central para o horário de verão.

using System;

public class TimeZoneAwareArithmetic
{
   public static void Main()
   {
      const string tzName = "Central Standard Time";

      DateTime generalTime = new DateTime(2008, 3, 9, 1, 30, 0);
      TimeZoneInfo cst = TimeZoneInfo.FindSystemTimeZoneById(tzName);
      TimeSpan twoAndAHalfHours = new TimeSpan(2, 30, 0);

      // Instantiate DateTimeOffset value to have correct CST offset
      try
      {
         DateTimeOffset centralTime1 = new DateTimeOffset(generalTime,
                                       cst.GetUtcOffset(generalTime));

         // Add two and a half hours
         DateTimeOffset utcTime = centralTime1.ToUniversalTime();
         utcTime += twoAndAHalfHours;

         DateTimeOffset centralTime2 = TimeZoneInfo.ConvertTime(utcTime, cst);
         // Display result
         Console.WriteLine("{0} + {1} hours = {2}", centralTime1,
                                                    twoAndAHalfHours.ToString(),
                                                    centralTime2);
      }
      catch (TimeZoneNotFoundException)
      {
         Console.WriteLine("Unable to retrieve Central Standard Time zone information.");
      }
   }
}
// The example displays the following output to the console:
//    3/9/2008 1:30:00 AM -06:00 + 02:30:00 hours = 3/9/2008 5:00:00 AM -05:00
Module TimeZoneAwareArithmetic
    Public Sub Main()
        Const tzName As String = "Central Standard Time"

        Dim generalTime As Date = #03/09/2008 1:30AM#
        Dim cst As TimeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(tzName)
        Dim twoAndAHalfHours As New TimeSpan(2, 30, 0)

        ' Instantiate DateTimeOffset value to have correct CST offset
        Try
            Dim centralTime1 As New DateTimeOffset(generalTime, _
                       cst.GetUtcOffset(generalTime))

            ' Add two and a half hours 
            Dim utcTime As DateTimeOffset = centralTime1.ToUniversalTime()
            utcTime += twoAndAHalfHours

            Dim centralTime2 As DateTimeOffset = TimeZoneInfo.ConvertTime(utcTime, cst)
            ' Display result
            Console.WriteLine("{0} + {1} hours = {2}", centralTime1, _
                                                       twoAndAHalfHours.ToString(), _
                                                       centralTime2)
        Catch e As TimeZoneNotFoundException
            Console.WriteLine("Unable to retrieve Central Standard Time zone information.")
        End Try
    End Sub
End Module
' The example displays the following output to the console:
'    3/9/2008 1:30:00 AM -06:00 + 02:30:00 hours = 3/9/2008 5:00:00 AM -05:00

Confira também