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.
Related
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);
I've been racking my brain all afternoon trying to figure this one out. Essentially, the problem itself seems simple. I'm given a date/time that is representative of a date and time in another time zone (not local). I want to convert this value to a UTC value to store in the database. However, all of the methods I find online seem to point to you either starting with UTC or starting with a local time zone. You can convert TO other time zones from these, but you can't start with anything other than those. As a result, it appears that I'll have to do some kind of convoluted offset math to do what I want. Here is an example of the problem:
var dateString = "8/20/2014 6:00:00 AM";
DateTime date1 = DateTime.Parse(dateString,
System.Globalization.CultureInfo.InvariantCulture);
var currentTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
// Now the server is set to Central Standard Time, so any automated offset calculation that it runs will come from that point of view:
var utcDate = date1.ToUniversalTime; // This is wrong
// Similarly, if I try to reverse-calculate it, it doesn't work either
var convertedDate = TimeZoneInfo.ConvertTime(date1, currentTimeZone);
utcDate = convertedDate.ToUniversalTime; // This is also wrong
In essence, I want to somehow tell the system that the datetime object I'm currently working with is from that time zone other than local, so that I know the conversion will be correct. I know that I'll eventually need to figure Daylight Savings Time in there, but that is a problem for another day.
Would this method be of any use to you ?
The TimeZoneInfo.ConvertTime method converts a time from one time zone
to another.
Alternatively, you could use the ConvertTimeToUtc method to simply convert any date (specifying the source time zone) to UTC.
var dateString = "8/20/2014 6:00:00 AM";
DateTime date1 = DateTime.Parse(dateString,
System.Globalization.CultureInfo.InvariantCulture);
var currentTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
var utcDate = TimeZoneInfo.ConvertTimeToUtc(date1, currentTimeZone);
The System.DateTime struct only has two bits for storing the "kind" information. That is why you can only have "local" or "universal" or "unknown" (or "magicl local").
Take a look at the System.DateTimeOffset struct. It is like a DateTime, but it also keeps the time zone (offset from (plus or minus) UTC).
What's the proper and more concise way to get the ZonedDateTime(s) which represent the start and the end of the current day in the timezone set on the system on which the code runs?
Isn't the following code too much complicated?
ZonedDateTime nowInZone = SystemClock.Instance.Now.InZone(DateTimeZoneProviders.Bcl.GetSystemDefault());
ZonedDateTime start = new LocalDateTime(nowInZone.Year, nowInZone.Month, nowInZone.Day, 0, 0, 0).InZoneStrictly(DateTimeZoneProviders.Bcl.GetSystemDefault());
ZonedDateTime end = new LocalDateTime(nowInZone.Year, nowInZone.Month, nowInZone.Day, 23, 59, 59).InZoneStrictly(DateTimeZoneProviders.Bcl.GetSystemDefault());
Given those values, I need to test if another ZonedDateTime is between them.
The AtStartOfDay value on the DateTimeZone object has the magic you're looking for.
// Get the current time
IClock systemClock = SystemClock.Instance;
Instant now = systemClock.Now;
// Get the local time zone, and the current date
DateTimeZone tz = DateTimeZoneProviders.Tzdb.GetSystemDefault();
LocalDate today = now.InZone(tz).Date;
// Get the start of the day, and the start of the next day as the end date
ZonedDateTime dayStart = tz.AtStartOfDay(today);
ZonedDateTime dayEnd = tz.AtStartOfDay(today.PlusDays(1));
// Compare instants using inclusive start and exclusive end
ZonedDateTime other = new ZonedDateTime(); // some other value
bool between = dayStart.ToInstant() <= other.ToInstant() &&
dayEnd.ToInstant() > other.ToInstant();
A couple of points:
It's better to get in the habit of separating the clock instance from the call to Now. This makes it easier to replace the clock later when unit testing.
You only need to get the local time zone once. I prefer to use the Tzdb provider, but either provider will work for this purpose.
For the end of day, it's better to use the start of the next day. This prevents you from having to deal with granularity issues, such as whether you should take 23:59, 23:59:59, 23:59.999, 23:59:59.9999999, etc. Also, it makes it easier to get whole-number results when doing math.
In general, date+time ranges (or time-only ranges) should be treated as half-open intervals [start,end) - while date-only ranges should be treated as fully-closed intervals [start,end].
Because of this, the start is compared with <= but the end is compared with >.
If you know for certain that the other ZonedDateTime value is in the same time zone and uses the same calendar, you can omit the calls to ToInstant and just compare them directly.
Update
As Jon mentioned in comments, the Interval type may be a useful convenience for this purpose. It is already set up to work with a half-open range of Instant values. The following function will get the interval for a the current "day" in a particular time zone:
public Interval GetTodaysInterval(IClock clock, DateTimeZone timeZone)
{
LocalDate today = clock.Now.InZone(timeZone).Date;
ZonedDateTime dayStart = timeZone.AtStartOfDay(today);
ZonedDateTime dayEnd = timeZone.AtStartOfDay(today.PlusDays(1));
return new Interval(dayStart.ToInstant(), dayEnd.ToInstant());
}
Call it like this (using the same values from above):
Interval day = GetTodaysInterval(systemClock, tz);
And now comparison can be done with the Contains function:
bool between = day.Contains(other.ToInstant());
Note that you still have to convert to an Instant, as the Interval type is not time zone aware.
My application is hosted on Windows Azure, which has all servers set to UTC.
I need to know when any given DateTime is subject to daylight savings time. For simplicity, we can assume that my users are all in the UK (so using Greenwich Mean Time).
The code I am using to convert my DateTime objects is
public static DateTime UtcToUserTimeZone(DateTime dateTime)
{
dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
var timeZone = TimeZoneInfo.FindSystemTimeZoneById("Greenwich Standard Time");
var userDateTime = TimeZoneInfo.ConvertTime(dateTime, timeZone);
return DateTime.SpecifyKind(userDateTime, DateTimeKind.Local);
}
However, the following test fails at the last line; the converted DateTime does not know that it should be in daylight savings time.
[Test]
public void UtcToUserTimeZoneShouldWork()
{
var utcDateTime = new DateTime(2014, 6, 1, 12, 0, 0, DateTimeKind.Utc);
Assert.IsFalse(utcDateTime.IsDaylightSavingTime());
var dateTime = Utils.UtcToUserTimeZone(utcDateTime);
Assert.IsTrue(dateTime.IsDaylightSavingTime());
}
Note that this only fails when my Windows time zone is set to (UTC) Co-ordinated Universal Time. When it is set to (UTC) Dublin, Edinburgh, Lisbon, London (or any other northern-hemisphere time zone that observes daylight savings), the test passes. If you change this setting in Windows a restart of Visual Studio is required in order for the change to fully take effect.
What am I missing?
Your last line specifies that the value is in the local time zone of the system it's running in - but it's not really... it's converted using a different time zone.
Basically DateTime is somewhat broken, in my view - which makes it hard to use correctly. There's no way of saying "This DateTime value is in time zone x" - you can perform a conversion to find a specific local time, but that doesn't remember the time zone.
For example, from the docs of DateTime.IsDaylightSavingTime (emphasis mine):
This method determines whether the current DateTime value falls within the daylight saving time range of the local time zone, which is returned by the TimeZoneInfo.Local property.
Obviously I'm biased, but I'd recommend using my Noda Time project for this instead - quite possibly using a ZonedDateTime. You can call GetZoneInterval to obtain the ZoneInterval for that ZonedDateTime, and then use ZoneInterval.Savings to check whether the current offset contains a daylight savings component:
bool daylight = zonedDateTime.GetZoneInterval().Savings != Offset.Zero;
This is slightly long-winded... I've added a feature request to consider adding an IsDaylightSavingTime() method to ZonedDateTime as well...
The main thing you are missing is that "Greenwich Standard Time" is not the TimeZoneInfo id for London. That one actually belongs to "(UTC) Monrovia, Reykjavik".
You want "GMT Standard Time", which maps to "(UTC) Dublin, Edinburgh, Lisbon, London"
Yes, Windows time zones are weird. (At least you don't live in France, which gets strangely labeled as "Romance Standard Time"!)
Also, you should not be doing this part:
return DateTime.SpecifyKind(userDateTime, DateTimeKind.Local);
That will make various other code think it came from the local machine's time zone. Leave it with the "Unspecified" kind. (Or even better, use a DateTimeOffset instead of a DateTime)
Then you also need to use the .IsDaylightSavingsTime method on the TimeZoneInfo object, rather than the one on the .DateTime object. (There are two different methods, with the same name, on different objects, with differing behavior. Ouch!)
But I wholeheartedly agree that this is way too complicated and error prone. Just use Noda Time. You'll be glad you did!
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.