Criando um DateTime em um fuso horário específico em c #

162

Estou tentando criar um teste de unidade para testar o caso de quando o fuso horário muda em uma máquina porque foi definido incorretamente e depois corrigido.

No teste, preciso criar objetos DateTime em um fuso horário local nenhum para garantir que as pessoas que executam o teste possam fazê-lo com êxito, independentemente de onde estejam localizadas.

Pelo que posso ver no construtor DateTime, posso definir o Fuso Horário como o fuso horário local, o fuso horário UTC ou não especificado.

Como crio um DateTime com um fuso horário específico como o PST?

Jack Hughes
fonte
Pergunta relacionada - stackoverflow.com/questions/2532729/…
Oded

Respostas:

216

A resposta de Jon fala sobre o TimeZone , mas eu sugiro usar o TimeZoneInfo .

Pessoalmente, gosto de manter as coisas no UTC sempre que possível (pelo menos no passado; armazenar o UTC para o futuro tem possíveis problemas ), então sugiro uma estrutura como esta:

public struct DateTimeWithZone
{
    private readonly DateTime utcDateTime;
    private readonly TimeZoneInfo timeZone;

    public DateTimeWithZone(DateTime dateTime, TimeZoneInfo timeZone)
    {
        var dateTimeUnspec = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified);
        utcDateTime = TimeZoneInfo.ConvertTimeToUtc(dateTimeUnspec, timeZone); 
        this.timeZone = timeZone;
    }

    public DateTime UniversalTime { get { return utcDateTime; } }

    public TimeZoneInfo TimeZone { get { return timeZone; } }

    public DateTime LocalTime
    { 
        get 
        { 
            return TimeZoneInfo.ConvertTime(utcDateTime, timeZone); 
        }
    }        
}

Você pode alterar os nomes "TimeZone" para "TimeZoneInfo" para tornar as coisas mais claras - prefiro os nomes mais breves.

Jon Skeet
fonte
5
Não conheço nenhuma construção equivalente do SQL Server, receio. Eu sugeriria ter o nome do fuso horário como uma coluna e o valor UTC em outra coluna. Busque-os separadamente e, em seguida, você poderá criar instâncias com bastante facilidade.
31909 Jon Skeet
2
Não tenho certeza sobre o uso esperado do construtor que usa DateTime e TimeZoneInfo, mas, como você está chamando o método dateTime.ToUniversalTime (), suspeito que você esteja imaginando que "talvez" esteja na hora local. Nesse caso, acho que você realmente deveria usar o TimeZoneInfo transmitido para convertê-lo em UTC, pois eles estão dizendo que deveria estar nesse fuso horário.
IDisposable
2
@ ChrisMoschini: Nesse ponto, você está apenas inventando seu próprio esquema de identificação - um esquema que ninguém mais usa no mundo. Vou continuar com a zona padrão da indústria, obrigado. (É difícil ver como "Europa / Londres" não tem sentido, por exemplo.)
Jon Skeet
2
@ ChrisMoschini: Exemplo diferente então: CST. Isso é UTC-5 ou UTC-6? E o IST - Israel, Índia ou Irlanda estão no seu banco de dados? (E mesmo que você saiba o deslocamento no momento, diferentes países que observam a mesma abreviação podem mudar em momentos diferentes. Portanto, ainda existe ambiguidade sobre qual fuso horário real significa. Fuso horário! = Deslocamento.) Voltando ao seu caso: você reivindica que usar abreviações melhor resolveu seu problema. Como o uso dos IDs de fuso horário padrão do setor teria sido pior?
Jon Skeet
6
@ ChrisMoschini: Bem, continuarei recomendando o uso de IDs de informações de zona inequívocas, padrão da indústria, em vez das abreviações ambíguas. Não se trata de cuja biblioteca é preferida - a autoria da biblioteca realmente não é um problema. Se alguém desejar usar outra biblioteca com uma boa escolha de identificador, tudo bem. A escolha do identificador para um fuso horário é importante, e acho que é muito importante que os leitores estejam cientes de que as abreviações são ambíguas, como mostrei no exemplo do IST.
21813 Jon Skeet
54

A estrutura DateTimeOffset foi criada para exatamente esse tipo de uso.

Vejo: http://msdn.microsoft.com/en-us/library/system.datetimeoffset.aspx

Aqui está um exemplo de criação de um objeto DateTimeOffset com um fuso horário específico:

DateTimeOffset do1 = new DateTimeOffset(2008, 8, 22, 1, 0, 0, new TimeSpan(-5, 0, 0));

CleverPatrick
fonte
1
Obrigado, esta é uma boa maneira de conseguir isso. Depois de obter o objeto DateTimeOffset no fuso horário correto, você pode usar a propriedade .UtcDateTime para obter um horário UTC para o que você criou. Se você armazenar as datas em UTC, em seguida, convertendo-os para a hora local de cada usuário não é grande coisa :)
Redth
2
Eu não acho que isso lida com o horário de verão corretamente, pois alguns fusos horários o respeitam, enquanto outros não. Também "no dia" o horário de verão começa / termina, partes desse dia estariam desativadas.
crokusek
14
Lição. DST é uma regra de um fuso horário específico. DateTimeOffset não está não, não está associado a nenhum fuso horário. Não confunda um valor de deslocamento UTC, como -5, com um fuso horário. Não é um fuso horário, é um deslocamento. O mesmo deslocamento é geralmente compartilhado por muitos fusos horários, portanto, é uma maneira ambígua de se referir a um fuso horário. Como DateTimeOffset está associado a um deslocamento, não a um fuso horário, não é possível aplicar regras de horário de verão. Assim, as 3h serão 3h em todos os dias do ano, sem exceção em uma estrutura DateTimeOffset (por exemplo, nas propriedades Hours e TimeOfDay).
Triynko 18/02
Onde você pode ficar confuso é se você olhar para a propriedade LocalDateTime do DateTimeOffset. Essa propriedade NÃO é um DateTimeOffset, é uma instância de DateTime cujo tipo é DateTimeKind.Local. Essa instância está associada a um fuso horário ... qualquer que seja o fuso horário do sistema local. Essa propriedade refletirá o horário de verão.
Triynko 18/02
4
Portanto, o verdadeiro problema do DateTimeOffset é que ele não inclui informações suficientes. Inclui um deslocamento, não um fuso horário. O deslocamento é ambíguo com vários fusos horários.
Triynko 18/02
41

As outras respostas aqui são úteis, mas não abordam como acessar o Pacífico especificamente - aqui está:

public static DateTime GmtToPacific(DateTime dateTime)
{
    return TimeZoneInfo.ConvertTimeFromUtc(dateTime,
        TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"));
}

Curiosamente, embora "Horário padrão do Pacífico" normalmente signifique algo diferente de "Horário de verão do Pacífico", neste caso, refere-se ao horário do Pacífico em geral. De fato, se você FindSystemTimeZoneByIdbuscá-lo, uma das propriedades disponíveis é um booleano informando se esse fuso horário está atualmente no horário de verão ou não.

Você pode ver exemplos mais generalizados disso em uma biblioteca que acabei lançando para lidar com o DateTimes de que preciso em diferentes fusos horários, com base no local de solicitação do usuário, etc:

https://github.com/b9chris/TimeZoneInfoLib.Net

Isso não funcionará fora do Windows (por exemplo, Mono no Linux), pois a lista de horários vem do Registro do Windows: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\

Abaixo, você encontrará chaves (ícones de pastas no Editor do Registro); os nomes dessas chaves são para os quais você passa FindSystemTimeZoneById. No Linux, você precisa usar um conjunto separado de definições de fuso horário, padrão do Linux, que não explorei adequadamente.

Chris Moschini
fonte
1
Além disso, há ConvertTimeBySystemTimeZoneId () ex: TimeZoneInfo.ConvertTimeBySystemTimeZoneId (DateTime.UtcNow, "Central Standard Time")
Brent
No Windows, a Lista de identificação do fuso horário também pode ver esta resposta: stackoverflow.com/a/24460750/4573839
yu yang Jian
7

Eu alterei Jon Skeet responder a um pouco para a web com método de extensão. Também funciona no azul como um encanto.

public static class DateTimeWithZone
{

private static readonly TimeZoneInfo timeZone;

static DateTimeWithZone()
{
//I added web.config <add key="CurrentTimeZoneId" value="Central Europe Standard Time" />
//You can add value directly into function.
    timeZone = TimeZoneInfo.FindSystemTimeZoneById(ConfigurationManager.AppSettings["CurrentTimeZoneId"]);
}


public static DateTime LocalTime(this DateTime t)
{
     return TimeZoneInfo.ConvertTime(t, timeZone);   
}
}
Jernej Novak
fonte
2

Você precisará criar um objeto personalizado para isso. Seu objeto personalizado conterá dois valores:

Não tenho certeza se já existe um tipo de dados fornecido pelo CLR, mas pelo menos o componente TimeZone já está disponível.

Jon Limjap
fonte
2

Gosto da resposta de Jon Skeet, mas gostaria de acrescentar uma coisa. Não tenho certeza se Jon esperava que o ctor sempre fosse passado no fuso horário local. Mas eu quero usá-lo para casos em que é algo diferente de local.

Estou lendo valores de um banco de dados e sei em que fuso horário esse banco de dados está. Portanto, no ctor, passo o fuso horário do banco de dados. Mas então eu gostaria do valor na hora local. O LocalTime de Jon não retorna a data original convertida em uma data de fuso horário local. Ele retorna a data convertida no fuso horário original (o que você tiver passado para o ctor).

Acho que esses nomes de propriedades esclarecem ...

public DateTime TimeInOriginalZone { get { return TimeZoneInfo.ConvertTime(utcDateTime, timeZone); } }
public DateTime TimeInLocalZone    { get { return TimeZoneInfo.ConvertTime(utcDateTime, TimeZoneInfo.Local); } }
public DateTime TimeInSpecificZone(TimeZoneInfo tz)
{
    return TimeZoneInfo.ConvertTime(utcDateTime, tz);
}
Gabe Halsmer
fonte
0

O uso da classe TimeZones facilita a criação de uma data específica para o fuso horário.

TimeZoneInfo.ConvertTime(DateTime.Now, TimeZoneInfo.FindSystemTimeZoneById(TimeZones.Paris.Id));
Meghs Dhameliya
fonte
1
Desculpe, mas não está disponível no Asp .NET Core 2.2 aqui, o VS2017 está me sugerindo a instalação de um pacote do Outlook Nuget.
Machado
exemplo => TimeZoneInfo.ConvertTime (DateTime.Now, TimeZoneInfo.FindSystemTimeZoneById ("Horário padrão do Pacífico"))
AZ_