Tworzenie DateTime w określonej strefie czasowej w języku C #

162

Próbuję utworzyć test jednostkowy, aby przetestować przypadek, gdy strefa czasowa zmienia się na komputerze, ponieważ została nieprawidłowo ustawiona, a następnie poprawiona.

W teście muszę mieć możliwość tworzenia obiektów DateTime w żadnej lokalnej strefie czasowej, aby upewnić się, że osoby uruchamiające test mogą to zrobić pomyślnie, niezależnie od tego, gdzie się znajdują.

Z tego, co widzę z konstruktora DateTime, mogę ustawić TimeZone jako lokalną strefę czasową, strefę czasową UTC lub nieokreśloną.

Jak utworzyć DateTime z określoną strefą czasową, taką jak PST?

Jack Hughes
źródło
Powiązane pytanie - stackoverflow.com/questions/2532729/…
Oded

Odpowiedzi:

216

Odpowiedź Jona mówi o TimeZone , ale zamiast tego sugerowałbym użycie TimeZoneInfo .

Osobiście lubię przechowywać rzeczy w UTC tam, gdzie to możliwe (przynajmniej w przeszłości; przechowywanie UTC na przyszłość wiąże się z potencjalnymi problemami ), więc proponuję taką strukturę:

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); 
        }
    }        
}

Możesz chcieć zmienić nazwy „TimeZone” na „TimeZoneInfo”, aby wszystko było jaśniejsze - sam wolę krótsze nazwy.

Jon Skeet
źródło
5
Obawiam się, że nie znam żadnej równoważnej konstrukcji SQL Server. Proponuję mieć nazwę strefy czasowej w jednej kolumnie, a wartość UTC w drugiej. Pobierz je osobno, a następnie możesz dość łatwo tworzyć instancje.
Jon Skeet
2
Nie mam pewności co do oczekiwanego użycia konstruktora, który pobiera DateTime i TimeZoneInfo, ale biorąc pod uwagę, że wywołujesz metodę dateTime.ToUniversalTime (), podejrzewam, że zgadujesz, że „może” być w czasie lokalnym. W takim przypadku myślę, że naprawdę powinieneś używać przekazanego TimeZoneInfo, aby przekonwertować go na UTC, ponieważ mówią ci, że powinno być w tej strefie czasowej.
IDisposable
2
@ChrisMoschini: W tym momencie po prostu wymyślasz swój własny schemat identyfikacyjny - schemat, którego nikt inny na świecie nie używa. Będę trzymał się standardowego w branży informacji o strefie, dzięki. (Na przykład trudno zrozumieć, dlaczego „Europa / Londyn” jest bez znaczenia).
Jon Skeet,
2
@ChrisMoschini: Inny przykład niż: CST. Czy to UTC-5 czy UTC-6? A co z IST - czy w waszej bazie danych znajduje się Izrael, Indie czy Irlandia? (Nawet jeśli znasz przesunięcie w tej chwili, różne kraje obserwujące ten sam skrót mogą się zmieniać w różnym czasie. Dlatego nadal istnieje niejasność co do tego, która strefa czasowa to oznacza. Strefa czasowa! = Przesunięcie.) Wracając do sprawy: twierdzisz że użycie skrótów najlepiej rozwiązało problem. Jak gorsze byłoby używanie standardowych identyfikatorów stref czasowych?
Jon Skeet
6
@ChrisMoschini: Cóż, będę nadal zalecał używanie standardowych, jednoznacznych identyfikatorów informacji o strefie, zamiast niejednoznacznych skrótów. To nie jest kwestia tego, czyja biblioteka jest preferowana - autorstwo biblioteki tak naprawdę nie jest problemem. Jeśli ktoś chce użyć innej biblioteki z dobrym wyborem identyfikatora, to w porządku. Wybór identyfikatora strefy czasowej jest jednak ważny i myślę, że bardzo ważne jest, aby czytelnicy byli świadomi, że skróty niejednoznaczne, jak pokazałem na przykładzie IST.
Jon Skeet
54

Struktura DateTimeOffset została stworzona właśnie z myślą o takich zastosowaniach.

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

Oto przykład tworzenia obiektu DateTimeOffset z określoną strefą czasową:

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

CleverPatrick
źródło
1
Dzięki, to dobry sposób, aby to osiągnąć. Po uzyskaniu obiektu DateTimeOffset w odpowiedniej strefie czasowej możesz użyć właściwości .UtcDateTime, aby uzyskać czas UTC utworzony przez siebie. Jeśli przechowujesz swoje daty w UTC, konwersja ich na czas lokalny dla każdego użytkownika to nic
trudnego
2
Nie sądzę, aby to poprawnie obsługiwało czas letni, ponieważ niektóre strefy czasowe go przestrzegają, a inne nie. Również „w dniu” rozpoczęcia / zakończenia czasu letniego, części tego dnia będą wolne.
crokusek
14
Lekcja. Czas letni to reguła określonej strefy czasowej. DateTimeOffset nie jest powiązana z żadną strefą czasową. Nie należy mylić wartości przesunięcia czasu UTC, na przykład -5, ze strefą czasową. To nie jest strefa czasowa, to przesunięcie. To samo przesunięcie jest często wspólne dla wielu stref czasowych, więc jest to niejednoznaczny sposób określania strefy czasowej. Ponieważ DateTimeOffset jest skojarzona z przesunięciem, a nie strefą czasową, nie może zastosować reguł czasu letniego. Tak więc 3 nad ranem będzie 3 nad ranem każdego dnia w roku, bez wyjątku w strukturze DateTimeOffset (np. We właściwościach Hours i TimeOfDay).
Triynko,
Miejscem, w którym możesz się pomylić, jest sprawdzenie właściwości LocalDateTime DateTimeOffset. Ta właściwość NIE jest DateTimeOffset, jest instancją DateTime, której rodzajem jest DateTimeKind.Local. Ta instancja JEST powiązana ze strefą czasową ... niezależnie od strefy czasowej systemu lokalnego. Ta właściwość BĘDZIE odzwierciedlać oszczędność światła dziennego.
Triynko,
4
Tak więc prawdziwym problemem związanym z DateTimeOffset jest to, że nie zawiera wystarczającej ilości informacji. Zawiera przesunięcie, a nie strefę czasową. Przesunięcie jest niejednoznaczne w przypadku wielu stref czasowych.
Triynko,
41

Pozostałe odpowiedzi są przydatne, ale nie obejmują konkretnego sposobu uzyskania dostępu do Pacific - proszę bardzo:

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

Co dziwne, chociaż „Pacific Standard Time” zwykle oznacza coś innego niż „Pacific Daylight Time”, w tym przypadku odnosi się ogólnie do czasu pacyficznego. W rzeczywistości, jeśli użyjesz go FindSystemTimeZoneByIddo pobrania, jedną z dostępnych właściwości jest wartość bool informująca, czy w tej strefie czasowej jest obecnie czas letni, czy nie.

Możesz zobaczyć bardziej uogólnione przykłady tego w bibliotece, którą ostatecznie zebrałem, aby poradzić sobie z datami, których potrzebuję w różnych strefach czasowych, w zależności od tego, skąd użytkownik pyta, itp.:

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

To nie zadziała poza systemem Windows (na przykład Mono w systemie Linux), ponieważ lista czasów pochodzi z rejestru systemu Windows: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\

Poniżej znajdziesz klucze (ikony folderów w Edytorze rejestru); nazwy tych kluczy są tym, do których przekazujesz FindSystemTimeZoneById. W systemie Linux musisz użyć oddzielnego zestawu definicji stref czasowych w standardzie Linux, którego nie zbadałem odpowiednio.

Chris Moschini
źródło
1
Dodatkowo istnieje ConvertTimeBySystemTimeZoneId (), np. TimeZoneInfo.ConvertTimeBySystemTimeZoneId (DateTime.UtcNow, „Central Standard Time”)
Brent
W systemie Windows TimeZone Id List można również zobaczyć tę odpowiedź: stackoverflow.com/a/24460750/4573839
yu yang Jian
7

I zmienił Jon Skeet odpowiedzieć trochę w internecie metodą wydłużania. Działa również na lazurze jak urok.

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
źródło
2

Będziesz musiał utworzyć do tego niestandardowy obiekt. Twój obiekt niestandardowy będzie zawierał dwie wartości:

Nie jestem pewien, czy istnieje już typ danych dostarczony przez CLR, który to ma, ale przynajmniej składnik TimeZone jest już dostępny.

Jon Limjap
źródło
2

Podoba mi się odpowiedź Jona Skeeta, ale chciałbym dodać jedną rzecz. Nie jestem pewien, czy Jon spodziewał się, że ktor zawsze będzie przekazywany w lokalnej strefie czasowej. Ale chcę go używać w przypadkach, gdy jest to coś innego niż lokalny.

Odczytuję wartości z bazy danych i wiem, w jakiej strefie czasowej znajduje się ta baza danych. W programie ctor przekażę strefę czasową bazy danych. Ale chciałbym mieć wartość w czasie lokalnym. Jon's LocalTime nie zwraca oryginalnej daty przekonwertowanej na datę lokalnej strefy czasowej. Zwraca datę przekonwertowaną na oryginalną strefę czasową (cokolwiek przeszedłeś do ctora).

Myślę, że te nazwy nieruchomości wyjaśniają to ...

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
źródło
0

Korzystanie z klasy TimeZones ułatwia tworzenie określonej daty dla strefy czasowej.

TimeZoneInfo.ConvertTime(DateTime.Now, TimeZoneInfo.FindSystemTimeZoneById(TimeZones.Paris.Id));
Meghs Dhameliya
źródło
1
Przepraszamy, ale nie jest dostępny w Asp .NET Core 2.2 tutaj, VS2017 sugeruje mi zainstalowanie pakietu Outlook Nuget.
Machado
example => TimeZoneInfo.ConvertTime (DateTime.Now, TimeZoneInfo.FindSystemTimeZoneById ("Pacific Standard Time"))
AZ_