I'm afraid I don't really understand how .Net's DateTime class handles local timestamps (I live in Germany, so my locale is de_DE). Perhaps someone can enlighten me a bit ;-)
The DateTime constructor can be called with year, month etc. parameters. Additionally a DateTimeKind value of Local, Utc, or Unspecified (=default) can be provided.
Example:
DateTime a = new DateTime(2015, 03, 29, 02, 30, 00, DateTimeKind.Local);
DateTime b = new DateTime(2015, 03, 29, 02, 30, 00, DateTimeKind.Utc);
DateTime c = new DateTime(2015, 03, 29, 02, 30, 00, DateTimeKind.Unspecified);
DateTime d = new DateTime(2015, 03, 29, 02, 30, 00);
Per definition, values c and d are identical. But if I compare all against each other, all four are identical. Inspecting the objects in VS's debugger shows that the Ticks value (and InternalTicks as well) is the same for all. However, internal dateData values are different, but are obviously ignored by the comparison operator.
As you might have noticed, I constructed a value for March 29th this year, 02:30 AM. This moment in time does not exist in our timezone as it is skipped by switching to Daylight Saving Time. So I had expected to get an exception for constructing object a, but this did not happen.
Furthermore, DateTime has a method ToUniversalTime() that converts a value that is interpreted as local time to the equivalent UTC value. For testing, I ran a loop as follows:
DateTime dt = new DateTime(2015, 03, 29, 01, 58, 00, DateTimeKind.Local);
DateTime dtEnd = new DateTime(2015, 03, 29, 03, 03, 00, DateTimeKind.Local);
while (dt < dtEnd)
{
Log(" Localtime " + dt + " converted to UTC is " + dt.ToUniversalTime());
dt = dt.AddMinutes(1);
}
The result is:
Localtime 29.03.2015 01:58:00 converted to UTC is 29.03.2015 00:58:00
Localtime 29.03.2015 01:59:00 converted to UTC is 29.03.2015 00:59:00
Localtime 29.03.2015 02:00:00 converted to UTC is 29.03.2015 01:00:00
Localtime 29.03.2015 02:01:00 converted to UTC is 29.03.2015 01:01:00
Localtime 29.03.2015 02:02:00 converted to UTC is 29.03.2015 01:02:00
...
Localtime 29.03.2015 02:58:00 converted to UTC is 29.03.2015 01:58:00
Localtime 29.03.2015 02:59:00 converted to UTC is 29.03.2015 01:59:00
Localtime 29.03.2015 03:00:00 converted to UTC is 29.03.2015 01:00:00
Localtime 29.03.2015 03:01:00 converted to UTC is 29.03.2015 01:01:00
Localtime 29.03.2015 03:02:00 converted to UTC is 29.03.2015 01:02:00
So, .Net has no problem converting non-existing timestamps from local time to UTC. Also, adding a minute to an existing local timestamp is not local-aware and gives a non-existing timestamp.
As a result, adding 64 single minutes yields, after conversion, a UTC timestamp that is only 4 minutes larger than before.
In other words, converting between local time and UTC should be a bijection, giving a one-to-one correspondence between legal timestamp values.
To cut a long story short: How do I handle this correctly the intended way (according to .Net)? What is the sense of having DateTimeKind if it is not taken into account correctly? I don't even dare to ask how leap seconds (at 23:59:60) are handled ;-)
Mike's answer is good. Yes, DateTimeOffset is almost always prefered over DateTime (but not for all scenarios), and Noda Time is vastly superior in many regards. However, I can add some more details to address your questions and observations.
First, MSDN has this to say:
UTC time is suitable for calculations, comparisons, and storing dates and time in files. Local time is appropriate for display in user interfaces of desktop applications. Time zone-aware applications (such as many Web applications) also need to work with a number of other time zones.
...
Conversion operations between time zones (such as between UTC and local time, or between one time zone and another) take daylight saving time into account, but arithmetic and comparison operations do not.
From this we can conclude that the test you provided is not valid because it performs calculations using a local time. It is useful only in that it highlights how the API allows you to break its own documented guidelines. In general, since the time from 02:00 to just before 03:00 does not exist in the local time zone on that date, it's not likely to be encountered in the real world unless it was obtained mathematically, such as by a daily recurrence pattern that didn't take DST into account.
BTW, the part of Noda Time that addresses this is the ZoneLocalMappingResolver, which is used when converting a LocalDateTime to a ZonedDateTime via the localDateTime.InZone method. There are some reasonable defaults such as InZoneStrictly, or InZoneLeniently, but it's not just silently shifted like you illustrated with DateTime.
With regard to your assertion:
In other words, converting between local time and UTC should be a bijection, giving a one-to-one correspondence between legal timestamp values.
Actually, it's not a bijection. (By the definition of bijection on Wikipedia, it does not satisfy criteria 3 or 4.) Only conversion in the UTC-to-local direction is a function. Conversion in the local-to-UTC direction has a discontinuity in during the spring-forward DST transition, and has ambiguity during the fall-back DST transition. You may wish to review the graphs in the DST tag wiki.
To answer your specific questions:
How do I handle this correctly the intended way (according to .Net)?
DateTime dt = new DateTime(2015, 03, 29, 01, 58, 00, DateTimeKind.Local);
DateTime dtEnd = new DateTime(2015, 03, 29, 03, 03, 00, DateTimeKind.Local);
// I'm putting this here in case you want to work with a different time zone
TimeZoneInfo tz = TimeZoneInfo.Local; // you would change this variable here
// Create DateTimeOffset wrappers so the offset doesn't get lost
DateTimeOffset dto = new DateTimeOffset(dt, tz.GetUtcOffset(dt));
DateTimeOffset dtoEnd = new DateTimeOffset(dtEnd, tz.GetUtcOffset(dtEnd));
// Or, if you're only going to work with the local time zone, you can use
// this constructor, which assumes TimeZoneInfo.Local
//DateTimeOffset dto = new DateTimeOffset(dt);
//DateTimeOffset dtoEnd = new DateTimeOffset(dtEnd);
while (dto < dtoEnd)
{
Log(" Localtime " + dto + " converted to UTC is " + dto.ToUniversalTime());
// Math with DateTimeOffset is safe in instantaneous time,
// but it might not leave you at the desired offset by local time.
dto = dto.AddMinutes(1);
// The offset might have changed in the local zone.
// Adjust it by either of the following (with identical effect).
dto = TimeZoneInfo.ConvertTime(dto, tz);
//dto = dto.ToOffset(tz.GetUtcOffset(dto));
}
What is the sense of having DateTimeKind if it is not taken into account correctly?
Originally, DateTime didn't have a kind. It behaved as if the kind was unspecified. DateTimeKind was added in .NET 2.0.
The main use case it covers is to prevent double conversion. For example:
DateTime result = DateTime.UtcNow.ToUniversalTime();
or
DateTime result = DateTime.Now.ToLocalTime();
Before .NET 2.0, these would both result in bad data, because the ToUniversalTime and ToLocalTime methods had to make the assumption that the input value was not converted. It would blindly apply the time zone offset, even when the value was already in the desired time zone.
There are a few other edge cases, but this is the main one. Also, there is a hidden fourth kind, which is used such that the following will still hold up with ambiguous values during the fall-back transition.
DateTime now = DateTime.Now;
Assert.True(now.ToUniversalTime().ToLocalTime() == now);
Jon Skeet has a good blog post about this, and you can also now see it discussed in the comments in the .NET Reference sources or in the new coreclr sources.
I don't even dare to ask how leap seconds (at 23:59:60) are handled ;-)
Leap seconds are actually not supported by .NET at all, including the current version of Noda Time. They're also not supported by any of the Win32 APIs, nor will you ever observe a leap second on the Windows clock.
In Windows, leap seconds are applied via NTP synchronization. The clock ticks by as if the leap second didn't occur, and during its next clock sync, the time is adjusted and it is absorbed. Here's what the next leap second will look like:
Real World Windows
-------------------- --------------------
2015-06-30T23:59:58Z 2015-06-30T23:59:58Z
2015-06-30T23:59:59Z 2015-06-30T23:59:59Z
2015-06-30T23:59:60Z 2015-07-01T00:00:00Z <-- one sec behind
2015-07-01T00:00:00Z 2015-07-01T00:00:01Z
2015-07-01T00:00:01Z 2015-07-01T00:00:02Z
2015-07-01T00:00:02Z 2015-07-01T00:00:02Z <-- NTP sync
2015-07-01T00:00:03Z 2015-07-01T00:00:03Z
I'm showing the sync at 2 seconds past midnight, but it could really be much later. Clock sync happens all the time, not just at leap seconds. The computer's local clock is not an ultra-precise instrument - it will drift, and periodically has to be corrected. You cannot assume the current time is always monotonically increasing - it could skip forward, or jump backward.
Also, the chart above isn't exactly accurate. I showed a hard shift in seconds, but in reality the OS will often introduce minor corrections by spreading out the effect of the change in several sub-second increments over a longer period of several seconds (a few milliseconds at a time).
At an API level, none of the APIs will support more than 59 in a seconds field. If they were to support it at all, it would probably just be during parsing.
DateTime.Parse("2015-06-30T23:59:60Z")
This will throw an exception. If it were to work, it would have to munge the extra leap second and either return the previous second (2015-06-30T23:59:59Z), or the next second (2015-07-01T00:00:00Z).
Yes, the DateTime type in .NET is major mess, as you can observe because it doesn't support the concepts of time-zones, multiple calendars and many other useful concepts such as intervals etc.
A little better is the DateTimeOffset type which adds time-zone offset information. The DateTimeOffset will allow you to more accurately represent the times you show in your question and comparisons will take the timezone offset into account. But this type is not perfect either. It still does not support true time zone information, only the offset. So it is not possible to perform complex DST calculations or to support advanced calendars.
For a much more thorough solution you can use NodaTime
Related
When I check optionDate's DateTime property's DateTimeKind value, I see Unspecified, even though I set dt's DateTimeKind as UTC in below code. I expect optionDate has a DateTime which has a DateTimeKind property set to UTC. Where am I wrong here?
var dt = new DateTime(Convert.ToInt32(optionDateInfo.dateTime.year),
Convert.ToInt32(optionDateInfo.dateTime.month), Convert.ToInt32(optionDateInfo.dateTime.day),
Convert.ToInt32(optionDateInfo.dateTime.hour), Convert.ToInt32(optionDateInfo.dateTime.minutes),
0, DateTimeKind.Utc);
var optionDate = new DateTimeOffset(dt);
This is documented:
DateTimeOffset.DateTime
The value of the DateTime.Kind property of the returned DateTime object is DateTimeKind.Unspecified.
Note that a DateTimeOffset does not have a "kind". It has a date, time, and offset. When you pass your DateTime with kind Utc, to it, it sets its offset to 0, and its date & time to the DateTime given. At this point, your DateTimeKind is "lost".
An offset of 0 does not necessarily mean that its kind is DateTimeKind.Utc. It could be the local time in London, or somewhere in Africa too. So it can't give you a DateTime with kind Utc just because its offset is 0 either.
In addition, DateTime being able to represent 3 kinds of things is already a questionable design, and if the DateTime property can now return 3 different kinds of DateTime depending on whether offset matches the local time, is UTC, or something else, that's just even worse.
Instead, it is designed to have 3 properties that give you DateTimes with different kinds.
DateTime gives you the date & time part of the DateTimeOffset, with kind Unspecified
LocalDateTime converts the date & time part of the DateTimeOffset to the current timezone, and gives you a DateTime with kind Local.
UtcDateTime converts the date & time part of the DateTimeOffset to UTC, and gives you a DateTime with kind Utc.
If you want a DateTime with kind Utc, you should use that last one.
Use the SpecifyKind
var myUtcZeroOffset = DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc)
//If constructing a datetime offset to be not utc you can supply the offset instead
var myOffSetExplicitLocal = new DateTimeOffset(DateTime.Now, new TimeSpan(1, 0, 0));
var localDateTime = myOffSetExplicitLocal.DateTime;
var utcZeroOffSetDateTime = myOffSetExplicitLocal.UtcDateTime;
To make matters worse of cause it is a criticisable implementation from Microsoft, because Universally Coordinated Time is not a timezone but a notation, as per ISO 8601, so in fact toUTC as a concept is flawed because '2021-11-02T10:16:25.12345+01:00' is completely valid in the UTC format and UTC Zero offset, popularily called Zulu being the '2021-11-02T09:16:25.12345Z' equivalent which then gets datetimekind UTC is actually just in coordinated time the zero line around GMT latitude, but what makes it coordinated is the + part which in +00:00 can be abbreviated to Z, so lots of stuff is done to mitigate the inherent conflict and with build servers and cloud providers the .Local is especially dubious, so I would recommend always to persist in ISO 8601 strings instead, unless you actually need to use them in with date operations in Your DB, in said case to name fields appropriate like DateTimeCreatedUtcZero column e.g.
just my five cents of reason on the topic in general, hope it helps.
This question already has answers here:
Why doesn't C# detect that 1970/1/1 was under BST?
(3 answers)
Closed 4 years ago.
British Summer time adjusts clocks forward in March and back in October every year. During 1968 to 1971 the UK trialed BST as a permanent option, such that the clocks where put forward 1 hour in March 1968 and not reverted back until October 1971.
I am creating dates in Javascript, serializing them as JSON and posting to a WebApi.
Currently using windows 7 as a development environment, Windows is NOT recognizing that period as BST. For example 01/01/1970 should be Daylight Saving time, however
new System.DateTime(1970, 01, 01, 00, 00, 00).IsDaylightSavingTime();
returns false.
also...
System.TimeZone.CurrentTimeZone.GetDaylightChanges(1970)
{System.Globalization.DaylightTime}
Delta: {01:00:00}
End: {25/10/1970 02:00:00}
Start: {29/03/1970 01:00:00}
1970 should have a rule covering the entire year as the whole year was BST.
Is there a patch to correct the flaw in Windows?
Update
There is a bug after all, or rather GMT Standard Time doesn't contain UK-specific rules. Between 1968 and 1970 the offset for the UK changed to +1:00 and there was no DST.
The real problem is that the offset is wrong for the UK for that period :
var date= new DateTime(1970,1,1,0,0,0,DateTimeKind.Local);
var tzi = TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time");
var offset=tzi.GetUtcOffset(date);
offset is 00:00:00. Oooops!
For 1970-08-01 the offset is 01:00:00 and IsDaylightSavingTime() returns True.
PS:
SQL Server's AT TIME ZONE uses the Windows time zone names. This could be an even bigger source of problems with historical data.
Original
There's no bug. When the entire year has a single offset it doesn't make sense to talk about Daylight Saving Time.
The IANA timezone database shows that 1970-01-01 did not use DST. The offset was +1:00. Using NodaTime :
var london = DateTimeZoneProviders.Tzdb["Europe/London"];
// Time zone conversions
var localDate = new LocalDateTime(1970, 1, 1, 0, 0, 00);
var before = london.AtStrictly(localDate);
Console.WriteLine($"{before} {before.IsDaylightSavingTime()} {before.Offset}");
This returns :
1970-01-01T00:00:00 Europe/London (+01) DST:False +01
For 1971-11-01 the result is :
1971-11-01T00:00:00 Europe/London (+00) DST:False +00
At that point the offset changed from +1:00 to +00:00 and the DST rule was reintroduced.
The results are more interesting for summer dates.
1971-07-30 returns :
1971-07-30T00:00:00 Europe/London (+01) DST:False +01
Which is correct - there were no DST rules in effect on that date. The offset was fixed at +1.
1972-07-30 returns :
1972-07-30T00:00:00 Europe/London (+01) DST:True +01
The offset is the same, because the DST rules were in effect on that date.
Windows does not have a separate time zone for the United Kingdom. The time zone used by Windows in the United Kingdom ("GMT Standard Time") is shared with Ireland and Portugal.
That's why historic deviations that only applied to the United Kingdom are not reflected.
Borders of countries have changed a lot even in the last 200 years and until quite recently (only a few decades ago) very small regions in Europe had their own definition of time zones and they changed frequently. Windows cannot reflect that complex information. If you need that information you need to use a dedicated database.
As has been stated in the comments, the code you presented is using the current timezone on the computer you are running that code. This might be the UK but it's not a good idea to assume. The below code takes into account the comments above:
var tzi = TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time");
var dt = new DateTime(1970, 1, 2, 0, 0, 0, DateTimeKind.Utc);
var isDlt = tzi.IsDaylightSavingTime(dt);
This also returns false though so the bug as you state does indeed exist. I very much doubt there is a patch but if you were so inclined you could quite easily write an extension method that uses the IANA database to determine if a given date is during daylight saving.
You might also want to look at the documentation for TimeZoneInfo.GetAdjustmentRules - https://msdn.microsoft.com/en-us/library/system.timezoneinfo.getadjustmentrules(v=vs.110).aspx
We have DateTimeOffsets in Database/Model. To display these values in Web, we convert the DateTimeOffsets into the current user's timezone.
According MSDN, DateTimeOffset can be ambiguous in a specific TimeZone:
TimeZoneInfo.IsAmbiguousTime Method (DateTimeOffset)
This doesn't make sense to me at all. Can someone please give me an example DateTimeOffset which is ambiguous?
We're in TimeZone "W. Europe Standard Time".
Does what the documentation says not make it clear?
Typically, ambiguous times result when the clock is set to return to standard time from daylight saving time
I.e. if at 2am you come off of DST and reset the clock to 1am, then if someone starts talking about 1.30am, you don't know if that's 30 minutes from now or happened 30 minutes in the past.
There are a set of values (typically an hour long) which map to two different set of moments in UTC time.
I think the confusion comes from the way that "ambiguous" is defined here.
To be clear, a DateTimeOffset is never ambiguous unto itself. It always represents a specific moment in absolute, instantaneous time. Given a date, time, and offset, I can tell you both the local wall-time, and the precise UTC time (by applying the offset).
However, the wall-time portion of the value can be ambiguous within a specific time zone. That is, the date and time only when you ignore the offset. That's what TimeZoneInfo.IsAmbiguousTime is telling you. That if it weren't for the offset, the value would be ambiguous. The wall-time may be one that a person in that time zone might find confusing.
Consider that there are two overloads of this method, one that takes a DateTime and one that takes a DateTimeOffset.
The DateTime one makes perfect sense when .Kind is DateTimeKind.Unspecified.
DateTime dt = new DateTime(2016, 10, 30, 2, 0, 0);
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time");
bool ambiguous = tz.IsAmbiguousTime(dt); // true
It makes a little less sense with the other kinds, because it does conversions to the given time zone first - but still it does the same thing:
DateTime dt = new DateTime(2016, 10, 30, 1, 0, 0, DateTimeKind.Utc);
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time");
bool ambiguous = tz.IsAmbiguousTime(dt); // true
The DateTimeOffset overload is essentially doing the same thing as the previous example. Whatever the offset is, it gets applied to the date and time, then ambiguity is checked on the resulting date and time alone - just like in the first example.
DateTimeOffset dto = new DateTimeOffset(2016, 10, 30, 2, 0, 0, TimeSpan.FromHours(1));
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time");
bool ambiguous = tz.IsAmbiguousTime(dto); // true
Even with an offset that is meaningless to that time zone, it still gets applied before comparing.
DateTimeOffset dto = new DateTimeOffset(2016, 10, 29, 19, 0, 0, TimeSpan.FromHours(-5));
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time");
bool ambiguous = tz.IsAmbiguousTime(dto); // true
It boils down to the implementation of the overload, which is essentially:
// Make sure the dto is adjusted to the tz. This could be a no-op if it already is.
DateTimeOffset adjusted = TimeZoneInfo.ConvertTime(dto, tz);
// Then just get the wall time, stripping away the offset.
// The resulting datetime has unspecified kind.
DateTime dt = adjusted.DateTime;
// Finally, call the datetime version of the function
bool ambiguous = tz.IsAmbiguousTime(dt);
You can see this in the .net reference source here. They condense it to two lines, and preface it with a shortcut for better perf when DST is not applicable, but that's what it does.
Well the sample is (the last October's Sunday 2:00-3:00)
DateTimeOffset example = new DateTimeOffset(2015, 10, 25, 2, 30, 0,
new TimeSpan(0, 2, 0, 0));
TimeZoneInfo tst = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time");
if (tst.IsAmbiguousTime(example))
Console.Write("Ambiguous time");
Opposite to Ambiguous time is Invalid time (the last March's Sunday 2:00-3:00):
TimeZoneInfo tst = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time");
if (tst.IsInvalidTime(new DateTime(2016, 03, 27, 2, 30, 0)))
Console.Write("Invalid time");
I am trying to create an Instant based upon a B.C.E. year in the Gregorian calendar.
Here is what I have so far:
Instant.FromDateTimeOffset(new DateTimeOffset(-1000, 10, 01,
0, 0, 0, 0,
new System.Globalization.GregorianCalendar(),
new TimeSpan()));
I get the error:
An unhandled exception of type 'System.ArgumentOutOfRangeException' occurred in mscorlib.dll
Additional information: Year, Month, and Day parameters describe an un-representable DateTime.
Edit
Doing some research on NodaTime, I can see that it does have the ability to represent the dates I want, as it can accept negative ticks from the Unix epoch:
The Noda Time Instant type represents a point on this global timeline: the number of ticks which have elapsed since the Unix epoch. The value can be negative for dates and times before 1970 of course - the range of supported dates is from around 27000 BCE to around 31000 CE in the Gregorian calendar.
- http://nodatime.org/1.3.x/userguide/concepts.html
So I would like to know how to do this using NodaTime rather than create my own implementation, as is mentioned here for instance.
The correct way to create a BCE date in Noda Time is like this:
LocalDate date = new LocalDate(Era.BeforeCommon, 1000, 10, 1);
This gives a LocalDate object, which is representative of just having a date. You asked for an Instant, which represents an exact point in time. These are two very different concepts.
In order to get an Instant from a LocalDate, one has to make a few assertions:
What time of day did it occur?
What time zone was it in?
Is the time valid and unambiguous on that date within that zone?
Let's pick midnight for the time and UTC for the time zone:
Instant instant = date.AtMidnight().InUtc().ToInstant();
Since we chose UTC, we didn't have to address the valid/unambiguous question. With other zones, we would use one of the InZone methods instead of InUtc.
Also - you can indeed create an Instant directly (as Caramiriel showed), but be careful. Year 1 BCE is represented by year 0, so if you want 1000 BCE, you'd have to pass -999, not -1000.
Instant instant = Instant.FromUtc(-999, 10, 1, 0, 0, 0);
Again, this assumes the time at midnight and the UTC time zone.
Finally, keep in mind that none of these calendar systems or time zones actually existed during that time period, and usually when working with dates so old, the time part is not very accurate or relevant. Therefore, I recommend you attempt to only work in terms of LocalDate objects, and not use Instant at all, if you can.
I've got an asp.net application that must run some code every day at a specific time in the Eastern Time Zone (DST aware). So my first thought is, get the eastern time value and convert it to local server time.
So I need something like this:
var eastern = DateTime.Today.AddHours(17); // run at 5pm eastern
var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
var utc = TimeZoneInfo.ConvertTimeToUtc(eastern, timeZoneInfo);
var local = TimeZoneInfo.ConvertTimeFromUtc(utc, TimeZoneInfo.Local);
But how do I specify that the eastern DateTime object should be in the EST timezone.
Am I approaching this the wrong way?
First, there are several things you have to consider. You have to deal with Daylight Savings Time, which from time to time seems to change (the start and end dates have changed twice in the last 10 years). So in the Northern Hemisphere Winter, Eastern time is -5 GMT (or UTC). But, in the Summer it's -6 GMT or is that -4 GMT, I can never keep it straight (nor should I have to).
There are some DNF library functions to deal with time zone information, however you really need .net 3.5 for the most useful stuff. There's the TimeZoneInfo class in .net 3.5.
TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime dt = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(DateTime.Now,
TimeZoneInfo.IsDaylightSavingsTime(tzi) ?
tzi.DaylightName : tzi.StandardName);
if (dt.Hour == 17)
....
Also, keep in mind that twice every year an hour is lost or gained, so you also have to account for that if, for example, you have a countdown timer you display "time until next processing" or something like that. The fact is, time handling is not as easy as it would seem at first thought, and there are a lot of edge cases.
Seems I was able to answer my own question. Here's the code I'm using to get a next-run DateTime object.
private DateTime GetNextRun()
{
var today = DateTime.Today;
var runTime = new DateTime(today.Year, today.Month, today.Day, 17, 0, 0);
var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
var offset = timeZoneInfo.GetUtcOffset(runTime);
var dto = new DateTimeOffset(runTime, offset);
if (DateTime.Now > dto.LocalDateTime)
dto = dto.AddDays(1);
return dto.LocalDateTime;
}
Doing all the conversion using DateTimeOffset instead of DateTime proved effective. It even seems to handle Daylight Savings Time correctly.
A DateTime doesn't know about a time zone. Even DateTimeOffset doesn't really know about a time zone - it knows about a UTC instant and an offset from that.
You can write your own struct which does have a TimeZoneInfo and a DateTime, but I'm not sure you need it in this case. Are you just trying to schedule 5pm in Eastern time, or is this actually more general? What are you doing with the DateTime (or whatever) afterwards? Using DateTimeOffset and TimeZoneInfo you can definitely get the UTC instant of the time you're interested in; if you just need to know the time between "now" and then, that's fairly easy.
I feel duty-bound to point out that when Noda Time is production-ready, it would almost certainly be the right answer :)
You could use the DateTime.UtcNow to get UTC central time(which I believe is GMT 0) and from htere on just figure out how many time zones the one you want is and remove/add an hour for each zone.