How do you deal with 'Ambiguous Time' in .net? - c#

I need to convert a 'DateTime' value from different timezones to UTC and vice-versa. I use TimeZoneInfo to do this. But, the issue is when the 'Day Light Saving' time change happens.
For example, this year, next time the time change happens at 2AM[CDT] on November 3. So on Nov 3, 1AM[CDT] is converted to 6AM and as time change happens the next hour we get 1AM[now its CST] again and it is also converted to 6AM. I tried the code on this page, but it didn't say anything how to handle this issue. So how to deal with this issue???
Edit:
I tried NodaTime and when I do the conversion like
DateTimeZoneProviders.Tzdb["America/Chicago"].AtStrictly(<localDateTime>)
it throws AmbiguousTimeException. Thats good and I can do this using TimeZoneInfo too. But how do I know which localTime value I need to pick?
Edit 2:
here is the link for the chat discussion with Matt.

If all you have is a local time, and that time is ambiguous, then you cannot convert it to an exact UTC instant. That is why we say "ambiguous".
For example, in the US Central time zone, which has the IANA zone name America/Chicago and the Windows zone id Central Standard Time - covering both "Central Standard Time" and "Central Daylight Time". If all I know is that it is November 3rd, 2013 at 1:00 AM, then then this time is ambiguous, and there is absolutely no way to know whether this was the first instance of 1:00 AM that was in Central Daylight Time (UTC-5), or Central Standard Time (UTC-6).
Different platforms do different things when asked to convert an ambiguous time to UTC. Some go with the first instance, which is usually the Daylight time. Some go with the Standard time, which is usually the second instance. Some throw an exception, and some (like NodaTime) give you a choice of what you want to happen.
Let's start with TimeZoneInfo first.
// Despite the name, this zone covers both CST and CDT.
var tz = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time");
var dt = new DateTime(2013, 11, 3, 1, 0, 0);
var utc = TimeZoneInfo.ConvertTimeToUtc(dt, tz);
Debug.WriteLine(utc); // 11/3/2013 7:00:00 AM
As you can see, .net chose to use the "standard" time, which is UTC-6. (Adding 6 hours to 1AM gets you to 7AM). It didn't give you any warning that the time was ambiguous. You could have checked yourself, like this:
if (tz.IsAmbiguousTime(dt))
{
throw new Exception("Ambiguous Time!");
}
But there isn't anything to enforce this. You must check it yourself.
The only way to not have ambiguity is to not use the DateTime type. Instead, you can use DateTimeOffset. Observe:
// Central Standard Time
var dto = new DateTimeOffset(2013, 11, 3, 1, 0, 0, TimeSpan.FromHours(-6));
var utc = dto.ToUniversalTime();
Debug.WriteLine(utc); // 11/3/2013 7:00:00 AM +00:00
// Central Daylight Time
var dto = new DateTimeOffset(2013, 11, 3, 1, 0, 0, TimeSpan.FromHours(-5));
var utc = dto.ToUniversalTime();
Debug.WriteLine(utc); // 11/3/2013 6:00:00 AM +00:00
Now, compare this to NodaTime:
var tz = DateTimeZoneProviders.Tzdb["America/Chicago"];
var ldt = new LocalDateTime(2013, 11, 3, 1, 0, 0);
// will throw an exception, only because the value is ambiguous.
var zdt = tz.AtStrictly(ldt);
// will pick the standard time, like TimeZoneInfo did
var zdt = tz.AtLeniently(ldt);
// manually specify the offset for CST
var zdt = new ZonedDateTime(ldt, tz, Offset.FromHours(-6));
// manually specify the offset for CDT
var zdt = new ZonedDateTime(ldt, tz, Offset.FromHours(-5));
// with any of the above, once you have a ZonedDateTime
// you can get an instant which represents UTC
var instant = zdt.ToInstant();
As you can see, there are lots of options. All are valid, it just depends on what you want to do.
If you want to completely avoid ambiguity, then always keep a DateTimeOffset, or when using NodaTime use a ZonedDateTime or OffsetDateTime. If you use DateTime or LocalDateTime, there is no avoiding ambiguity.

Related

C# Datetime ISO 8601 format gives incorrect value

I am attempting to get a correctly formatted ISO 8601 date, and I keep hitting issues. My initial code seemed to be working, but I found that DST dates were not returning as expected. I made a .NET fiddle to ask about this issue here on stackoverflow, but it seems the way the "system" timezone works is going to cause me further problems when I deploy my code.
Here is a dotnet fiddle that displays something completely wrong:
using System;
public class Program
{
public static void Main()
{
var val3 = TimeZoneInfo.ConvertTimeFromUtc(new DateTime(2021, 10, 13, 18, 0, 0), TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time"));
Console.WriteLine(val3.ToString("yyyy-MM-ddTHH:mm:sszzz"));
}
}
If I run this, I get the following:
2021-10-13T13:00:00+00:00
So the time is correct for CST, but the offset looks like it is reflecting the "system" timezone on the server. Taken altogether, the date is completely different than the input date.
If I run this code on my development system where the local timezone is CST, I get yet another different answer:
2021-10-13T08:00:00-06:00
Given that the date in question was in DST, I expect both of the above to return the following:
2021-10-13T13:00:00-05:00
What I am doing wrong?
Let me see if my understanding is correct (I'm still not entirely sure when the SOAP comes into play in the question).
You have a UTC DateTime:
var dt = new DateTime(2021, 10, 13, 18, 0, 0, DateTimeKind.Utc);
And you'd like to format that in CST -- which, on October 13, is actually CDT (-05:00)?
If that is correct, then you want to utilise DateTimeOffset, which has a better understanding of.. well, time offsets.
// Your original value. This will have an offset of "00:00"
// as the DateTime above was created as `DateTimeKind.Utc`
var timeAtUtc = new DateTimeOffset(dt);
// Alternatively:
// var timeAtUtc = new DateTimeOffset(2021, 10, 13, 18, 0, 0, TimeSpan.Zero);
// Find out the correct offset on that day (-5:00)
var cstOffsetAtTheTime = TimeZoneInfo
.FindSystemTimeZoneById("Central Standard Time")
.GetUtcOffset(timeAtUtc);
// And now apply the offset to your original value
var timeAtCst = timeAtUtc.ToOffset(cstOffsetAtTheTime);
// You will now get "2021-10-13T13:00:00-05:00"
// no matter where you run this from.
Console.WriteLine(timeAtCst.ToString("yyyy-MM-ddTHH:mm:sszzz"));
// Edit: If you pass it a date in Feb, you'll see that it will correctly be at -06:00.
Edit 2022-03-07 based on comment below. If you don't need to care about the offset value, you can simply do:
var timeAtUtc = ...;
var timeAtCst = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(
timeAtUtc,
"Central Standard Time");
NPras's answer is good, but just to answer the question about what was wrong with the original code:
A DateTime object does not store time zone offset information. It only stores a Kind property, which can be one of three DateTimeKind values, either Utc, Local, or Unspecified.
Since your input was a DateTime, the output also has to be a DateTime, which has no time zone offset. Thus the offset related to your target time zone is discarded. Using a DateTimeOffset allows the target offset to persist.
In the final console output, you used zzz to show the offset. The .NET documentation explains why this used the server's time zone:
With DateTime values, the "z" custom format specifier represents the signed offset of the local operating system's time zone from Coordinated Universal Time (UTC), measured in hours. It doesn't reflect the value of an instance's DateTime.Kind property. For this reason, the "z" format specifier is not recommended for use with DateTime values.
With DateTimeOffset values, this format specifier represents the DateTimeOffset value's offset from UTC in hours.
As an aside, if you actually need to show an offset for a DateTime whose Kind is either Utc or Local, you should use the K specifier (the K is for Kind) instead of zzz. With the K specifier, Utc kinds will display a "Z", Local values will display the local offset such as "-05:00", and Unspecified values will display neither (an empty string).

In C#, how can I always treat a DateTime value as being local to Europe/London at the time represented by the DateTime, and convert this to UTC?

I am working with DateTime values in a SQL Server database I don't maintain, and I want to work with them in my code as UTC.
To assist in understanding the problem, the values I'm working with represent the time that actions took place in our CRM system.
When I retrieve the values from SQL Server, they have no time zone indication on them but I know that they always represent Europe/London - either UTC in the winter, or UTC+1 in the summer.
I understand that I can use DateTimeKind.Local to indicate that a DateTime value is expressed in local time, but I don't understand how I specify which time zone the DateTime applies to. For example, if I'm working with the DateTime 2021-01-01 12:34:56, I need to ensure that regardless of where or when my code is running, this date is correctly interpreted as 2021-01-01 12:34:56 +00:00. Equally, I need to ensure that regardless of where or when my code is running, I need to interpret 2021-05-01 12:34:56 as 2021-05-01 12:34:56 +01:00.
How can I indicate that my DateTime values always apply to Europe/London at the time they represent?
Check nodaTime is a powerful datetime library for C#.
The example in there homepage seems to be related to your case.
/ Instant represents time from epoch
Instant now = SystemClock.Instance.GetCurrentInstant();
// Convert an instant to a ZonedDateTime
ZonedDateTime nowInIsoUtc = now.InUtc();
// Create a duration
Duration duration = Duration.FromMinutes(3);
// Add it to our ZonedDateTime
ZonedDateTime thenInIsoUtc = nowInIsoUtc + duration;
// Time zone support (multiple providers)
var london = DateTimeZoneProviders.Tzdb["Europe/London"];
// Time zone conversions
var localDate = new LocalDateTime(2012, 3, 27, 0, 45, 00);
var before = london.AtStrictly(localDate);

DateTime conversion for HubSpot API is not working

I am pushing date/time values into the HubSpot CRM system via their API. For date/time values, the HS API requires UNIX format, which is milliseconds from Epoch (1/1/1970 12:00 AM). [HubSpot docs: https://developers.hubspot.com/docs/faq/how-should-timestamps-be-formatted-for-hubspots-apis]
But my dates are representing incorrectly. I am pulling dates from a SQL database that is in EST, and performing the following conversion:
string dbValue = "2019-02-03 00:00:00";
DateTime dt = Convert.ToDateTime(dbValue);
DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc);
long apiValue = Convert.ToInt64(dt.Subtract(epoch).TotalMilliseconds);
However, in HubSpot, the date field shows 2/2/2019. The time zone in HubSpot is set to UTC -4 Eastern Time.
It seems like there is some conversion issue, but I do not know what to do to correct it. I've tried using DateTime.SpecifiyKind to explicitly set dt to local before converting to long:
dt = DateTime.SpecifyKind(dt, System.DateTimeKind.Local);
But that hasn't worked either. I tried doing a basic test:
var dt1 = new DateTime(2019, 4, 1, 12, 0, 0, DateTimeKind.Local);
var dt2 = new DateTime(2019, 4, 1, 12, 0, 0, DateTimeKind.Utc);
Console.WriteLine(dt1.Subtract(dt2).TotalSeconds);
But the result was 0. I am in CST, and I was expecting like a 5 hour difference. I feel like I am missing some fundamental concept here on how DateTimes work in C#.
A few things:
Subtracting DateTime values does not take DateTimeKind into account.
.NET Framework 4.6 and higher have conversion functions to/from Unix time built in to the DateTimeOffset class, so you don't need to do any subtraction at all.
When you say EST or CST, I'll assume you meant US Eastern Time or US Central Time. Keep in mind that because of daylight saving time, that EDT or CDT might apply to some of your values.
You shouldn't be parsing from strings if the value is coming from your database. I'll assume you just gave that for the example here. But in your actual code, you should be doing something like:
DateTime dt = (DateTime) dataReader("field");
(If you're using Entity Framework or some other ORM, then this part would be handled for you.)
It doesn't matter what time zone the SQL Server is located in. It matters rather what time zone the dbValue you have is intended to represent. A best practice is to keep time in terms of UTC, in which case the server's time zone should be irrelevant.
If the datetime stored in your SQL server is actually in UTC, then you can simply do this:
long apiValue = new DateTimeOffset(dt, TimeSpan.Zero).ToUnixTimeMilliseconds();
If the datetime stored in your SQL server really is in US Eastern Time, then you will need to first convert from Eastern Time to UTC:
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime utc = TimeZoneInfo.ConvertTimeToUtc(dt, tz);
long apiValue = new DateTimeOffset(utc).ToUnixTimeMilliseconds();
Note that the Windows time zone ID of "Eastern Standard Time" represents US Eastern Time including EDT when applicable, despite having the word "Standard" in the middle.
If you are running in .NET Core on a non-Windows platform, pass "America/New_York" instead. (And if you need to write for cross-platform resiliency, use my TimeZoneConverter library.)
Lastly, though it might be a bit dangerous to assume the time in the DB is in the same local time as the code accessing the DB, if you really wanted to make such a gamble, you could do it like this:
long apiValue = new DateTimeOffset(dt).ToUnixTimeMilliseconds();
That works only if dt.Kind is DateTimeKind.Unspecified or DateTimeKind.Local, as it would then apply the local time zone. But again, I would recommend against this.

Getting local hour with NodaTime

New to NodaTime and I chose to use it instead of the BCL libraries because it removes a lot of ambiguity when dealing with dates. However, I can't seem to get it to do what I want. I have a date and time specified by year, month, day, hours, and minutes. I also have two timezones for which I need to display the "clock time". For example, if my input is December 15, 2015 at 3:30 PM and my two timezones are central standard and eastern standard (one hour apart), I expect my output to be
12/15/2015 3:30 PM Central
12/15/2015 4:30 PM Eastern
But I can only seem to get the central (local to me, if that matters) timezone. Here's my code:
var localDateTime = new LocalDateTime(
year: 2015,
month: 12,
day: 15,
hour: 15,
minute: 30,
second: 0
);
var centralTimeZone = DateTimeZoneProviders.Bcl.GetZoneOrNull("Central Standard Time");
var easternTimeZone = DateTimeZoneProviders.Bcl.GetZoneOrNull("Eastern Standard Time");
var centralTime = centralTimeZone.AtLeniently(localDateTime);
var easternTime = easternTimeZone.AtLeniently(localDateTime);
It seems that centralTime and easternTime are both ZonedDateTime objects whose times are 2015-12-10T15:30 with the correct offset i.e. centralTime is -6 and easternTime is -5.)
I just can't figure out how to get the output I want.
It sounds like your initial date/time is actually in Central time - so once you've performed the initial conversion, you can just say "Hey, I want the same instant in time, but in a different time zone" - which isn't the same as "I want a different instant in time for the same local time". You want:
var centralTime = centralTimeZone.AtLeniently(localDateTime);
var easternTime = centralTime.InZone(easternTimeZone);

Difference Between DateTime (UTC) Based on a Local Timezone

I have two DateTime objects which contain two UTC date/times and a users TimezoneId (tzdb) as a string. I'm trying to write a method that takes these three parameters and returns the total seconds (or Duration) between the two datetimes relative to the timezone.
public static double GetDurationForTimezone(DateTime startUtc, DateTime endUtc, string timezoneId)
{
var timezone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(timezoneId);
// convert UTC to timezone
var startInstantUtc = Instant.FromDateTimeUtc(startUtc);
var startZonedDateTime = startInstantUtc.InZone(timezone);
var endInstantUtc = Instant.FromDateTimeUtc(endUtc);
var endZonedDateTime = endInstantUtc.InZone(timezone);
return endZonedDateTime.ToInstant().Minus(startZonedDateTime.ToInstant()).ToTimeSpan().TotalSeconds;
}
I want to do it w.r.t. the timezone, to ensure it takes into account any possible Daylight Saving changes that may occur throughout this period.
Example test:
// DST starts (25h day -- DST starts: 10/4 # 2am local time)
var result = GetDurationForTimezone(
new DateTime(2015, 10, 3, 15, 0, 0, DateTimeKind.Utc),
new DateTime(2015, 10, 4, 15, 0, 0, DateTimeKind.Utc),
"Australia/Sydney");
Assert.Equal(TimeSpan.FromHours(25).TotalSeconds, result);
But when running this test, it seems like the calls to .ToInstant() are not adhering to the ZonedDateTime versions, but rather the original UTC DateTime objects. Thus I'm seeing the result be 24 hours.
When determining the duration between UTC-based timestamps, the time zone is irrelevant.
UTC is Coordinated Universal Time. It is the same for everyone on the planet. It does not have daylight saving time, and it's offset is always zero (UTC+00:00).
Since you have already asserted that the input values are in UTC, you do not necessarily need to use Noda Time for this operation. Just subtract the two values.
TimeSpan duration = endUtc - startUtc;
If you do use Noda Time, a UTC value is best represented by an Instant, which makes it very easy to obtain a Duration.
Instant start = Instant.FromDateTimeUtc(startUtc);
Instant end = Instant.FromDateTimeUtc(endUtc);
Duration duration = end - start;
You could also represent them using ZonedDateTime values that happen to be "in UTC", however you'd quickly find that the API requires you convert them back to Instant values to obtain a Duration anyway.
ZonedDateTime start = LocalDateTime.FromDateTime(startUtc).InUtc();
ZonedDateTime end = LocalDateTime.FromDateTime(endUtc).InUtc();
Duration duration = end.ToInstant() - start.ToInstant();
You might think that just using LocalDateTime would be an option, but that structure represents a wall time, without any time zone information. You can't obtain a Duration between two of them. You could obtain a Period by using Period.Between, but that would represent the calendar/clock-value difference between the two representations - which is not the same as the actual amount of time that has elapsed.
As a thought exercise that will help understand the difference, consider these two values:
2015-11-01 00:30
2015-11-01 01:30
If I tell you that the values are in UTC, then there is one hour difference. However, if I tell you these are wall-clock values and they are in the US Eastern Time zone, then they might be one hour apart, or they might be two hours apart. It depends on whether or not the 01:30 is the one before the DST transition, or the one after - as there are two on this day.
Now if instead I gave you these values:
2015-11-01 00:30
2015-11-01 02:30
Again, if you interpret them as UTC they are exactly two hours apart. But if you interpret them in the same US Eastern time zone, then they are exactly three hours apart, because the range is inclusive of the DST transition. If you just subtract the local wall-time values then you'd get two hours, which would be incorrect.
Switching to utilize the LocalDateTime property of the ZonedDateTime allows for comparing the date/times relative to the timezone. This works for both prime test cases (23h and 25h days):
public static double GetDurationForTimezone(DateTime startUtc, DateTime endUtc, string timezoneId)
{
var timezone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(timezoneId);
// convert UTC to timezone
var startInstantUtc = Instant.FromDateTimeUtc(startUtc);
var startZonedDateTime = startInstantUtc.InZone(timezone);
var startLocalDateTime = startZonedDateTime.LocalDateTime;
var endInstantUtc = Instant.FromDateTimeUtc(endUtc);
var endZonedDateTime = endInstantUtc.InZone(timezone);
var endLocalDateTime = endZonedDateTime.LocalDateTime;
return Period.Between(startLocalDateTime, endLocalDateTime, PeriodUnits.Seconds).Seconds;
}
Studying this page: ZonedDateTime.Comparer Members
it seems like you have to use property Local and not Instant to reflect the local daylight savings.

Categories